izulu


Nameizulu JSON
Version 0.5.3 PyPI version JSON
download
home_pageNone
SummaryThe exceptional library
upload_time2024-12-10 00:07:30
maintainerNone
docs_urlNone
authorNone
requires_python>=3.6
licenseCopyright (c) 2023-2024 Dmitry Burmistrov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization.
keywords error exception oop izulu
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            izulu
#####

.. image:: https://gitlab.com/uploads/-/system/project/avatar/50698236/izulu_logo_512.png?width=128

|

    *"The exceptional library"*

|


**Installation**

::

    pip install izulu


Presenting ``izulu``
********************

Bring OOP into exception/error management
=========================================

You can read docs *from top to bottom* or jump straight into **"Quickstart"** section.
For details note **"Specifications"** sections below.


Neat #1: Stop messing with raw strings and manual message formatting
--------------------------------------------------------------------

.. code-block:: python

    if not data:
        raise ValueError("Data is invalid: no data")

    amount = data["amount"]
    if amount < 0:
        raise ValueError(f"Data is invalid: amount can't be negative ({amount})")
    elif amount > 1000:
        raise ValueError(f"Data is invalid: amount is too large ({amount})")

    if data["status"] not in {"READY", "IN_PROGRESS"}:
        raise ValueError("Data is invalid: unprocessable status")

With ``izulu`` you can forget about manual error message management all over the codebase!

.. code-block:: python

    class ValidationError(Error):
        __template__ = "Data is invalid: {reason}"

    class AmountValidationError(ValidationError):
        __template__ = "Invalid amount: {amount}"


    if not data:
        raise ValidationError(reason="no data")

    amount = data["amount"]
    if amount < 0:
        raise AmountValidationError(reason="amount can't be negative", amount=amount)
    elif amount > 1000:
        raise AmountValidationError(reason="amount is too large", amount=amount)

    if data["status"] not in {"READY", "IN_PROGRESS"}:
        raise ValidationError(reason="unprocessable status")


Provide only variable data for error instantiations. Keep static data within error class.

Under the hood ``kwargs`` are used to format ``__template__`` into final error message.


Neat #2: Attribute errors with useful fields
--------------------------------------------

.. code-block:: python

    from falcon import HTTPBadRequest

    class AmountValidationError(ValidationError):
        __template__ = "Data is invalid: {reason} ({amount})"
        amount: int


    try:
        validate(data)
    except AmountValidationError as e:
        if e.amount < 0:
            raise HTTPBadRequest(f"Bad amount: {e.amount}")
        raise


Annotated instance attributes automatically populated from ``kwargs``.


Neat #3: Static and dynamic defaults
------------------------------------

.. code-block:: python

    class AmountValidationError(ValidationError):
        __template__ = "Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}"
        _MAX: ClassVar[int] = 1000
        amount: int
        reason: str = "amount is too large"
        ts: datetime = factory(datetime.now)


    print(AmountValidationError(amount=15000))
    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 22:59:25.132699

    print(AmountValidationError(amount=-1, reason="amount can't be negative"))
    # Data is invalid: amount can't be negative (-1; MAX=1000) at 2024-01-13 22:59:54.482577


Quickstart
==========

.. note::

    **Prepare playground**

    ::

        pip install ipython izulu

        ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'


Let's start with defining our initial error class (exception)
-------------------------------------------------------------

#. subclass ``Error``
#. provide special message template for each of your exceptions
#. use **only kwargs** to instantiate exception *(no more message copying across the codebase)*

.. code-block:: python

    class MyError(Error):
        __template__ = "Having count={count} for owner={owner}"


    print(MyError(count=10, owner="me"))
    # MyError: Having count=10 for owner=me

    MyError(10, owner="me")
    # TypeError: __init__() takes 1 positional argument but 2 were given


Move on and improve our class with attributes
---------------------------------------------

#. define annotations for fields you want to publish as exception instance attributes
#. you have to define desired template fields in annotations too
   (see ``AttributeError`` for ``owner``)
#. you can provide annotation for attributes not included in template (see ``timestamp``)
#. **type hinting from annotations are not enforced or checked** (see ``timestamp``)

.. code-block:: python

    class MyError(Error):
        __template__ = "Having count={count} for owner={owner}"
        count: int
        timestamp: datetime

    e = MyError(count=10, owner="me", timestamp=datetime.now())

    print(e.count)
    # 10
    print(e.timestamp)
    # 2023-09-27 18:18:22.957925

    e.owner
    # AttributeError: 'MyError' object has no attribute 'owner'


We can provide defaults for our attributes
------------------------------------------

#. define *default static values* after field annotation just as usual
#. for *dynamic defaults* use provided ``factory`` tool with your callable - it would be
   evaluated without arguments during exception instantiation
#. now fields would receive values from ``kwargs`` if present - otherwise from *defaults*

.. code-block:: python

    class MyError(Error):
        __template__ = "Having count={count} for owner={owner}"
        count: int
        owner: str = "nobody"
        timestamp: datetime = factory(datetime.now)

    e = MyError(count=10)

    print(e.count)
    # 10
    print(e.owner)
    # nobody
    print(e.timestamp)
    # 2023-09-27 18:19:37.252577


Dynamic defaults also supported
-------------------------------

.. code-block:: python

    class MyError(Error):
        __template__ = "Having count={count} for owner={owner}"

        count: int
        begin: datetime
        owner: str = "nobody"
        timestamp: datetime = factory(datetime.now)
        duration: timedelta = factory(lambda self: self.timestamp - self.begin, self=True)


    begin = datetime.fromordinal(date.today().toordinal())
    e = MyError(count=10, begin=begin)

    print(e.begin)
    # 2023-09-27 00:00:00
    print(e.duration)
    # 18:45:44.502490
    print(e.timestamp)
    # 2023-09-27 18:45:44.502490


* very similar to dynamic defaults, but callable must accept single
  argument - your exception fresh instance
