Skip to main content

Python package that makes creating complicated business logic easy

Project description

=====================
Python Business Logic
=====================

.. image:: https://badge.fury.io/py/python-business-logic.svg
:target: https://badge.fury.io/py/python-business-logic

.. image:: https://travis-ci.org/Valian/python-business-logic.svg?branch=master
:target: https://travis-ci.org/Valian/python-business-logic

.. image:: https://codecov.io/gh/Valian/python-business-logic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/Valian/python-business-logic

.. image:: https://readthedocs.org/projects/python-business-logic/badge/?version=latest
:target: http://python-business-logic.readthedocs.io


Traditionally, most web applications are written using MVC pattern. Python Business Logic helps you to add *Business Layer*, also called *Application Layer*, that is dependent only on models and composed of simple functions. Code written this way is extremely easy to read, test, and use in different scenarios. Package has no dependencies and can be used in any web framework, like Django, Flask, Bottle and others.

Documentation
-------------

The full documentation is at https://python-business-logic.readthedocs.io. Still under development.

Installation
------------

Install Python Business Logic::

pip install python-business-logic


Getting Started
---------------

Core elements of library are validators, functions created to ensure that business logic is correct:

.. code:: python

>>> from business_logic.core import validator

>>> @validator
... def can_watch_movie(user, movie):
... # some example business logic, it can be as complex as you want
... return user.is_parent or user.age >= movie.age_restriction



With validators you can decorate actions performed that will be checked against that validator:

.. code:: python

>>> from business_logic.core import validated_by

>>> @validated_by(can_watch_movie)
... def watch_movie(user, movie):
... print("'{}' is watching movie '{}'".format(user.name, movie.name))



As you can see, arguments to validator must match those passed to function.
Now every call to :code:`watch_movie` will require that validator :code:`can_watch_movie` passes:

.. code:: python

>>> import collections
>>> User = collections.namedtuple('User', ['name', 'age', 'is_parent'])
>>> Movie = collections.namedtuple('Movie', ['name', 'age_restriction'])
>>> alice = User('Alice', 32, True)
>>> bob = User('Bob', 6, False)
>>> cartoon = Movie('Tom&Jerry', 0)
>>> horror = Movie('Scream', 18)

>>> watch_movie(bob, cartoon)
'Bob' is watching movie 'Tom&Jerry'

>>> watch_movie(alice, horror)
'Alice' is watching movie 'Scream'

>>> watch_movie(bob, horror) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
File "business_logic/core.py", line 48, in wrapper
raise ServiceException("Validation failed!")
business_logic.exceptions.LogicException: Validation failed!


You can skip validation using :code:`validate=False`:

.. code:: python

>>> watch_movie(user=bob, movie=horror, validate=False)
'Bob' is watching movie 'Scream'



Also, if we just want to know if action is permitted, just let's run:

.. code:: python

>>> validation = can_watch_movie(bob, horror, raise_exception=False)
>>> validation
<PermissionResult success=False error=Validation failed!>

>>> bool(validation)
False

>>> validation.error # it's an actual exception
LogicException('Validation failed!', error_code=None)



Chaining validators is really easy and readable:

.. code:: python

>>> @validator
... def is_old_enough(user, movie):
... return user.age >= movie.age_restriction

>>> @validator
... def can_watch_movie(user, movie):
... is_old_enough(user, movie)
... # we don't have to return anything, @validator makes use of exceptions

>>> can_watch_movie(bob, horror) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
File "business_logic/core.py", line 48, in wrapper
raise LogicException("Validation failed!")
business_logic.exceptions.LogicException: Validation failed!



Ok, but we're still missing something. We don't know why exactly validation failed,
all we have is a generic "Validation failed!" message. How to fix that? It's easy, let's
make our own errors!

.. code:: python

>>> from business_logic import LogicErrors, LogicException
>>> class AgeRestrictionErrors(LogicErrors):
... CANT_WATCH_MOVIE_TOO_YOUNG = LogicException("User is too young to watch this")

>>> @validator
... def is_old_enough(user, movie):
... if user.age < movie.age_restriction:
... raise AgeRestrictionErrors.CANT_WATCH_MOVIE_TOO_YOUNG

