django-logic


Namedjango-logic JSON
Version 0.1.6 PyPI version JSON
download
home_pagehttps://github.com/Borderless360/django-logic
SummaryDjango Logic - easy way to implement state-based business logic
upload_time2025-07-11 05:00:35
maintainerNone
docs_urlNone
authorEmil Balashov
requires_python>=3.6
licenseMIT License
keywords django
VCS
bugtrack_url
requirements django django-model-utils djangorestframework
Travis-CI
coveralls test coverage No coveralls.
            ![django-logic](https://user-images.githubusercontent.com/6745569/87846635-dabb1500-c903-11ea-9fae-f1960dd2f82d.png)

[![Build Status](https://travis-ci.org/Borderless360/django-logic.svg?branch=master)](https://travis-ci.org/Borderless360/django-logic) 
[![Coverage Status](https://coveralls.io/repos/github/Borderless360/django-logic/badge.svg?branch=master)](https://coveralls.io/github/Borderless360/django-logic?branch=master)
     
Django Logic is a workflow framework allowing developers to implement the business logic via pure functions. 
It's designed based on [Finite-State-Machine (FSM)](https://en.wikipedia.org/wiki/Finite-state_machine) principles. 
Therefore, it needs to define a `state` field for a model's object. Every change of the `state` is performed by a 
transition and every transition could be grouped into a process. Also, you can define some side-effects that will be
executed during the transition from one state to another and callbacks that will be run after. 
This concept provides you a place for the business logic, rather than splitting it across the views, models, forms, 
serializers or even worse, in templates. 

## Definitions 
- **Transition** - class changes a state of an object from one to another. It also contains its own conditions,
 permissions, side-effects, callbacks, and failure callbacks. 
- **Action** - in contrast with the transition, the action does not change the state. 
But it contains its own conditions, permissions, side-effects, callbacks, and failure callbacks. 
- **Side-effects** - class defines a set of functions that executing within one particular transition
 before reaching the `target` state. During the execution, the state changes to the `in_progress` state.
 In case, if one of the functions interrupts the execution, then it changes to the `failed` state.
- **Callbacks** - class defines a set of functions that executing within one particular transition
 after reaching the `target` state. In case, if one of the functions interrupts the execution, it will log
 an exception and the execution will be stopped (without changing the state to failed). 
- **Failure callbacks** - class defines a set of functions that executing within one particular 
transition in case if one of the side-effects has been failed to execute. 
- **Conditions** - class defines a set of functions which receives an object
 and return `True` or `False` based on one particular requirement.
- **Permissions** - class defines a set of functions which receives an object and user, then returns `True` or 
`False` based on given permissions.
- **Process** - class defines a set of transitions with some common conditions and permissions.
It also accepts nested processes that allow building the hierarchy.

## Installation

Use the package manager [pip](https://pip.pypa.io/en/stable/) to install Django-Logic.

```bash
pip install django-logic
```

## Usage
0. Add to INSTALLED_APPS
```python
INSTALLED_APPS = (
    ...
    'django_logic',
    ...
)
```

1. Define django model with one or more state fields. 
```python
from django.db import models


MY_STATE_CHOICES = (
     ('draft', 'Draft'),
     ('approved', 'Approved'),
     ('paid', 'Paid'),
     ('void', 'Void'),
 )

class Invoice(models.Model):
    my_state = models.CharField(choices=MY_STATE_CHOICES, default='open', max_length=16, blank=True)    
    my_status = models.CharField(choices=MY_STATE_CHOICES, default='draft', max_length=16, blank=True)
    
```

2. Define a process class with some transitions.
```python
from django_logic import Process as BaseProcess, Transition, Action
from .choices import MY_STATE_CHOICES


class MyProcess(BaseProcess):
    states = MY_STATE_CHOICES
    transitions = [
        Transition(action_name='approve', sources=['draft'], target='approved'),
        Transition(action_name='pay', sources=['approve'], target='paid'),
        Transition(action_name='void', sources=['draft', 'approved'], target='void'),
        Action(action_name='update', side_effects=[update_data]),
    ]
```

3. Bind the process with a model.
```python
from django_logic import Process as BaseProcess, Transition, ProcessManager, Action
from .models import Invoice, MY_STATE_CHOICES


class MyProcess(BaseProcess):
    states = MY_STATE_CHOICES
    transitions = [
        Transition(action_name='approve', sources=['draft'], target='approved'),
        Transition(action_name='void', sources=['draft', 'approved'], target='void'),
        Action(action_name='update', side_effects=[update_data]),
    ]

ProcessManager.bind_model_process(Invoice, MyProcess, state_field='my_state')
``` 

4. Advance your process with conditions, side-effects, and callbacks into the process. Use next_transition to automatically continue the process. 
```python 
class MyProcess(BaseProcess):
    process_name = 'my_process' 
    permissions = [
        is_accountant, 
    ]
    states = MY_STATE_CHOICES
    transitions = [
        Transition(
            action_name='approve',
            sources=['draft'], 
            target='approved',
            conditions=[
                is_customer_active, 
            ]
            side_effects=[
                generate_pdf_invoice, 
            ],
            callbacks=[
                send_approved_invoice_email_to_accountant, 
            ],
            next_transition='pay' 
        ),
        Transition(
            action_name='pay',
            sources=['approved'],
            target='paid',
            side_effects=[
                make_payment, 
            ]
        ),         
        Transition(
            action_name='void', 
            callbacks=[
                send_void_invoice_email_to_accountant
            ],
            sources=['approved'],
            target='void'
        ),
        Action(
            action_name='update', 
            side_effects=[
                update_data
            ],
        ),
    ]
```

5. This approval process defines the business logic where:
- The user who performs the action must have accountant role (permission).
- It shouldn't be possible to invoice inactive customers (condition). 
- Once the invoice record is approved, it should generate a PDF file and send it to 
an accountant via email. (side-effects  and callbacks)
- If the invoice voided it needs to notify the accountant about that.
As you see, these business requirements should not know about each other. Furthermore, it gives a simple way 
to test every function separately as Django-Logic takes care of connection them into the business process.  

6. Execute in the code:
```python
from invoices.models import Invoice


def approve_view(request, pk):
    invoice = Invoice.objects.get(pk=pk)
    invoice.my_process.approve(user=request.user, context={'my_var': 1})
```
Use context to pass data between side-effects and callbacks.

7. If you want to override the value of the state field, it must be done explicitly. For example: 
```python
Invoice.objects.filter(status='draft').update(my_state='open')
# or 
invoice = Invoice.objects.get(pk=pk)
invoice.my_state = 'open'
invoice.save(update_fields=['my_state'])
```
Save without `update_fields` won't update the value of the state field in order to protect the data from corrupting. 

8. Error handling:
```python 
from django_logic.exceptions import TransitionNotAllowed

try:
    invoice.my_process.approve()
except TransitionNotAllowed:
    logger.error('Approve is not allowed') 
```

#### Display process
Drawing a process with the following elements:
- Process - a transparent rectangle 
- Transition - a grey rectangle 
- State - a transparent ellipse 
- Process' conditions and permissions are defined inside of related process as a transparent diamond
- Transition' conditions and permissions are defined inside of related transition's process as a grey diamond
   
[![][diagram-img]][diagram-img]

From this diagram you can visually check that the following the business requirements have been implemented properly:
- Personnel involved: User and Staff
- Lock has to be available before any actions taken. It's  defined by a condition  `is_lock_available`. 
- User is able to lock and unlock an available locker. 
- Staff is able to lock, unlock and put a locker under maintenance if such was planned.  


## Django-Logic vs Django FSM 
[Django FSM](https://github.com/viewflow/django-fsm) is a parent package of Django-Logic. 
It's been used in production for many years until the number of new ideas and requirements swamped us.
Therefore, it's been decided to implement these ideas under a new package. For example, supporting Processes or 
background transitions which were implemented under [Django-Logic-Celery](https://github.com/Borderless360/django-logic-celery).
Finally, we want to provide a standard way on where to put the business logic in Django by using Django-Logic. 

## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

## License
[MIT](https://choosealicense.com/licenses/mit/)

## Project status
Under development


[diagram-img]: https://user-images.githubusercontent.com/6745569/74101382-25c24680-4b74-11ea-8767-0eabd4f27ebc.png

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/Borderless360/django-logic",
    "name": "django-logic",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": null,
    "keywords": "django",
    "author": "Emil Balashov",
    "author_email": "emil@borderless360.com",
    "download_url": "https://files.pythonhosted.org/packages/d2/13/40a58edaf86d1be9f44d1d5ff03db9af1a3d86d07c293cad363ffea780ab/django_logic-0.1.6.tar.gz",
    "platform": "any",
    "description": "![django-logic](https://user-images.githubusercontent.com/6745569/87846635-dabb1500-c903-11ea-9fae-f1960dd2f82d.png)\n\n[![Build Status](https://travis-ci.org/Borderless360/django-logic.svg?branch=master)](https://travis-ci.org/Borderless360/django-logic) \n[![Coverage Status](https://coveralls.io/repos/github/Borderless360/django-logic/badge.svg?branch=master)](https://coveralls.io/github/Borderless360/django-logic?branch=master)\n     \nDjango Logic is a workflow framework allowing developers to implement the business logic via pure functions. \nIt's designed based on [Finite-State-Machine (FSM)](https://en.wikipedia.org/wiki/Finite-state_machine) principles. \nTherefore, it needs to define a `state` field for a model's object. Every change of the `state` is performed by a \ntransition and every transition could be grouped into a process. Also, you can define some side-effects that will be\nexecuted during the transition from one state to another and callbacks that will be run after. \nThis concept provides you a place for the business logic, rather than splitting it across the views, models, forms, \nserializers or even worse, in templates. \n\n## Definitions \n- **Transition** - class changes a state of an object from one to another. It also contains its own conditions,\n permissions, side-effects, callbacks, and failure callbacks. \n- **Action** - in contrast with the transition, the action does not change the state. \nBut it contains its own conditions, permissions, side-effects, callbacks, and failure callbacks. \n- **Side-effects** - class defines a set of functions that executing within one particular transition\n before reaching the `target` state. During the execution, the state changes to the `in_progress` state.\n In case, if one of the functions interrupts the execution, then it changes to the `failed` state.\n- **Callbacks** - class defines a set of functions that executing within one particular transition\n after reaching the `target` state. In case, if one of the functions interrupts the execution, it will log\n an exception and the execution will be stopped (without changing the state to failed). \n- **Failure callbacks** - class defines a set of functions that executing within one particular \ntransition in case if one of the side-effects has been failed to execute. \n- **Conditions** - class defines a set of functions which receives an object\n and return `True` or `False` based on one particular requirement.\n- **Permissions** - class defines a set of functions which receives an object and user, then returns `True` or \n`False` based on given permissions.\n- **Process** - class defines a set of transitions with some common conditions and permissions.\nIt also accepts nested processes that allow building the hierarchy.\n\n## Installation\n\nUse the package manager [pip](https://pip.pypa.io/en/stable/) to install Django-Logic.\n\n```bash\npip install django-logic\n```\n\n## Usage\n0. Add to INSTALLED_APPS\n```python\nINSTALLED_APPS = (\n    ...\n    'django_logic',\n    ...\n)\n```\n\n1. Define django model with one or more state fields. \n```python\nfrom django.db import models\n\n\nMY_STATE_CHOICES = (\n     ('draft', 'Draft'),\n     ('approved', 'Approved'),\n     ('paid', 'Paid'),\n     ('void', 'Void'),\n )\n\nclass Invoice(models.Model):\n    my_state = models.CharField(choices=MY_STATE_CHOICES, default='open', max_length=16, blank=True)    \n    my_status = models.CharField(choices=MY_STATE_CHOICES, default='draft', max_length=16, blank=True)\n    \n```\n\n2. Define a process class with some transitions.\n```python\nfrom django_logic import Process as BaseProcess, Transition, Action\nfrom .choices import MY_STATE_CHOICES\n\n\nclass MyProcess(BaseProcess):\n    states = MY_STATE_CHOICES\n    transitions = [\n        Transition(action_name='approve', sources=['draft'], target='approved'),\n        Transition(action_name='pay', sources=['approve'], target='paid'),\n        Transition(action_name='void', sources=['draft', 'approved'], target='void'),\n        Action(action_name='update', side_effects=[update_data]),\n    ]\n```\n\n3. Bind the process with a model.\n```python\nfrom django_logic import Process as BaseProcess, Transition, ProcessManager, Action\nfrom .models import Invoice, MY_STATE_CHOICES\n\n\nclass MyProcess(BaseProcess):\n    states = MY_STATE_CHOICES\n    transitions = [\n        Transition(action_name='approve', sources=['draft'], target='approved'),\n        Transition(action_name='void', sources=['draft', 'approved'], target='void'),\n        Action(action_name='update', side_effects=[update_data]),\n    ]\n\nProcessManager.bind_model_process(Invoice, MyProcess, state_field='my_state')\n``` \n\n4. Advance your process with conditions, side-effects, and callbacks into the process. Use next_transition to automatically continue the process. \n```python \nclass MyProcess(BaseProcess):\n    process_name = 'my_process' \n    permissions = [\n        is_accountant, \n    ]\n    states = MY_STATE_CHOICES\n    transitions = [\n        Transition(\n            action_name='approve',\n            sources=['draft'], \n            target='approved',\n            conditions=[\n                is_customer_active, \n            ]\n            side_effects=[\n                generate_pdf_invoice, \n            ],\n            callbacks=[\n                send_approved_invoice_email_to_accountant, \n            ],\n            next_transition='pay' \n        ),\n        Transition(\n            action_name='pay',\n            sources=['approved'],\n            target='paid',\n            side_effects=[\n                make_payment, \n            ]\n        ),         \n        Transition(\n            action_name='void', \n            callbacks=[\n                send_void_invoice_email_to_accountant\n            ],\n            sources=['approved'],\n            target='void'\n        ),\n        Action(\n            action_name='update', \n            side_effects=[\n                update_data\n            ],\n        ),\n    ]\n```\n\n5. This approval process defines the business logic where:\n- The user who performs the action must have accountant role (permission).\n- It shouldn't be possible to invoice inactive customers (condition). \n- Once the invoice record is approved, it should generate a PDF file and send it to \nan accountant via email. (side-effects  and callbacks)\n- If the invoice voided it needs to notify the accountant about that.\nAs you see, these business requirements should not know about each other. Furthermore, it gives a simple way \nto test every function separately as Django-Logic takes care of connection them into the business process.  \n\n6. Execute in the code:\n```python\nfrom invoices.models import Invoice\n\n\ndef approve_view(request, pk):\n    invoice = Invoice.objects.get(pk=pk)\n    invoice.my_process.approve(user=request.user, context={'my_var': 1})\n```\nUse context to pass data between side-effects and callbacks.\n\n7. If you want to override the value of the state field, it must be done explicitly. For example: \n```python\nInvoice.objects.filter(status='draft').update(my_state='open')\n# or \ninvoice = Invoice.objects.get(pk=pk)\ninvoice.my_state = 'open'\ninvoice.save(update_fields=['my_state'])\n```\nSave without `update_fields` won't update the value of the state field in order to protect the data from corrupting. \n\n8. Error handling:\n```python \nfrom django_logic.exceptions import TransitionNotAllowed\n\ntry:\n    invoice.my_process.approve()\nexcept TransitionNotAllowed:\n    logger.error('Approve is not allowed') \n```\n\n#### Display process\nDrawing a process with the following elements:\n- Process - a transparent rectangle \n- Transition - a grey rectangle \n- State - a transparent ellipse \n- Process' conditions and permissions are defined inside of related process as a transparent diamond\n- Transition' conditions and permissions are defined inside of related transition's process as a grey diamond\n   \n[![][diagram-img]][diagram-img]\n\nFrom this diagram you can visually check that the following the business requirements have been implemented properly:\n- Personnel involved: User and Staff\n- Lock has to be available before any actions taken. It's  defined by a condition  `is_lock_available`. \n- User is able to lock and unlock an available locker. \n- Staff is able to lock, unlock and put a locker under maintenance if such was planned.  \n\n\n## Django-Logic vs Django FSM \n[Django FSM](https://github.com/viewflow/django-fsm) is a parent package of Django-Logic. \nIt's been used in production for many years until the number of new ideas and requirements swamped us.\nTherefore, it's been decided to implement these ideas under a new package. For example, supporting Processes or \nbackground transitions which were implemented under [Django-Logic-Celery](https://github.com/Borderless360/django-logic-celery).\nFinally, we want to provide a standard way on where to put the business logic in Django by using Django-Logic. \n\n## Contributing\nPull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.\n\nPlease make sure to update tests as appropriate.\n\n## License\n[MIT](https://choosealicense.com/licenses/mit/)\n\n## Project status\nUnder development\n\n\n[diagram-img]: https://user-images.githubusercontent.com/6745569/74101382-25c24680-4b74-11ea-8767-0eabd4f27ebc.png\n",
    "bugtrack_url": null,
    "license": "MIT License",
    "summary": "Django Logic - easy way to implement state-based business logic",
    "version": "0.1.6",
    "project_urls": {
        "Homepage": "https://github.com/Borderless360/django-logic"
    },
    "split_keywords": [
        "django"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b41be52e5633edffbfbced7e48a74f24f589b40aae01f274f2e88ea7ee9da708",
                "md5": "74ee06928fd062d3b835925f02ed6b42",
                "sha256": "79cb11e04d282916c8ead3a7b639404ce53b82c8a2f3665a74926edb253c800b"
            },
            "downloads": -1,
            "filename": "django_logic-0.1.6-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "74ee06928fd062d3b835925f02ed6b42",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 13833,
            "upload_time": "2025-07-11T05:00:34",
            "upload_time_iso_8601": "2025-07-11T05:00:34.030280Z",
            "url": "https://files.pythonhosted.org/packages/b4/1b/e52e5633edffbfbced7e48a74f24f589b40aae01f274f2e88ea7ee9da708/django_logic-0.1.6-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d21340a58edaf86d1be9f44d1d5ff03db9af1a3d86d07c293cad363ffea780ab",
                "md5": "69bce9b34c624e494ae733abe7da839e",
                "sha256": "fa7585cb0defe0b01fd60a0c7a1c81a8134d982700493d4b13e6762bf243f9b0"
            },
            "downloads": -1,
            "filename": "django_logic-0.1.6.tar.gz",
            "has_sig": false,
            "md5_digest": "69bce9b34c624e494ae733abe7da839e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 18898,
            "upload_time": "2025-07-11T05:00:35",
            "upload_time_iso_8601": "2025-07-11T05:00:35.490921Z",
            "url": "https://files.pythonhosted.org/packages/d2/13/40a58edaf86d1be9f44d1d5ff03db9af1a3d86d07c293cad363ffea780ab/django_logic-0.1.6.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-07-11 05:00:35",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Borderless360",
    "github_project": "django-logic",
    "travis_ci": true,
    "coveralls": false,
    "github_actions": false,
    "requirements": [
        {
            "name": "django",
            "specs": [
                [
                    ">=",
                    "2.0"
                ]
            ]
        },
        {
            "name": "django-model-utils",
            "specs": [
                [
                    "==",
                    "4.5.1"
                ]
            ]
        },
        {
            "name": "djangorestframework",
            "specs": [
                [
                    "==",
                    "3.15.2"
                ]
            ]
        }
    ],
    "lcname": "django-logic"
}
        
Elapsed time: 0.83070s