* **don't forget** to provide second ``True`` argument for ``factory`` tool
  (keyword or positional - doesn't matter)


Specifications
**************

``izulu`` bases on class definitions to provide handy instance creation.


The 6 pillars of ``izulu``
==========================

* all behavior is defined on the class-level

* ``__template__`` class attribute defines the template for target error message

  * template may contain *"fields"* for substitution from ``kwargs`` and *"defaults"* to produce final error message

* ``__features__`` class attribute defines constraints and behaviour (see "Features" section below)

  * by default all constraints are enabled

* *"class hints"* annotated with ``ClassVar`` are noted by ``izulu``

  * annotated class attributes normally should have values (treated as *"class defaults"*)
  * *"class defaults"* can only be static
  * *"class defaults"* may be referred within ``__template__``

* *"instance hints"* regularly annotated (not with ``ClassVar``) are noted by ``izulu``

  * all annotated attributes are treated as *"instance attributes"*
  * each *"instance attribute"* will automatically obtain value from the ``kwarg`` of the same name
  * *"instance attributes"* with default are also treated as *"instance defaults"*
  * *"instance defaults"* may be **static and dynamic**
  * *"instance defaults"* may be referred within ``__template__``

* ``kwargs`` — the new and main way to form exceptions/error instance

  * forget about creating exception instances from message strings
  * ``kwargs`` are the datasource for template *"fields"* and *"instance attributes"*
    (shared input for templating attribution)

.. warning:: **Types from type hints are not validated or enforced!**


Mechanics
=========

.. note::

    **Prepare playground**

    ::

        pip install ipython izulu

        ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'


* inheritance from ``izulu.root.Error`` is required

.. code-block:: python

    class AmountError(Error):
        pass

* **optionally** behaviour can be adjusted with ``__features__`` (not recommended)

.. code-block:: python

    class AmountError(Error):
        __features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS

* you should provide a template for the target error message with ``__template__``

  .. code-block:: python

    class AmountError(Error):
        __template__ = "Data is invalid: {reason} (amount={amount})"

    print(AmountError(reason="negative amount", amount=-10.52))
    # [2024-01-23 19:16] Data is invalid: negative amount (amount=-10.52)

  * sources of formatting arguments:

    * *"class defaults"*
    * *"instance defaults"*
    * ``kwargs`` (overlap any *"default"*)

  * new style formatting is used:

    .. code-block:: python

      class AmountError(Error):
          __template__ = "[{ts:%Y-%m-%d %H:%M}] Data is invalid: {reason:_^20} (amount={amount:06.2f})"

      print(AmountError(ts=datetime.now(), reason="negative amount", amount=-10.52))
      # [2024-01-23 19:16] Data is invalid: __negative amount___ (amount=-10.52)

    * ``help(str.format)``
    * https://pyformat.info/
    * https://docs.python.org/3/library/string.html#format-string-syntax

      .. warning::
        There is a difference between docs and actual behaviour:
        https://discuss.python.org/t/format-string-syntax-specification-differs-from-actual-behaviour/46716

  * only named fields are allowed

    * positional (digit) and empty field are forbidden

* error instantiation requires data to format ``__template__``

  * all data for ``__template__`` fields must be provided

    .. code-block:: python

      class AmountError(Error):
          __template__ = "Data is invalid: {reason} (amount={amount})"

      print(AmountError(reason="amount can't be negative", amount=-10))
      # Data is invalid: amount can't be negative (amount=-10)

      AmountError()
      # TypeError: Missing arguments: 'reason', 'amount'
      AmountError(amount=-10)
      # TypeError: Missing arguments: 'reason'

  * only named arguments allowed: ``__init__()`` accepts only ``kwargs``

    .. code-block:: python

      class AmountError(Error):
          __template__ = "Data is invalid: {reason} (amount={amount})"

      print(AmountError(reason="amount can't be negative", amount=-10))
      # Data is invalid: amount can't be negative (amount=-10)

      AmountError("amount can't be negative", -10)
      # TypeError: __init__() takes 1 positional argument but 3 were given
      AmountError("amount can't be negative", amount=-10)
      # TypeError: __init__() takes 1 positional argument but 2 were given

* *"class defaults"* can be defined and used

  * *"class defaults"* must be type hinted with ``ClassVar`` annotation and provide static values
  * template *"fields"* may refer *"class defaults"*

.. code-block:: python

    class AmountError(Error):
        LIMIT: ClassVar[int] = 10_000
        __template__ = "Amount is too large: amount={amount} limit={LIMIT}"
        amount: int

    print(AmountError(amount=10_500))
    # Amount is too large: amount=10500 limit=10000

* *"instance attributes"* are populated from relevant ``kwargs``

.. code-block:: python

    class AmountError(Error):
        amount: int

    print(AmountError(amount=-10).amount)
    # -10

* instance and class attribute types from **annotations are not validated or enforced**
  (``izulu`` uses type hints just for attribute discovery and only ``ClassVar`` marker
  is processed for instance/class segregation)

.. code-block:: python

    class AmountError(Error):
        amount: int

    print(AmountError(amount="lots of money").amount)
    # lots of money

* static *"instance defaults"* can be provided regularly with instance type hints and static values

.. code-block:: python

    class AmountError(Error):
        amount: int = 500

    print(AmountError().amount)
    # 500

* dynamic *"instance defaults"* are also supported

  * they must be type hinted and have special value
  * value must be a callable object wrapped with ``factory`` helper
  * ``factory`` provides 2 modes depending on value of the ``self`` flag:

    * ``self=False`` (default): callable accepting no arguments

      .. code-block:: python

        class AmountError(Error):
            ts: datetime = factory(datetime.now)

        print(AmountError().ts)
        # 2024-01-23 23:18:22.019963

    * ``self=True``: provide callable accepting single argument (error instance)

      .. code-block:: python

        class AmountError(Error):
            LIMIT = 10_000
            amount: int
            overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)

        print(AmountError(amount=10_500).overflow)
        # 500

* *"instance defaults"* and *"instance attributes"* may be referred in ``__template__``

.. code-block:: python

    class AmountError(Error):
        __template__ = "[{ts:%Y-%m-%d %H:%M}] Amount is too large: {amount}"
        amount: int
        ts: datetime = factory(datetime.now)

    print(AmountError(amount=10_500))
    # [2024-01-23 23:21] Amount is too large: 10500

* *Pause and sum up: defaults, attributes and template*

.. code-block:: python

    class AmountError(Error):
        LIMIT: ClassVar[int] = 10_000
        __template__ = "[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}"
        amount: int
        overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)
        ts: datetime = factory(datetime.now)

    err = AmountError(amount=15_000)

    print(err.amount)
    # 15000
    print(err.LIMIT)
    # 10000
    print(err.overflow)
    # 5000
    print(err.ts)
    # 2024-01-23 23:21:26

    print(err)
    # [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000

* ``kwargs`` overlap *"instance defaults"*

.. code-block:: python

    class AmountError(Error):
        LIMIT: ClassVar[int] = 10_000
        __template__ = "[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}"
        amount: int = 15_000
        overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)
        ts: datetime = factory(datetime.now)

    print(AmountError())
    # [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000

    print(AmountError(amount=10_333, overflow=42, ts=datetime(1900, 1, 1)))
    # [2024-01-23 23:21] Amount is too large: amount=10333 limit=10000 overflow=42

* ``izulu`` provides flexibility for templates, fields, attributes and defaults

  * *"defaults"* are not required to be ``__template__`` *"fields"*

    .. code-block:: python

      class AmountError(Error):
          LIMIT: ClassVar[int] = 10_000
          __template__ = "Amount is too large"

      print(AmountError().LIMIT)
      # 10000
      print(AmountError())
      # Amount is too large

  * there can be hints for attributes not present in error message template

    .. code-block:: python

      class AmountError(Error):
          __template__ = "Amount is too large"
          amount: int

      print(AmountError(amount=500).amount)
      # 500
      print(AmountError(amount=500))
      # Amount is too large

  * *"fields"* don't have to be hinted as instance attributes

    .. code-block:: python

      class AmountError(Error):
          __template__ = "Amount is too large: {amount}"

      print(AmountError(amount=500))
      # Amount is too large: 500
      print(AmountError(amount=500).amount)
      # AttributeError: 'AmountError' object has no attribute 'amount'


Features
========

The ``izulu`` error class behaviour is controlled by ``__features__`` class attribute.

(For details about "runtime" and "class definition" stages
see **Validation and behavior in case of problems** below)


Supported features
------------------

* ``FORBID_MISSING_FIELDS``: checks provided ``kwargs`` contain data for all template *"fields"*
  and *"instance attributes"* that have no *"defaults"*

  * always should be enabled (provides consistent and detailed ``TypeError`` exceptions
    for appropriate arguments)
  * if disabled raw exceptions from ``izulu`` machinery internals could appear

  =======  =============
   Stage      Raises
  =======  =============
  runtime  ``TypeError``
  =======  =============

.. code-block:: python

    class AmountError(Error):
        __template__ = "Some {amount} of money for {client_id} client"
        client_id: int

    # I. enabled
    AmountError()
    # TypeError: Missing arguments: client_id, amount

    # II. disabled
    AmountError.__features__ ^= Features.FORBID_MISSING_FIELDS

    AmountError()
    # ValueError: Failed to format template with provided kwargs:

