izulu


Nameizulu JSON
Version 0.5.0 PyPI version JSON
download
home_page
SummaryAn exceptional library
upload_time2024-03-02 12:18:32
maintainer
docs_urlNone
author
requires_python>=3.6
license
keywords error exception oop izulu
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            izulu
=====

    *"An exceptional library"*


**Installation**

::

    pip install izulu


**Prepare playground**

::

    pip install ipython

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


Presenting ``izulu``: bring OOP into exception/error management
---------------------------------------------------------------

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


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

::

    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!

::

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

    class AmountValidationError(ValidationError):
        __template__ = f"{ValidationError.__template__} ({{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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    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
----------

**Prepare playground**

::

    pip install ipython

    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)*

::

    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``)

::

    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*

::

    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**

::

    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


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**)

Features are represented as flag enum ``Features`` with following options:

* ``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``
  =======  =============

::

    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``
  =======  =============

::

    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``
  =======  =============

::

    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``
  ================  ==============

::

    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'


Mechanics
^^^^^^^^^

::

    pip install ipython

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

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

::

    class AmountError(Error):
        pass

* **optionally** behaviour can be adjusted with ``__features__``

::

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

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

    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::

        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

  * only named fields are allowed

    * positional (digit) and empty field are forbidden

* ``__init__()`` accepts only ``kwargs``

::

    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"*

::

    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``

::

    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)

::

    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

::

    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 ::

        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) ::

        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__``

::

    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*

::

    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"*

::

    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

**Special notes**

* XXX *"fields"* defined in ``__template__`` rules

  * *"fields"* may refer class and instance *"defaults"*
  * you can omit them in ``kwargs`` or not (override defaults)
  * *"fields"* defined in ``__template__`` require these data in ``kwargs``

* *"defaults"* don't have to be ``__template__`` *"fields"*

  * there can be hints for attributes not present in error message template
  * and vice versa — there can be *"fields"* not present as instance attributes



**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``: constraint and argument issues
* ``ValueError``: template and formatting issues

