├── 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 |
4 |
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 |
--------------------------------------------------------------------------------