* ``FORBID_UNDECLARED_FIELDS``: forbids undefined arguments in provided ``kwargs``
  (names not present in template *"fields"* and *"instance/class hints"*)

  * if disabled allows and **completely ignores** unknown data in ``kwargs``

  =======  =============
   Stage      Raises
  =======  =============
  runtime  ``TypeError``
  =======  =============

.. code-block:: python

    class MyError(Error):
        __template__ = "My error occurred"

    # I. enabled
    MyError(unknown_data="data")
    # Undeclared arguments: unknown_data

    # II. disabled
    MyError.__features__ ^= Features.FORBID_UNDECLARED_FIELDS
    err = MyError(unknown_data="data")

    print(err)
    # Unspecified error
    print(repr(err))
    # __main__.MyError(unknown_data='data')
    err.unknown_data
    # AttributeError: 'MyError' object has no attribute 'unknown_data'

* ``FORBID_KWARG_CONSTS``: checks provided ``kwargs`` not to contain attributes defined as ``ClassVar``

  * if disabled allows data in ``kwargs`` to overlap class attributes during template formatting
  * overlapping data won't modify class attribute values

  =======  =============
   Stage      Raises
  =======  =============
  runtime  ``TypeError``
  =======  =============

.. code-block:: python

    class MyError(Error):
        __template__ = "My error occurred {_TYPE}"
        _TYPE: ClassVar[str]

    # I. enabled
    MyError(_TYPE="SOME_ERROR_TYPE")
    # TypeError: Constants in arguments: _TYPE

    # II. disabled
    MyError.__features__ ^= Features.FORBID_KWARG_CONSTS
    err = MyError(_TYPE="SOME_ERROR_TYPE")

    print(err)
    # My error occurred SOME_ERROR_TYPE
    print(repr(err))
    # __main__.MyError(_TYPE='SOME_ERROR_TYPE')
    err._TYPE
    # AttributeError: 'MyError' object has no attribute '_TYPE'

* ``FORBID_NON_NAMED_FIELDS``: forbids empty and digit field names in ``__template__``

  * if disabled validation (runtime issues)
  * ``izulu`` relies on ``kwargs`` and named fields
  * by default it's forbidden to provide empty (``{}``) and digit (``{0}``) fields in ``__template__``

  ================  ==============
   Stage               Raises
  ================  ==============
  class definition  ``ValueError``
  ================  ==============

.. code-block:: python

    class MyError(Error):
        __template__ = "My error occurred {_TYPE}"
        _TYPE: ClassVar[str]

    # I. enabled
    MyError(_TYPE="SOME_ERROR_TYPE")
    # TypeError: Constants in arguments: _TYPE

    # II. disabled
    MyError.__features__ ^= Features.FORBID_KWARG_CONSTS
    err = MyError(_TYPE="SOME_ERROR_TYPE")

    print(err)
    # My error occurred SOME_ERROR_TYPE
    print(repr(err))
    # __main__.MyError(_TYPE='SOME_ERROR_TYPE')
    err._TYPE
    # AttributeError: 'MyError' object has no attribute '_TYPE'


Tuning ``__features__``
-----------------------

Features are represented as *"Flag Enum"*, so you can use regular operations
to configure desired behaviour.
Examples:

* Use single option

.. code-block:: python

    class AmountError(Error):
        __features__ = Features.FORBID_MISSING_FIELDS

* Use presets

.. code-block:: python

    class AmountError(Error):
        __features__ = Features.NONE

* Combining wanted features:

.. code-block:: python

    class AmountError(Error):
        __features__ = Features.FORBID_MISSING_FIELDS | Features.FORBID_KWARG_CONSTS

* Discarding unwanted feature from default feature set:

.. code-block:: python

    class AmountError(Error):
        __features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS


Validation and behavior in case of problems
===========================================

``izulu`` may trigger native Python exceptions on invalid data during validation process.
By default you should expect following ones

* ``TypeError``: argument constraints issues
* ``ValueError``: template and formatting issues

Some exceptions are *raised from* original exception (e.g. template formatting issues),
so you can check ``e.__cause__`` and traceback output for details.


The validation behavior depends on the set of enabled features.
Changing feature set may cause different and raw exceptions being raised.
Read and understand **"Features"** section to predict and experiment with different situations and behaviours.


``izulu`` has **2 validation stages:**

* class definition stage

  * validation is made during error class definition

    .. code-block:: python

      # when you import error module
      from izulu import root

      # when you import error from module
      from izulu.root import Error

      # when you interactively define new error classes
      class MyError(Error):
          pass

  * class attributes ``__template__`` and ``__features__`` are validated

    .. code-block:: python

      class MyError(Error):
          __template__ = "Hello {}"

      # ValueError: Field names can't be empty

* runtime stage

  * validation is made during error instantiation

    .. code-block:: python

      root.Error()

  * ``kwargs`` are validated according to enabled features

    .. code-block:: python

      class MyError(Error):
          __template__ = "Hello {name}"

      MyError()
      # TypeError: Missing arguments: 'name'


Additional APIs
===============


Representations
---------------

.. code-block:: python

    class AmountValidationError(Error):
        __template__ = "Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}"
        _MAX: ClassVar[int] = 1000
        amount: int
        reason: str = "amount is too large"
        ts: datetime = factory(datetime.now)


    err = AmountValidationError(amount=15000)

    print(str(err))
    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586

    print(repr(err))
    # __main__.AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')


* ``str`` and ``repr`` output differs
* ``str`` is for humans and Python (Python dictates the result to be exactly and only the message)
* ``repr`` allows to reconstruct the same error instance from its output
  (if data provided into ``kwargs`` supports ``repr`` the same way)

  **note:** class name is fully qualified name of class (dot-separated module full path with class name)

  .. code-block:: python

    reconstructed = eval(repr(err).replace("__main__.", "", 1))

    print(str(reconstructed))
    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586

    print(repr(reconstructed))
    # AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')

* in addition to ``str`` there is another human-readable representations provided by ``.as_str()`` method;
  it prepends message with class name:

  .. code-block:: python

    print(err.as_str())
    # AmountValidationError: Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586


Pickling, dumping and loading
-----------------------------

Pickling
""""""""

``izulu``-based errors **support pickling** by default.


Dumping
"""""""

* ``.as_kwargs()`` dumps shallow copy of original ``kwargs``

.. code-block:: python

    err.as_kwargs()
    # {'amount': 15000}

* ``.as_dict()`` by default, combines original ``kwargs`` and all *"instance attribute"* values into *"full state"*

  .. code-block:: python

    err.as_dict()
    # {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large'}

  Additionally, there is the ``wide`` flag for enriching the result with *"class defaults"*
  (note additional ``_MAX`` data)

  .. code-block:: python

    err.as_dict(True)
    # {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large', '_MAX': 1000}

  Data combination process follows prioritization — if there are multiple values for same name then high priority data
  will overlap data with lower priority. Here is the prioritized list of data sources:

  #. ``kwargs`` (max priority)
  #. *"instance attributes"*
  #. *"class defaults"*


Loading
"""""""

* ``.as_kwargs()`` result can be used to create **inaccurate** copy of original error,
  but pay attention to dynamic factories — ``datetime.now()``, ``uuid()`` and many others would produce new values
  for data missing in ``kwargs`` (note ``ts`` field in the example below)

.. code-block:: python

    inaccurate_copy = AmountValidationError(**err.as_kwargs())

    print(inaccurate_copy)
    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080
    print(repr(inaccurate_copy))
    # __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))

