├── test ├── __init__.py └── test_functional.py ├── commandbus ├── __init__.py ├── _exception.py └── _model.py ├── Pipfile ├── setup.py ├── .gitignore ├── LICENSE ├── README.md └── Pipfile.lock /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commandbus/__init__.py: -------------------------------------------------------------------------------- 1 | from ._exception import * 2 | from ._model import Command, CommandHandler, CommandBus 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | verify_ssl = true 4 | name = "pypi" 5 | url = "https://pypi.python.org/simple" 6 | 7 | 8 | [dev-packages] 9 | 10 | coverage = "*" 11 | pytest = "*" 12 | -------------------------------------------------------------------------------- /commandbus/_exception.py: -------------------------------------------------------------------------------- 1 | class CommandBusException(Exception): 2 | pass 3 | 4 | 5 | class CommandAlreadyExistException(CommandBusException): 6 | pass 7 | 8 | 9 | class CommandHandlerDoesNotExistException(CommandBusException): 10 | pass 11 | 12 | 13 | class CommandExecutionAlreadyInProgressException(CommandBusException): 14 | pass 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="commandbus", 5 | version="1.0.1", 6 | packages=find_packages(), 7 | author="Petru Rares Sincraian", 8 | author_email="psincraian@gmail.com", 9 | description="Command Bus Pattern in Python made easy", 10 | keywords="commandbus command bus python pattern patterns", 11 | url="https://github.com/psincraian/commandbus", 12 | license="MIT", 13 | ) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | *.so 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | *.manifest 25 | *.spec 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | htmlcov/ 29 | .tox/ 30 | .coverage 31 | .coverage.* 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | *.cover 36 | .hypothesis/ 37 | *.mo 38 | *.pot 39 | *.log 40 | .static_storage/ 41 | .media/ 42 | local_settings.py 43 | instance/ 44 | .webassets-cache 45 | .scrapy 46 | docs/_build/ 47 | target/ 48 | .ipynb_checkpoints 49 | .python-version 50 | celerybeat-schedule 51 | *.sage.py 52 | .env 53 | .venv 54 | env/ 55 | venv/ 56 | ENV/ 57 | env.bak/ 58 | venv.bak/ 59 | .spyderproject 60 | .spyproject 61 | .ropeproject 62 | /site 63 | .mypy_cache/ 64 | *.pytest_cache 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Petru Rares Sincraian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /commandbus/_model.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABC 2 | 3 | 4 | class Command: 5 | pass 6 | 7 | 8 | class CommandHandler(ABC): 9 | @abstractmethod 10 | def handle(self, cmd: Command): 11 | pass 12 | 13 | 14 | class CommandBus: 15 | def __init__(self): 16 | self._commands = {} 17 | self._executing = False 18 | 19 | def subscribe(self, cmd: type, handler: CommandHandler): 20 | if cmd in self._commands: 21 | from commandbus import CommandAlreadyExistException 22 | raise CommandAlreadyExistException 23 | self._commands[cmd] = handler 24 | 25 | def publish(self, cmd: Command): 26 | if cmd.__class__ not in self._commands: 27 | from commandbus import CommandHandlerDoesNotExistException 28 | raise CommandHandlerDoesNotExistException() 29 | if self._executing: 30 | from commandbus import CommandExecutionAlreadyInProgressException 31 | raise CommandExecutionAlreadyInProgressException() 32 | self._executing = True 33 | try: 34 | self._commands[cmd.__class__].handle(cmd) 35 | finally: 36 | self._executing = False 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

commandbus

2 |

3 | PyPI version 4 | Download stats version 5 |