Some exceptions are *raised from* original exception (e.g. templating 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 ::

     # 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

* runtime stage

  * validation is made during error instantiation ::

      root.Error()

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





**Recommended**


Additional APIs
^^^^^^^^^^^^^^^

Representations
"""""""""""""""

::

    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) ::

    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::

    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``

::

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

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

      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) ::

      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)

::

    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)

::

    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.

::

    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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    # 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

::

    from uuid import uuid4

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

* lambdas

::

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

* function

::

    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

::

    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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    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": "",
    "name": "izulu",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": "",
    "keywords": "error,exception,oop,izulu",
    "author": "",
    "author_email": "Dima Burmistrov <pyctrl.dev@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/ad/82/02f56c716119c4297e50ab4c84e38b304944022effcf978afd5ac1768545/izulu-0.5.0.tar.gz",
    "platform": null,
    "description": "izulu\n=====\n\n    *\"An exceptional library\"*\n\n\n**Installation**\n\n::\n\n    pip install izulu\n\n\n**Prepare playground**\n\n::\n\n    pip install ipython\n\n    ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n\nPresenting ``izulu``: bring OOP into exception/error management\n---------------------------------------------------------------\n\nYou can read docs *from top to bottom* or jump strait 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::\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::\n\n    class ValidationError(Error):\n        __template__ = \"Data is invalid: {reason}\"\n\n    class AmountValidationError(ValidationError):\n        __template__ = f\"{ValidationError.__template__} ({{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::\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::\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**Prepare playground**\n\n::\n\n    pip install ipython\n\n    ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n\n**Let's start with defining our initial error class (exception).**\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::\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\n**Move on and improve our class with attributes**\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::\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\n**We can provide defaults for our attributes**\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::\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\n**Dynamic defaults also supported**\n\n::\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\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**)\n\nFeatures are represented as flag enum ``Features`` with following options:\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::\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::\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::\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::\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\nMechanics\n^^^^^^^^^\n\n::\n\n    pip install ipython\n\n    ipython -i -c 'from izulu.root import *; from typing import *; from datetime import *'\n\n* inheritance from ``izulu.root.Error`` is required\n\n::\n\n    class AmountError(Error):\n        pass\n\n* **optionally** behaviour can be adjusted with ``__features__``\n\n::\n\n    class AmountError(Error):\n        __features__ = Features.FORBID_MISSING_FIELDS | Features.FORBID_KWARG_CONSTS\n\n* you should provide a template for the target error message with ``__template__`` ::\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        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  * only named fields are allowed\n\n    * positional (digit) and empty field are forbidden\n\n* ``__init__()`` accepts only ``kwargs``\n\n::\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::\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::\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::\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::\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        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        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::\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::\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::\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**Special notes**\n\n* XXX *\"fields\"* defined in ``__template__`` rules\n\n  * *\"fields\"* may refer class and instance *\"defaults\"*\n  * you can omit them in ``kwargs`` or not (override defaults)\n  * *\"fields\"* defined in ``__template__`` require these data in ``kwargs``\n\n* *\"defaults\"* don't have to be ``__template__`` *\"fields\"*\n\n  * there can be hints for attributes not present in error message template\n  * and vice versa \u2014 there can be *\"fields\"* not present as instance attributes\n\n\n\n**Validation and behavior in case of problems**\n\n``izulu`` may trigger native Python exceptions on invalid data during validation process.\nBy default you should expect following ones\n\n* ``TypeError``: constraint and argument issues\n* ``ValueError``: template and formatting issues\n\nSome exceptions are *raised from* original exception (e.g. templating 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     # 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* runtime stage\n\n  * validation is made during error instantiation ::\n\n      root.Error()\n\n  * ``kwargs`` are validated according to enabled features\n\n\n\n\n\n**Recommended**\n\n\nAdditional APIs\n^^^^^^^^^^^^^^^\n\nRepresentations\n\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n\n::\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    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    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\n\nPickling, dumping and loading\n\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\n\n**Pickling**\n\n``izulu``-based errors **support pickling** by default.\n\n\n**Dumping**\n\n* ``.as_kwargs()`` dumps shallow copy of original ``kwargs``\n\n::\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      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      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\n**Loading**\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::\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::\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::\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::\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::\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::\n\n    class MyError(Error):\n        timestamp: datetime = factory(lambda: datetime.now().isoformat())\n\n* function\n\n::\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::\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::\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": "",
    "summary": "An exceptional library",
    "version": "0.5.0",
    "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": "c4f43c85c22f7ffed3c050ab5356d496f87c8fd83ea3505df1bdd2e9f4ef8f68",
                "md5": "1c62c846bc6af234f04addafd325e7a5",
                "sha256": "2cd9d6cd49aac37fb2ecc4a3f18bd2e0e0d40b32c76c5123e658ab9c9b5f0b04"
            },
            "downloads": -1,
            "filename": "izulu-0.5.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "1c62c846bc6af234f04addafd325e7a5",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 13316,
            "upload_time": "2024-03-02T12:18:30",
            "upload_time_iso_8601": "2024-03-02T12:18:30.282983Z",
            "url": "https://files.pythonhosted.org/packages/c4/f4/3c85c22f7ffed3c050ab5356d496f87c8fd83ea3505df1bdd2e9f4ef8f68/izulu-0.5.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "ad8202f56c716119c4297e50ab4c84e38b304944022effcf978afd5ac1768545",
                "md5": "3f0421264e65bc40de48c1a938ced336",
                "sha256": "05f82736a8ae3d1cfa80c6006385cd44835031175b6820ab94b7d714d4e79840"
            },
            "downloads": -1,
            "filename": "izulu-0.5.0.tar.gz",
            "has_sig": false,
            "md5_digest": "3f0421264e65bc40de48c1a938ced336",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 24148,
            "upload_time": "2024-03-02T12:18:32",
            "upload_time_iso_8601": "2024-03-02T12:18:32.534842Z",
            "url": "https://files.pythonhosted.org/packages/ad/82/02f56c716119c4297e50ab4c84e38b304944022effcf978afd5ac1768545/izulu-0.5.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-02 12:18:32",
    "github": false,
    "gitlab": true,
    "bitbucket": false,
    "codeberg": false,
    "gitlab_user": "pyctrl",
    "gitlab_project": "izulu",
    "lcname": "izulu"
}
        
Elapsed time: 0.19901s