* ``.as_dict()`` result can be used to create **accurate** copy of original error;
  flag ``wide`` should be ``False`` by default according to ``FORBID_KWARG_CONSTS`` restriction
  (if you disable ``FORBID_KWARG_CONSTS`` then you may need to use ``wide=True`` depending on your situation)

.. code-block:: python

    accurate_copy = AmountValidationError(**err.as_dict())

    print(accurate_copy)
    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080
    print(repr(accurate_copy))
    # __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))


(advanced) Wedge
----------------

There is a special method you can override and additionally manage the machinery.

But it should not be need in 99,9% cases. Avoid it, please.

.. code-block:: python

    def _hook(self,
              store: _utils.Store,
              kwargs: dict[str, t.Any],
              msg: str) -> str:
        """Adapter method to wedge user logic into izulu machinery

        This is the place to override message/formatting if regular mechanics
        don't work for you. It has to return original or your flavored message.
        The method is invoked between izulu preparations and original
        `Exception` constructor receiving the result of this hook.

        You can also do any other logic here. You will be provided with
        complete set of prepared data from izulu. But it's recommended
        to use classic OOP inheritance for ordinary behaviour extension.

        Params:
          * store: dataclass containing inner error class specifications
          * kwargs: original kwargs from user
          * msg: formatted message from the error template
        """

        return msg


Tips
****

1. inheritance / root exception
===============================

.. code-block:: python

    # intermediate class to centrally control the default behaviour
    class BaseError(Error):  # <-- inherit from this in your code (not directly from ``izulu``)
        __features__ = Features.None


    class MyRealError(BaseError):
        __template__ = "Having count={count} for owner={owner}"


2. factories
============

TODO: self=True / self.as_kwargs()  (as_dict forbidden? - recursion)


* stdlib factories

.. code-block:: python

    from uuid import uuid4

    class MyError(Error):
        id: datetime = factory(uuid4)
        timestamp: datetime = factory(datetime.now)

* lambdas

.. code-block:: python

    class MyError(Error):
        timestamp: datetime = factory(lambda: datetime.now().isoformat())

* function

.. code-block:: python

    from random import randint

    def flip_coin():
        return "TAILS" if randint(0, 100) % 2 else "HEADS

    class MyError(Error):
        coin: str = factory(flip_coin)


* method

.. code-block:: python

    class MyError(Error):
        __template__ = "Having count={count} for owner={owner}"

        def __make_duration(self) -> timedelta:
            kwargs = self.as_kwargs()
            return self.timestamp - kwargs["begin"]

        timestamp: datetime = factory(datetime.now)
        duration: timedelta = factory(__make_duration, self=True)


    begin = datetime.fromordinal(date.today().toordinal())
    e = MyError(count=10, begin=begin)

    print(e.begin)
    # 2023-09-27 00:00:00
    print(e.duration)
    # 18:45:44.502490
    print(e.timestamp)
    # 2023-09-27 18:45:44.502490


3. handling errors in presentation layers / APIs
================================================