6 | 7 | ## 📜 About 8 | Command bus is a pattern from CQRS. In this pattern we have three components: 9 | 10 | * `Command`: represents the desired action to be done, like `RegisterUserCommand`. 11 | * `CommandHandler`: retrieves the data from the command and executes all the actions to accomplish the command 12 | objective. In the previous case creates a new user and saves it do database. 13 | * `CommandBus`: routes the command to the handler 14 | 15 | ## ⚒️ Installation 16 | Installation is so easy, you only need to execute 17 | ``` 18 | pip install commandbus 19 | ``` 20 | 21 | ## 🚀 Using commandbus 22 | Using command is as easy as type 23 | ```python3 24 | from commandbus import Command, CommandHandler, CommandBus 25 | 26 | class SomeCommand(Command): 27 | pass 28 | 29 | class SomeCommandHandler(CommandHandler): 30 | def __init__(self): 31 | self.called = False 32 | 33 | def handle(self, cmd: Command): 34 | self.called = True 35 | 36 | bus = CommandBus() 37 | handler = SomeCommandHandler() 38 | bus.subscribe(SomeCommand, handler) 39 | assert not handler.called 40 | bus.publish(SomeCommand()) 41 | assert handler.called 42 | ``` 43 | 44 | 45 | ## 🚩 License 46 | The code is available under the MIT license. 47 | -------------------------------------------------------------------------------- /test/test_functional.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from commandbus import Command, CommandHandler, CommandBus, CommandExecutionAlreadyInProgressException, \ 4 | CommandAlreadyExistException, CommandHandlerDoesNotExistException 5 | 6 | 7 | class SomeCommand(Command): 8 | pass 9 | 10 | 11 | class SomeCommandHandler(CommandHandler): 12 | def __init__(self): 13 | self.called = False 14 | 15 | def handle(self, cmd: Command): 16 | self.called = True 17 | 18 | 19 | class SomeCommandCallsAnotherCommandHandler(CommandHandler): 20 | def __init__(self, bus): 21 | self._bus = bus 22 | 23 | def handle(self, cmd: Command): 24 | self._bus.publish(SomeCommand()) 25 | 26 | 27 | @pytest.fixture 28 | def bus(): 29 | return CommandBus() 30 | 31 | 32 | def test_handle_command(bus): 33 | handler = SomeCommandHandler() 34 | bus.subscribe(SomeCommand, handler) 35 | assert not handler.called 36 | bus.publish(SomeCommand()) 37 | assert handler.called 38 | 39 | 40 | def test_should_throw_exception_when_call_another_command_inside_a_command(bus): 41 | handler = SomeCommandCallsAnotherCommandHandler(bus) 42 | bus.subscribe(SomeCommand, handler) 43 | with pytest.raises(CommandExecutionAlreadyInProgressException): 44 | bus.publish(SomeCommand()) 45 | 46 | 47 | def test_should_throw_command_already_exists_exception(bus): 48 | handler = SomeCommandHandler() 49 | bus.subscribe(SomeCommand, handler) 50 | with pytest.raises(CommandAlreadyExistException): 51 | bus.subscribe(SomeCommand, handler) 52 | 53 | 54 | def test_should_throw_command_does_not_exist_exception(bus): 55 | with pytest.raises(CommandHandlerDoesNotExistException): 56 | bus.publish(SomeCommand) 57 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "fdd5f1c6690747e07232bdd23c0c2a5f19d91e3773b974d10d239d979e878c70" 5 | }, 6 | "host-environment-markers": { 7 | "implementation_name": "cpython", 8 | "implementation_version": "3.5.2", 9 | "os_name": "posix", 10 | "platform_machine": "x86_64", 11 | "platform_python_implementation": "CPython", 12 | "platform_release": "4.13.0-26-generic", 13 | "platform_system": "Linux", 14 | "platform_version": "#29~16.04.2-Ubuntu SMP Tue Jan 9 22:00:44 UTC 2018", 15 | "python_full_version": "3.5.2", 16 | "python_version": "3.5", 17 | "sys_platform": "linux" 18 | }, 19 | "pipfile-spec": 6, 20 | "requires": {}, 21 | "sources": [ 22 | { 23 | "name": "pypi", 24 | "url": "https://pypi.python.org/simple", 25 | "verify_ssl": true 26 | } 27 | ] 28 | }, 29 | "default": { 30 | "attrs": { 31 | "hashes": [ 32 | "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", 33 | "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" 34 | ], 35 | "version": "==17.4.0" 36 | }, 37 | "pluggy": { 38 | "hashes": [ 39 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" 40 | ], 41 | "version": "==0.6.0" 42 | }, 43 | "py": { 44 | "hashes": [ 45 | "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", 46 | "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" 47 | ], 48 | "version": "==1.5.2" 49 | }, 50 | "pytest": { 51 | "hashes": [ 52 | "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", 53 | "sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6" 54 | ], 55 | "version": "==3.4.1" 56 | }, 57 | "six": { 58 | "hashes": [ 59 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", 60 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" 61 | ], 62 | "version": "==1.11.0" 63 | } 64 | }, 65 | "develop": { 66 | "attrs": { 67 | "hashes": [ 68 | "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", 69 | "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" 70 | ], 71 | "version": "==17.4.0" 72 | }, 73 | "coverage": { 74 | "hashes": [ 75 | "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", 76 | "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", 77 | "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", 78 | "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", 79 | "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", 80 | "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", 81 | "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", 82 | "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", 83 | "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", 84 | "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", 85 | "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", 86 | "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", 87 | "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", 88 | "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", 89 | "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", 90 | "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", 91 | "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", 92 | "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", 93 | "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", 94 | "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", 95 | "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", 96 | "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", 97 | "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", 98 | "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", 99 | "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", 100 | "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", 101 | "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", 102 | "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", 103 | "sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", 104 | "sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", 105 | "sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", 106 | "sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", 107 | "sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", 108 | "sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", 109 | "sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", 110 | "sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e" 111 | ], 112 | "version": "==4.5.1" 113 | }, 114 | "pluggy": { 115 | "hashes": [ 116 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" 117 | ], 118 | "version": "==0.6.0" 119 | }, 120 | "py": { 121 | "hashes": [ 122 | "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", 123 | "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" 124 | ], 125 | "version": "==1.5.2" 126 | }, 127 | "pytest": { 128 | "hashes": [ 129 | "sha256:8970e25181e15ab14ae895599a0a0e0ade7d1f1c4c8ca1072ce16f25526a184d", 130 | "sha256:9ddcb879c8cc859d2540204b5399011f842e5e8823674bf429f70ada281b3cc6" 131 | ], 132 | "version": "==3.4.1" 133 | }, 134 | "six": { 135 | "hashes": [ 136 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", 137 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" 138 | ], 139 | "version": "==1.11.0" 140 | } 141 | } 142 | } 143 | --------------------------------------------------------------------------------