>>> is_old_enough(bob, horror) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
business_logic.exceptions.LogicException: User is too young to watch this

>>> # we can also obtain exception details like this
>>> result = is_old_enough(bob, horror, raise_exception=False)
>>> bool(result)
False

>>> result.error
LogicException('User is too young to watch this', error_code='CANT_WATCH_MOVIE_TOO_YOUNG')

>>> result.error_code == 'CANT_WATCH_MOVIE_TOO_YOUNG'
True

>>> # result.errors is shortcut to registry with all errors
>>> result.error == result.errors['CANT_WATCH_MOVIE_TOO_YOUNG']
True



Testing is really easy:

.. code:: python

>>> def test_user_cant_watch_movie_if_under_age_restriction():
... bob = User('Bob', 6, False)
... horror = Movie('Scream', 18)
... result = is_old_enough(bob, horror, raise_exception=False)
... # There are two ways to check if expected exceptions was raised
... assert result.error_code == 'CANT_WATCH_MOVIE_TOO_YOUNG'
... assert result.error == AgeRestrictionErrors.CANT_WATCH_MOVIE_TOO_YOUNG

>>> test_user_cant_watch_movie_if_under_age_restriction()


Also, if you need to display parametrizable error messages, just use `.format` method

.. code:: python
>>>
>>> exc = LogicException('User {user} is way too young!', error_code='TOO_YOUNG')
>>> formatted_exc = exc.format(user='Bob')
>>> assert str(formatted_exc) == 'User Bob is way too young!'
>>> assert exc.error_code == formatted_exc.error_code
>>> assert exc == formatted_exc

Usage
-----

When using this package, you should write all your business logic as simple functions, using only
inputs and Database Layer (eg. `Django ORM or SQLAlchemy`). This way, you can easily test your
logic and use it in any way you like. Convention that I follow is to put all functions inside `logic.py` file or `logic` submodule.

In **views** and **API** calls: Your role is to prepare all required data for business function (from forms, user session etc), call function
and present results to user. Middleware catching LogicException and, for example, displaying message to user in a generic way
can improve readability a lot, because no exception handling need to be done in view.

As **management commands**: In Django you can create custom `management command`, that allows you to use cli to perform custom logic.
Python Business Logic functions works very well for that use case!

From **external code**: Just import your function and use it. Since there shouldn't be any framework-related
inputs other than Database Models, usage is really simple. In reality, your business functions form *business API* of your application.

Examples
--------

For examples how to use this library, look into directory *examples*. Currently there is only one called *Football match*. Most important file there is :code:`logic.py`.


Running Tests
-------------

Does the code actually work?

::

$ pip install -r requirements_test.txt
$ tox




History
-------

0.2.0 (2017-10-22)
++++++++++++++++++

* Support for Python 3.7
* Parametrizable errors

0.1.1 (2017-10-22)
++++++++++++++++++

* Fixed encoding in python 2.7.

0.1.0 (2017-07-16)
++++++++++++++++++

* First release on PyPI.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python-business-logic-0.2.0.tar.gz (9.7 kB view details)

Uploaded Source

Built Distribution

python_business_logic-0.2.0-py2.py3-none-any.whl (11.3 kB view details)

Uploaded Python 2 Python 3

File details

Details for the file python-business-logic-0.2.0.tar.gz.

File metadata

File hashes

Hashes for python-business-logic-0.2.0.tar.gz
Algorithm Hash digest
SHA256 224304ce47fdc8151cb65e86ed164a9f310addd06a1a1eeaea3484e34011583e
MD5 e64a1e7d5158f670e90b8b1b222f52d6
BLAKE2b-256 e01e50dfd868855f7d652edae5a855540163b5b422c0f4382b57604b278333f6

See more details on using hashes here.

File details

Details for the file python_business_logic-0.2.0-py2.py3-none-any.whl.

File metadata

File hashes

Hashes for python_business_logic-0.2.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 d6b76ab56de979c96cfa91b771d54be6f3982a854e9e2df52db97330ee755bc1
MD5 eabac8e80a513c4805bf11a60cb65471
BLAKE2b-256 293ce9f00491b77436c5135c79029491cd6791888042374da57927cc5cc7b97e

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page