.. code-block:: python

    err = Error()
    view = RespModel(error=err.as_dict(wide=True)


    class MyRealError(BaseError):
        __template__ = "Having count={count} for owner={owner}"


Additional examples
-------------------

TBD


For developers
**************

* Running tests::

    tox

* Building package::

    tox -e build

* Contributing: contact me through `Issues <https://gitlab.com/pyctrl/izulu/-/issues>`__


Versioning
**********

`SemVer <http://semver.org/>`__ used for versioning.
For available versions see the repository
`tags <https://gitlab.com/pyctrl/izulu/-/tags>`__
and `releases <https://gitlab.com/pyctrl/izulu/-/releases>`__.


Authors
*******

-  **Dima Burmistrov** - *Initial work* -
   `pyctrl <https://gitlab.com/pyctrl/>`__

*Special thanks to* `Eugene Frolov <https://github.com/phantomii/>`__ *for inspiration.*


License
*******

This project is licensed under the X11 License (extended MIT) - see the
`LICENSE <https://gitlab.com/pyctrl/izulu/-/blob/main/LICENSE>`__ file for details

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "izulu",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": null,
    "keywords": "error, exception, oop, izulu",
    "author": null,
    "author_email": "Dima Burmistrov <pyctrl.dev@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/45/ec/4bf45aaba7831efa9ea35925e0ec1ac6422ed82eddac34322f53cc6cff77/izulu-0.5.3.tar.gz",
    "platform": null,
    "description": "izulu\n#####\n\n.. image:: https://gitlab.com/uploads/-/system/project/avatar/50698236/izulu_logo_512.png?width=128\n\n|\n\n    *\"The exceptional library\"*\n\n|\n\n\n**Installation**\n\n::\n\n    pip install izulu\n\n\nPresenting ``izulu``\n********************\n\nBring OOP into exception/error management\n=========================================\n\nYou can read docs *from top to bottom* or jump straight into **\"Quickstart\"** section.\nFor details note **\"Specifications\"** sections below.\n\n\nNeat #1: Stop messing with raw strings and manual message formatting\n--------------------------------------------------------------------\n\n.. code-block:: python\n\n    if not data:\n        raise ValueError(\"Data is invalid: no data\")\n\n    amount = data[\"amount\"]\n    if amount < 0:\n        raise ValueError(f\"Data is invalid: amount can't be negative ({amount})\")\n    elif amount > 1000:\n        raise ValueError(f\"Data is invalid: amount is too large ({amount})\")\n\n    if data[\"status\"] not in {\"READY\", \"IN_PROGRESS\"}:\n        raise ValueError(\"Data is invalid: unprocessable status\")\n\nWith ``izulu`` you can forget about manual error message management all over the codebase!\n\n.. code-block:: python\n\n    class ValidationError(Error):\n        __template__ = \"Data is invalid: {reason}\"\n\n    class AmountValidationError(ValidationError):\n        __template__ = \"Invalid amount: {amount}\"\n\n\n    if not data:\n        raise ValidationError(reason=\"no data\")\n\n    amount = data[\"amount\"]\n    if amount < 0:\n        raise AmountValidationError(reason=\"amount can't be negative\", amount=amount)\n    elif amount > 1000:\n        raise AmountValidationError(reason=\"amount is too large\", amount=amount)\n\n    if data[\"status\"] not in {\"READY\", \"IN_PROGRESS\"}:\n        raise ValidationError(reason=\"unprocessable status\")\n\n\nProvide only variable data for error instantiations. Keep static data within error class.\n\nUnder the hood ``kwargs`` are used to format ``__template__`` into final error message.\n\n\nNeat #2: Attribute errors with useful fields\n--------------------------------------------\n\n.. code-block:: python\n\n    from falcon import HTTPBadRequest\n\n    class AmountValidationError(ValidationError):\n        __template__ = \"Data is invalid: {reason} ({amount})\"\n        amount: int\n\n\n    try:\n        validate(data)\n    except AmountValidationError as e:\n        if e.amount < 0:\n            raise HTTPBadRequest(f\"Bad amount: {e.amount}\")\n        raise\n\n\nAnnotated instance attributes automatically populated from ``kwargs``.\n\n\nNeat #3: Static and dynamic defaults\n------------------------------------\n\n.. code-block:: python\n\n    class AmountValidationError(ValidationError):\n        __template__ = \"Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}\"\n        _MAX: ClassVar[int] = 1000\n        amount: int\n        reason: str = \"amount is too large\"\n        ts: datetime = factory(datetime.now)\n\n\n    print(AmountValidationError(amount=15000))\n    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 22:59:25.132699\n\n    print(AmountValidationError(amount=-1, reason=\"amount can't be negative\"))\n    # Data is invalid: amount can't be negative (-1; MAX=1000) at 2024-01-13 22:59:54.482577\n\n\nQuickstart\n==========\n\n.. note::\n\n    **Prepare playground**\n\n    ::\n\n        pip install ipython izulu\n\n        ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n\nLet's start with defining our initial error class (exception)\n-------------------------------------------------------------\n\n#. subclass ``Error``\n#. provide special message template for each of your exceptions\n#. use **only kwargs** to instantiate exception *(no more message copying across the codebase)*\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"Having count={count} for owner={owner}\"\n\n\n    print(MyError(count=10, owner=\"me\"))\n    # MyError: Having count=10 for owner=me\n\n    MyError(10, owner=\"me\")\n    # TypeError: __init__() takes 1 positional argument but 2 were given\n\n\nMove on and improve our class with attributes\n---------------------------------------------\n\n#. define annotations for fields you want to publish as exception instance attributes\n#. you have to define desired template fields in annotations too\n   (see ``AttributeError`` for ``owner``)\n#. you can provide annotation for attributes not included in template (see ``timestamp``)\n#. **type hinting from annotations are not enforced or checked** (see ``timestamp``)\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"Having count={count} for owner={owner}\"\n        count: int\n        timestamp: datetime\n\n    e = MyError(count=10, owner=\"me\", timestamp=datetime.now())\n\n    print(e.count)\n    # 10\n    print(e.timestamp)\n    # 2023-09-27 18:18:22.957925\n\n    e.owner\n    # AttributeError: 'MyError' object has no attribute 'owner'\n\n\nWe can provide defaults for our attributes\n------------------------------------------\n\n#. define *default static values* after field annotation just as usual\n#. for *dynamic defaults* use provided ``factory`` tool with your callable - it would be\n   evaluated without arguments during exception instantiation\n#. now fields would receive values from ``kwargs`` if present - otherwise from *defaults*\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"Having count={count} for owner={owner}\"\n        count: int\n        owner: str = \"nobody\"\n        timestamp: datetime = factory(datetime.now)\n\n    e = MyError(count=10)\n\n    print(e.count)\n    # 10\n    print(e.owner)\n    # nobody\n    print(e.timestamp)\n    # 2023-09-27 18:19:37.252577\n\n\nDynamic defaults also supported\n-------------------------------\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"Having count={count} for owner={owner}\"\n\n        count: int\n        begin: datetime\n        owner: str = \"nobody\"\n        timestamp: datetime = factory(datetime.now)\n        duration: timedelta = factory(lambda self: self.timestamp - self.begin, self=True)\n\n\n    begin = datetime.fromordinal(date.today().toordinal())\n    e = MyError(count=10, begin=begin)\n\n    print(e.begin)\n    # 2023-09-27 00:00:00\n    print(e.duration)\n    # 18:45:44.502490\n    print(e.timestamp)\n    # 2023-09-27 18:45:44.502490\n\n\n* very similar to dynamic defaults, but callable must accept single\n  argument - your exception fresh instance\n* **don't forget** to provide second ``True`` argument for ``factory`` tool\n  (keyword or positional - doesn't matter)\n\n\nSpecifications\n**************\n\n``izulu`` bases on class definitions to provide handy instance creation.\n\n\nThe 6 pillars of ``izulu``\n==========================\n\n* all behavior is defined on the class-level\n\n* ``__template__`` class attribute defines the template for target error message\n\n  * template may contain *\"fields\"* for substitution from ``kwargs`` and *\"defaults\"* to produce final error message\n\n* ``__features__`` class attribute defines constraints and behaviour (see \"Features\" section below)\n\n  * by default all constraints are enabled\n\n* *\"class hints\"* annotated with ``ClassVar`` are noted by ``izulu``\n\n  * annotated class attributes normally should have values (treated as *\"class defaults\"*)\n  * *\"class defaults\"* can only be static\n  * *\"class defaults\"* may be referred within ``__template__``\n\n* *\"instance hints\"* regularly annotated (not with ``ClassVar``) are noted by ``izulu``\n\n  * all annotated attributes are treated as *\"instance attributes\"*\n  * each *\"instance attribute\"* will automatically obtain value from the ``kwarg`` of the same name\n  * *\"instance attributes\"* with default are also treated as *\"instance defaults\"*\n  * *\"instance defaults\"* may be **static and dynamic**\n  * *\"instance defaults\"* may be referred within ``__template__``\n\n* ``kwargs`` \u2014 the new and main way to form exceptions/error instance\n\n  * forget about creating exception instances from message strings\n  * ``kwargs`` are the datasource for template *\"fields\"* and *\"instance attributes\"*\n    (shared input for templating attribution)\n\n.. warning:: **Types from type hints are not validated or enforced!**\n\n\nMechanics\n=========\n\n.. note::\n\n    **Prepare playground**\n\n    ::\n\n        pip install ipython izulu\n\n        ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n\n* inheritance from ``izulu.root.Error`` is required\n\n.. code-block:: python\n\n    class AmountError(Error):\n        pass\n\n* **optionally** behaviour can be adjusted with ``__features__`` (not recommended)\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS\n\n* you should provide a template for the target error message with ``__template__``\n\n  .. code-block:: python\n\n    class AmountError(Error):\n        __template__ = \"Data is invalid: {reason} (amount={amount})\"\n\n    print(AmountError(reason=\"negative amount\", amount=-10.52))\n    # [2024-01-23 19:16] Data is invalid: negative amount (amount=-10.52)\n\n  * sources of formatting arguments:\n\n    * *\"class defaults\"*\n    * *\"instance defaults\"*\n    * ``kwargs`` (overlap any *\"default\"*)\n\n  * new style formatting is used:\n\n    .. code-block:: python\n\n      class AmountError(Error):\n          __template__ = \"[{ts:%Y-%m-%d %H:%M}] Data is invalid: {reason:_^20} (amount={amount:06.2f})\"\n\n      print(AmountError(ts=datetime.now(), reason=\"negative amount\", amount=-10.52))\n      # [2024-01-23 19:16] Data is invalid: __negative amount___ (amount=-10.52)\n\n    * ``help(str.format)``\n    * https://pyformat.info/\n    * https://docs.python.org/3/library/string.html#format-string-syntax\n\n      .. warning::\n        There is a difference between docs and actual behaviour:\n        https://discuss.python.org/t/format-string-syntax-specification-differs-from-actual-behaviour/46716\n\n  * only named fields are allowed\n\n    * positional (digit) and empty field are forbidden\n\n* error instantiation requires data to format ``__template__``\n\n  * all data for ``__template__`` fields must be provided\n\n    .. code-block:: python\n\n      class AmountError(Error):\n          __template__ = \"Data is invalid: {reason} (amount={amount})\"\n\n      print(AmountError(reason=\"amount can't be negative\", amount=-10))\n      # Data is invalid: amount can't be negative (amount=-10)\n\n      AmountError()\n      # TypeError: Missing arguments: 'reason', 'amount'\n      AmountError(amount=-10)\n      # TypeError: Missing arguments: 'reason'\n\n  * only named arguments allowed: ``__init__()`` accepts only ``kwargs``\n\n    .. code-block:: python\n\n      class AmountError(Error):\n          __template__ = \"Data is invalid: {reason} (amount={amount})\"\n\n      print(AmountError(reason=\"amount can't be negative\", amount=-10))\n      # Data is invalid: amount can't be negative (amount=-10)\n\n      AmountError(\"amount can't be negative\", -10)\n      # TypeError: __init__() takes 1 positional argument but 3 were given\n      AmountError(\"amount can't be negative\", amount=-10)\n      # TypeError: __init__() takes 1 positional argument but 2 were given\n\n* *\"class defaults\"* can be defined and used\n\n  * *\"class defaults\"* must be type hinted with ``ClassVar`` annotation and provide static values\n  * template *\"fields\"* may refer *\"class defaults\"*\n\n.. code-block:: python\n\n    class AmountError(Error):\n        LIMIT: ClassVar[int] = 10_000\n        __template__ = \"Amount is too large: amount={amount} limit={LIMIT}\"\n        amount: int\n\n    print(AmountError(amount=10_500))\n    # Amount is too large: amount=10500 limit=10000\n\n* *\"instance attributes\"* are populated from relevant ``kwargs``\n\n.. code-block:: python\n\n    class AmountError(Error):\n        amount: int\n\n    print(AmountError(amount=-10).amount)\n    # -10\n\n* instance and class attribute types from **annotations are not validated or enforced**\n  (``izulu`` uses type hints just for attribute discovery and only ``ClassVar`` marker\n  is processed for instance/class segregation)\n\n.. code-block:: python\n\n    class AmountError(Error):\n        amount: int\n\n    print(AmountError(amount=\"lots of money\").amount)\n    # lots of money\n\n* static *\"instance defaults\"* can be provided regularly with instance type hints and static values\n\n.. code-block:: python\n\n    class AmountError(Error):\n        amount: int = 500\n\n    print(AmountError().amount)\n    # 500\n\n* dynamic *\"instance defaults\"* are also supported\n\n  * they must be type hinted and have special value\n  * value must be a callable object wrapped with ``factory`` helper\n  * ``factory`` provides 2 modes depending on value of the ``self`` flag:\n\n    * ``self=False`` (default): callable accepting no arguments\n\n      .. code-block:: python\n\n        class AmountError(Error):\n            ts: datetime = factory(datetime.now)\n\n        print(AmountError().ts)\n        # 2024-01-23 23:18:22.019963\n\n    * ``self=True``: provide callable accepting single argument (error instance)\n\n      .. code-block:: python\n\n        class AmountError(Error):\n            LIMIT = 10_000\n            amount: int\n            overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)\n\n        print(AmountError(amount=10_500).overflow)\n        # 500\n\n* *\"instance defaults\"* and *\"instance attributes\"* may be referred in ``__template__``\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __template__ = \"[{ts:%Y-%m-%d %H:%M}] Amount is too large: {amount}\"\n        amount: int\n        ts: datetime = factory(datetime.now)\n\n    print(AmountError(amount=10_500))\n    # [2024-01-23 23:21] Amount is too large: 10500\n\n* *Pause and sum up: defaults, attributes and template*\n\n.. code-block:: python\n\n    class AmountError(Error):\n        LIMIT: ClassVar[int] = 10_000\n        __template__ = \"[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}\"\n        amount: int\n        overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)\n        ts: datetime = factory(datetime.now)\n\n    err = AmountError(amount=15_000)\n\n    print(err.amount)\n    # 15000\n    print(err.LIMIT)\n    # 10000\n    print(err.overflow)\n    # 5000\n    print(err.ts)\n    # 2024-01-23 23:21:26\n\n    print(err)\n    # [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000\n\n* ``kwargs`` overlap *\"instance defaults\"*\n\n.. code-block:: python\n\n    class AmountError(Error):\n        LIMIT: ClassVar[int] = 10_000\n        __template__ = \"[{ts:%Y-%m-%d %H:%M}] Amount is too large: amount={amount} limit={LIMIT} overflow={overflow}\"\n        amount: int = 15_000\n        overflow: int = factory(lambda self: self.amount - self.LIMIT, self=True)\n        ts: datetime = factory(datetime.now)\n\n    print(AmountError())\n    # [2024-01-23 23:21] Amount is too large: amount=15000 limit=10000 overflow=5000\n\n    print(AmountError(amount=10_333, overflow=42, ts=datetime(1900, 1, 1)))\n    # [2024-01-23 23:21] Amount is too large: amount=10333 limit=10000 overflow=42\n\n* ``izulu`` provides flexibility for templates, fields, attributes and defaults\n\n  * *\"defaults\"* are not required to be ``__template__`` *\"fields\"*\n\n    .. code-block:: python\n\n      class AmountError(Error):\n          LIMIT: ClassVar[int] = 10_000\n          __template__ = \"Amount is too large\"\n\n      print(AmountError().LIMIT)\n      # 10000\n      print(AmountError())\n      # Amount is too large\n\n  * there can be hints for attributes not present in error message template\n\n    .. code-block:: python\n\n      class AmountError(Error):\n          __template__ = \"Amount is too large\"\n          amount: int\n\n      print(AmountError(amount=500).amount)\n      # 500\n      print(AmountError(amount=500))\n      # Amount is too large\n\n  * *\"fields\"* don't have to be hinted as instance attributes\n\n    .. code-block:: python\n\n      class AmountError(Error):\n          __template__ = \"Amount is too large: {amount}\"\n\n      print(AmountError(amount=500))\n      # Amount is too large: 500\n      print(AmountError(amount=500).amount)\n      # AttributeError: 'AmountError' object has no attribute 'amount'\n\n\nFeatures\n========\n\nThe ``izulu`` error class behaviour is controlled by ``__features__`` class attribute.\n\n(For details about \"runtime\" and \"class definition\" stages\nsee **Validation and behavior in case of problems** below)\n\n\nSupported features\n------------------\n\n* ``FORBID_MISSING_FIELDS``: checks provided ``kwargs`` contain data for all template *\"fields\"*\n  and *\"instance attributes\"* that have no *\"defaults\"*\n\n  * always should be enabled (provides consistent and detailed ``TypeError`` exceptions\n    for appropriate arguments)\n  * if disabled raw exceptions from ``izulu`` machinery internals could appear\n\n  =======  =============\n   Stage      Raises\n  =======  =============\n  runtime  ``TypeError``\n  =======  =============\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __template__ = \"Some {amount} of money for {client_id} client\"\n        client_id: int\n\n    # I. enabled\n    AmountError()\n    # TypeError: Missing arguments: client_id, amount\n\n    # II. disabled\n    AmountError.__features__ ^= Features.FORBID_MISSING_FIELDS\n\n    AmountError()\n    # ValueError: Failed to format template with provided kwargs:\n\n* ``FORBID_UNDECLARED_FIELDS``: forbids undefined arguments in provided ``kwargs``\n  (names not present in template *\"fields\"* and *\"instance/class hints\"*)\n\n  * if disabled allows and **completely ignores** unknown data in ``kwargs``\n\n  =======  =============\n   Stage      Raises\n  =======  =============\n  runtime  ``TypeError``\n  =======  =============\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"My error occurred\"\n\n    # I. enabled\n    MyError(unknown_data=\"data\")\n    # Undeclared arguments: unknown_data\n\n    # II. disabled\n    MyError.__features__ ^= Features.FORBID_UNDECLARED_FIELDS\n    err = MyError(unknown_data=\"data\")\n\n    print(err)\n    # Unspecified error\n    print(repr(err))\n    # __main__.MyError(unknown_data='data')\n    err.unknown_data\n    # AttributeError: 'MyError' object has no attribute 'unknown_data'\n\n* ``FORBID_KWARG_CONSTS``: checks provided ``kwargs`` not to contain attributes defined as ``ClassVar``\n\n  * if disabled allows data in ``kwargs`` to overlap class attributes during template formatting\n  * overlapping data won't modify class attribute values\n\n  =======  =============\n   Stage      Raises\n  =======  =============\n  runtime  ``TypeError``\n  =======  =============\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"My error occurred {_TYPE}\"\n        _TYPE: ClassVar[str]\n\n    # I. enabled\n    MyError(_TYPE=\"SOME_ERROR_TYPE\")\n    # TypeError: Constants in arguments: _TYPE\n\n    # II. disabled\n    MyError.__features__ ^= Features.FORBID_KWARG_CONSTS\n    err = MyError(_TYPE=\"SOME_ERROR_TYPE\")\n\n    print(err)\n    # My error occurred SOME_ERROR_TYPE\n    print(repr(err))\n    # __main__.MyError(_TYPE='SOME_ERROR_TYPE')\n    err._TYPE\n    # AttributeError: 'MyError' object has no attribute '_TYPE'\n\n* ``FORBID_NON_NAMED_FIELDS``: forbids empty and digit field names in ``__template__``\n\n  * if disabled validation (runtime issues)\n  * ``izulu`` relies on ``kwargs`` and named fields\n  * by default it's forbidden to provide empty (``{}``) and digit (``{0}``) fields in ``__template__``\n\n  ================  ==============\n   Stage               Raises\n  ================  ==============\n  class definition  ``ValueError``\n  ================  ==============\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"My error occurred {_TYPE}\"\n        _TYPE: ClassVar[str]\n\n    # I. enabled\n    MyError(_TYPE=\"SOME_ERROR_TYPE\")\n    # TypeError: Constants in arguments: _TYPE\n\n    # II. disabled\n    MyError.__features__ ^= Features.FORBID_KWARG_CONSTS\n    err = MyError(_TYPE=\"SOME_ERROR_TYPE\")\n\n    print(err)\n    # My error occurred SOME_ERROR_TYPE\n    print(repr(err))\n    # __main__.MyError(_TYPE='SOME_ERROR_TYPE')\n    err._TYPE\n    # AttributeError: 'MyError' object has no attribute '_TYPE'\n\n\nTuning ``__features__``\n-----------------------\n\nFeatures are represented as *\"Flag Enum\"*, so you can use regular operations\nto configure desired behaviour.\nExamples:\n\n* Use single option\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __features__ = Features.FORBID_MISSING_FIELDS\n\n* Use presets\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __features__ = Features.NONE\n\n* Combining wanted features:\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __features__ = Features.FORBID_MISSING_FIELDS | Features.FORBID_KWARG_CONSTS\n\n* Discarding unwanted feature from default feature set:\n\n.. code-block:: python\n\n    class AmountError(Error):\n        __features__ = Features.DEFAULT ^ Features.FORBID_UNDECLARED_FIELDS\n\n\nValidation and behavior in case of problems\n===========================================\n\n``izulu`` may trigger native Python exceptions on invalid data during validation process.\nBy default you should expect following ones\n\n* ``TypeError``: argument constraints issues\n* ``ValueError``: template and formatting issues\n\nSome exceptions are *raised from* original exception (e.g. template formatting issues),\nso you can check ``e.__cause__`` and traceback output for details.\n\n\nThe validation behavior depends on the set of enabled features.\nChanging feature set may cause different and raw exceptions being raised.\nRead and understand **\"Features\"** section to predict and experiment with different situations and behaviours.\n\n\n``izulu`` has **2 validation stages:**\n\n* class definition stage\n\n  * validation is made during error class definition\n\n    .. code-block:: python\n\n      # when you import error module\n      from izulu import root\n\n      # when you import error from module\n      from izulu.root import Error\n\n      # when you interactively define new error classes\n      class MyError(Error):\n          pass\n\n  * class attributes ``__template__`` and ``__features__`` are validated\n\n    .. code-block:: python\n\n      class MyError(Error):\n          __template__ = \"Hello {}\"\n\n      # ValueError: Field names can't be empty\n\n* runtime stage\n\n  * validation is made during error instantiation\n\n    .. code-block:: python\n\n      root.Error()\n\n  * ``kwargs`` are validated according to enabled features\n\n    .. code-block:: python\n\n      class MyError(Error):\n          __template__ = \"Hello {name}\"\n\n      MyError()\n      # TypeError: Missing arguments: 'name'\n\n\nAdditional APIs\n===============\n\n\nRepresentations\n---------------\n\n.. code-block:: python\n\n    class AmountValidationError(Error):\n        __template__ = \"Data is invalid: {reason} ({amount}; MAX={_MAX}) at {ts}\"\n        _MAX: ClassVar[int] = 1000\n        amount: int\n        reason: str = \"amount is too large\"\n        ts: datetime = factory(datetime.now)\n\n\n    err = AmountValidationError(amount=15000)\n\n    print(str(err))\n    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586\n\n    print(repr(err))\n    # __main__.AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')\n\n\n* ``str`` and ``repr`` output differs\n* ``str`` is for humans and Python (Python dictates the result to be exactly and only the message)\n* ``repr`` allows to reconstruct the same error instance from its output\n  (if data provided into ``kwargs`` supports ``repr`` the same way)\n\n  **note:** class name is fully qualified name of class (dot-separated module full path with class name)\n\n  .. code-block:: python\n\n    reconstructed = eval(repr(err).replace(\"__main__.\", \"\", 1))\n\n    print(str(reconstructed))\n    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586\n\n    print(repr(reconstructed))\n    # AmountValidationError(amount=15000, ts=datetime.datetime(2024, 1, 13, 23, 33, 13, 847586), reason='amount is too large')\n\n* in addition to ``str`` there is another human-readable representations provided by ``.as_str()`` method;\n  it prepends message with class name:\n\n  .. code-block:: python\n\n    print(err.as_str())\n    # AmountValidationError: Data is invalid: amount is too large (15000; MAX=1000) at 2024-01-13 23:33:13.847586\n\n\nPickling, dumping and loading\n-----------------------------\n\nPickling\n\"\"\"\"\"\"\"\"\n\n``izulu``-based errors **support pickling** by default.\n\n\nDumping\n\"\"\"\"\"\"\"\n\n* ``.as_kwargs()`` dumps shallow copy of original ``kwargs``\n\n.. code-block:: python\n\n    err.as_kwargs()\n    # {'amount': 15000}\n\n* ``.as_dict()`` by default, combines original ``kwargs`` and all *\"instance attribute\"* values into *\"full state\"*\n\n  .. code-block:: python\n\n    err.as_dict()\n    # {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large'}\n\n  Additionally, there is the ``wide`` flag for enriching the result with *\"class defaults\"*\n  (note additional ``_MAX`` data)\n\n  .. code-block:: python\n\n    err.as_dict(True)\n    # {'amount': 15000, 'ts': datetime(2024, 1, 13, 23, 33, 13, 847586), 'reason': 'amount is too large', '_MAX': 1000}\n\n  Data combination process follows prioritization \u2014 if there are multiple values for same name then high priority data\n  will overlap data with lower priority. Here is the prioritized list of data sources:\n\n  #. ``kwargs`` (max priority)\n  #. *\"instance attributes\"*\n  #. *\"class defaults\"*\n\n\nLoading\n\"\"\"\"\"\"\"\n\n* ``.as_kwargs()`` result can be used to create **inaccurate** copy of original error,\n  but pay attention to dynamic factories \u2014 ``datetime.now()``, ``uuid()`` and many others would produce new values\n  for data missing in ``kwargs`` (note ``ts`` field in the example below)\n\n.. code-block:: python\n\n    inaccurate_copy = AmountValidationError(**err.as_kwargs())\n\n    print(inaccurate_copy)\n    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080\n    print(repr(inaccurate_copy))\n    # __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))\n\n* ``.as_dict()`` result can be used to create **accurate** copy of original error;\n  flag ``wide`` should be ``False`` by default according to ``FORBID_KWARG_CONSTS`` restriction\n  (if you disable ``FORBID_KWARG_CONSTS`` then you may need to use ``wide=True`` depending on your situation)\n\n.. code-block:: python\n\n    accurate_copy = AmountValidationError(**err.as_dict())\n\n    print(accurate_copy)\n    # Data is invalid: amount is too large (15000; MAX=1000) at 2024-02-01 21:11:21.681080\n    print(repr(accurate_copy))\n    # __main__.AmountValidationError(amount=15000, reason='amount is too large', ts=datetime.datetime(2024, 2, 1, 21, 11, 21, 681080))\n\n\n(advanced) Wedge\n----------------\n\nThere is a special method you can override and additionally manage the machinery.\n\nBut it should not be need in 99,9% cases. Avoid it, please.\n\n.. code-block:: python\n\n    def _hook(self,\n              store: _utils.Store,\n              kwargs: dict[str, t.Any],\n              msg: str) -> str:\n        \"\"\"Adapter method to wedge user logic into izulu machinery\n\n        This is the place to override message/formatting if regular mechanics\n        don't work for you. It has to return original or your flavored message.\n        The method is invoked between izulu preparations and original\n        `Exception` constructor receiving the result of this hook.\n\n        You can also do any other logic here. You will be provided with\n        complete set of prepared data from izulu. But it's recommended\n        to use classic OOP inheritance for ordinary behaviour extension.\n\n        Params:\n          * store: dataclass containing inner error class specifications\n          * kwargs: original kwargs from user\n          * msg: formatted message from the error template\n        \"\"\"\n\n        return msg\n\n\nTips\n****\n\n1. inheritance / root exception\n===============================\n\n.. code-block:: python\n\n    # intermediate class to centrally control the default behaviour\n    class BaseError(Error):  # <-- inherit from this in your code (not directly from ``izulu``)\n        __features__ = Features.None\n\n\n    class MyRealError(BaseError):\n        __template__ = \"Having count={count} for owner={owner}\"\n\n\n2. factories\n============\n\nTODO: self=True / self.as_kwargs()  (as_dict forbidden? - recursion)\n\n\n* stdlib factories\n\n.. code-block:: python\n\n    from uuid import uuid4\n\n    class MyError(Error):\n        id: datetime = factory(uuid4)\n        timestamp: datetime = factory(datetime.now)\n\n* lambdas\n\n.. code-block:: python\n\n    class MyError(Error):\n        timestamp: datetime = factory(lambda: datetime.now().isoformat())\n\n* function\n\n.. code-block:: python\n\n    from random import randint\n\n    def flip_coin():\n        return \"TAILS\" if randint(0, 100) % 2 else \"HEADS\n\n    class MyError(Error):\n        coin: str = factory(flip_coin)\n\n\n* method\n\n.. code-block:: python\n\n    class MyError(Error):\n        __template__ = \"Having count={count} for owner={owner}\"\n\n        def __make_duration(self) -> timedelta:\n            kwargs = self.as_kwargs()\n            return self.timestamp - kwargs[\"begin\"]\n\n        timestamp: datetime = factory(datetime.now)\n        duration: timedelta = factory(__make_duration, self=True)\n\n\n    begin = datetime.fromordinal(date.today().toordinal())\n    e = MyError(count=10, begin=begin)\n\n    print(e.begin)\n    # 2023-09-27 00:00:00\n    print(e.duration)\n    # 18:45:44.502490\n    print(e.timestamp)\n    # 2023-09-27 18:45:44.502490\n\n\n3. handling errors in presentation layers / APIs\n================================================\n\n.. code-block:: python\n\n    err = Error()\n    view = RespModel(error=err.as_dict(wide=True)\n\n\n    class MyRealError(BaseError):\n        __template__ = \"Having count={count} for owner={owner}\"\n\n\nAdditional examples\n-------------------\n\nTBD\n\n\nFor developers\n**************\n\n* Running tests::\n\n    tox\n\n* Building package::\n\n    tox -e build\n\n* Contributing: contact me through `Issues <https://gitlab.com/pyctrl/izulu/-/issues>`__\n\n\nVersioning\n**********\n\n`SemVer <http://semver.org/>`__ used for versioning.\nFor available versions see the repository\n`tags <https://gitlab.com/pyctrl/izulu/-/tags>`__\nand `releases <https://gitlab.com/pyctrl/izulu/-/releases>`__.\n\n\nAuthors\n*******\n\n-  **Dima Burmistrov** - *Initial work* -\n   `pyctrl <https://gitlab.com/pyctrl/>`__\n\n*Special thanks to* `Eugene Frolov <https://github.com/phantomii/>`__ *for inspiration.*\n\n\nLicense\n*******\n\nThis project is licensed under the X11 License (extended MIT) - see the\n`LICENSE <https://gitlab.com/pyctrl/izulu/-/blob/main/LICENSE>`__ file for details\n",
    "bugtrack_url": null,
    "license": "Copyright (c) 2023-2024 Dmitry Burmistrov  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.  Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization. ",
    "summary": "The exceptional library",
    "version": "0.5.3",
    "project_urls": {
        "Bug Tracker": "https://gitlab.com/pyctrl/izulu/-/issues",
        "Homepage": "https://gitlab.com/pyctrl/izulu"
    },
    "split_keywords": [
        "error",
        " exception",
        " oop",
        " izulu"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0119b0d61c3731f9fe62d8e0ae54d928d2e3c35117852a5e3370584c63768eab",
                "md5": "e256ffebc0a6a448cc828cab4dada825",
                "sha256": "9120697416aed99c33b7f873cb6a6d4dc822d16958f97cfd56db25aacbecb3db"
            },
            "downloads": -1,
            "filename": "izulu-0.5.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "e256ffebc0a6a448cc828cab4dada825",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 15005,
            "upload_time": "2024-12-10T00:07:27",
            "upload_time_iso_8601": "2024-12-10T00:07:27.830714Z",
            "url": "https://files.pythonhosted.org/packages/01/19/b0d61c3731f9fe62d8e0ae54d928d2e3c35117852a5e3370584c63768eab/izulu-0.5.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "45ec4bf45aaba7831efa9ea35925e0ec1ac6422ed82eddac34322f53cc6cff77",
                "md5": "581aee6b56e66facbe4c78c88f1c81b2",
                "sha256": "54987b8eec77808984cd5e91b629ad3971d01161c7ebea7166851ec7a19e2fe0"
            },
            "downloads": -1,
            "filename": "izulu-0.5.3.tar.gz",
            "has_sig": false,
            "md5_digest": "581aee6b56e66facbe4c78c88f1c81b2",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 34788,
            "upload_time": "2024-12-10T00:07:30",
            "upload_time_iso_8601": "2024-12-10T00:07:30.918989Z",
            "url": "https://files.pythonhosted.org/packages/45/ec/4bf45aaba7831efa9ea35925e0ec1ac6422ed82eddac34322f53cc6cff77/izulu-0.5.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-12-10 00:07:30",
    "github": false,
    "gitlab": true,
    "bitbucket": false,
    "codeberg": false,
    "gitlab_user": "pyctrl",
    "gitlab_project": "izulu",
    "lcname": "izulu"
}
        
Elapsed time: 0.40722s