{ "info": { "author": "Olivier Philippon", "author_email": "olivier@rougemine.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "# pymessagebus\n\n

a Message/Command Bus for Python

\n\n

\n\"Build\n\"Coverage\n\"Code\n

\n\nPymessagebus is a message bus library. It comes with a generic MessageBus class, as well as a more specialised CommandBus one.\n\n_N.B.: here the \"Message Bus\" / \"Command Bus\" terms refer to a design patterns, and have nothing to do with messaging systems like RabbitMQ. (even though they can be used together)_\n\nI created it because I've been using this design pattern for years while working on Symfony applications, and it never disappointed me - it's really a pretty simple and efficient way to decouple the business actions from their implementations.\n\nYou can have a look at the following URLs to learn more about this design pattern:\n\n- https://matthiasnoback.nl/2015/01/a-wave-of-command-buses/ - a great series of articles explaining the design pattern - it uses PHP but that doesn't matter, the pattern is the same whatever the language is :-)\n- http://tactician.thephpleague.com/ - this is a pretty good and pragamatic PHP implementation of the CommandBus pattern, with clear explanations about the pattern\n- http://docs.simplebus.io/en/latest/ - another excellent PHP implementation, a bit more pure since sending Commands on the CommandBus can't return values here. _(my personal experience is that it's often handy to be able to return something from the execution of a COmmand, even if it's a bit less pure)_\n- https://en.wikipedia.org/wiki/Command_pattern\n\n## Install\n\n```bash\n$ pip install \"pymessagebus==1.*\"\n```\n\n## Synopsis\n\nA naive example of how the CommandBus allows one to keep the business actions (Commands) decoupled from the implementation of their effect (the Command Handlers):\n\n```python\n# domain.py\nimport typing as t\n\nclass CreateCustomerCommand(t.NamedTuple):\n first_name: str\n last_name: str\n\n# command_handlers.py\nimport domain\n\ndef handle_customer_creation(command: domain.CreateCustomerCommand) -> int:\n customer = OrmCustomer()\n customer.full_name = f\"{command.first_name} {command.last_name}\"\n customer.creation_date = datetime.now()\n customer.save()\n return customer.id\n\n# command_bus.py\ncommand_bus = CommandBus()\ncommand_bus.add_handler(CreateCustomerCommand, handle_customer_creation)\n\n# api.py\nimport domain\nfrom command_bus import command_bus\n\n@post(\"/customer)\ndef post_customer(params):\n # Note that the implmentation (the \"handle_customer_creation\" function)\n # is completely invisible here, we only know about the (agnostic) CommandBus\n # and the class that describe the business action (the Command)\n command = CreateCustomerCommand(params[\"first_name\"], params[\"last_name\"])\n customer_id = command_bus.handle(command)\n return customer_id\n```\n\n## API\n\n#### MessageBus\n\nThe `MessageBus` class allows one to trigger one or multiple handlers when a\nmessage of a given type is sent on the bus. \nThe result is an array of results, where each item is the result of one the handlers execution.\n\n```python\nclass BusinessMessage(t.NamedTuple):\n payload: int\n\ndef handler_one(message: BusinessMessage):\n return f\"handler one result: {message.payload}\"\n\ndef handler_two(message: BusinessMessage):\n return f\"handler two result: {message.payload}\"\n\nmessage_bus = MessageBus()\nmessage_bus.add_handler(BusinessMessage, handler_one)\nmessage_bus.add_handler(BusinessMessage, handler_two)\n\nmessage = BusinessMessage(payload=33)\nresult = message_bus.handle(message)\n# result = [\"handler one result: 33\", \"handler one result: 34\"]\n```\n\nThe API is therefore pretty straightforward (you can see it as an abstract class in the [api](/pymessagebus/api.py) module):\n\n- `add_handler(message_class: type, message_handler: t.Callable) -> None` adds a handler, that will be triggered by the instance of the bus when a message of this class is sent to it.\n- `handle(message: object) -> t.List[t.Any]` trigger the handler(s) previously registered for that message class. If no handler has been registered for this kind of message, an empty list is returned.\n- `has_handler_for(message_class: type) -> bool` just allows one to check if one or more handlers have been registered for a given message class.\n\n#### CommandBus\n\nThe `CommandBus` is a specialised version of a `MessageBus` (technically it's just a proxy on top of a MessageBus, which adds the management of those specificities), which comes with the following subtleties:\n\n- Only one handler can be registered for a given message class\n- When a message is sent to the bus via the `handle` method, an error will be raised if no handler has been registered for this message class.\n\n**In short, a Command Bus assumes that it's mandatory to a handler triggered for every business action we send on it - an to have only one.**\n\nThe API is thus exactly the same than the MessageBus, with the following technical differences:\n\n- the `add_handler(message_class, handler)` method will raise a `api.CommandHandlerAlreadyRegisteredForAType` exception if one tries to register a handler for a class of message for which another handler has already been registered before.\n- the `handle(message)` method returns a single result rather than a list of result (as we can - and must - have only one single handler for a given message class). If no handler has been registered for this message class, a `api.CommandHandlerNotFound` exception is raised.\n\n##### Additional options for the CommandBus\n\nThe CommandBus constructor have additional options that you can use to customise its behaviour:\n\n- `allow_result`: it's possible to be stricter about the implementation of the CommandBus pattern, by using the `allow_result=True` named parameter when the class is instanciated (the default value being `False`). \n In that case the result of the `handle(message)` will always be `None`. By doing this one can follow a more pure version of the design pattern. (and access the result of the Command handling via the application repositories, though a pre-generated id attached to the message for example)\n- `locking`: by default the CommandBus will raise a `api.CommandBusAlreadyProcessingAMessage` exception if a message is sent to it while another message is still processed (which can happen if one of the Command Handlers sends a message to the bus). \n You can disable this behaviour by setting the named argument `locking=False` (the default value being `True`).\n\n#### Middlewares\n\nLast but not least, both kinds of buses can accept Middlewares.\n\nA Middleware is a function that receives a message (sent to the bus) as its first argument and a \"next_middleware\" function as second argument. That function can do some custom processing before or/and after the next Middleware (or the execution of the handler(s) registered for that kind of message) is triggered.\n\nMiddlewares are triggered in a \"onion shape\": in the case of 2 Middlweares for example:\n\n- the first registered Middleware \"pre-processing\" will be executed first\n- the second one will come after\n- then the handler(s) registed for that message class is executed (it's the core of the onion)\n\nAnd then we get out of the onion in the opposite direction:\n\n- the second Middleware \"post-processing\" takes place\n- the first Middleware \"post-processing\" is triggered\n- the result if finally returned\n\nMiddlewares can change the message sent to the next Middlewares (or to the message handler(s)), but they can also perform some processing that doesn't affect the message (like logging for instance).\n\nHere is a snippet illustrating this:\n\n```python\nclass MessageWithList(t.NamedTuple):\n payload: t.List[str]\n\ndef middleware_one(message: MessageWithList, next: api.CallNextMiddleware):\n message.payload.append(\"middleware one: does something before the handler\")\n result = next(message)\n message.payload.append(\"middleware one: does something after the handler\")\n return result\n\ndef middleware_two(message: MessageWithList, next: api.CallNextMiddleware):\n message.payload.append(\"middleware two: does something before the handler\")\n result = next(message)\n message.payload.append(\"middleware two: does something after the handler\")\n return result\n\ndef handler(message: MessageWithList) -> str:\n message.payload.append(\"handler does something\")\n return \"handler result\"\n\nmessage_bus = MessageBus(middlewares=[middleware_one, middleware_two])\nmessage_bus.add_handler(MessageWithList, handler)\n\nmessage = MessageWithList(payload=[\"initial message payload\"])\nresult = sut.handle(message)\nassert message.payload == [\n \"initial message payload\",\n \"middleware one: does something before the handler\",\n \"middleware two: does something before the handler\",\n \"handler does something\",\n \"middleware two: does something after the handler\",\n \"middleware one: does something after the handler\",\n]\nassert result == \"handler result\"\n```\n\n#### Logging middleware\n\nFor convenience a \"logging\" middleware comes with the package.\n\nSynopis\n\n```python\nimport logging\nfrom pymessagebus.middleware.logger import get_logger_middleware\n\nlogger = logging.getLogger(\"message_bus\")\nlogging_middleware = get_logger_middleware(logger)\n\nmessage_bus = MessageBus(middlewares=[logging_middleware])\n\n# Now you will get logging messages:\n# - when a message is sent on the bus (default logging level: DEBUG)\n# - when a message has been successfully handled by the bus, with no Exception raised (default logging level: DEBUG)\n# - when the processing of a message has raised an Exception (default logging level: ERROR)\n```\n\nYou can customise the logging levels of the middleware via the `LoggingMiddlewareConfig` class:\n\n```python\nimport logging\nfrom pymessagebus.middleware.logger import get_logger_middleware, LoggingMiddlewareConfig\n\nlogger = logging.getLogger(\"message_bus\")\nlogging_middleware_config = LoggingMiddlewareConfig(\n mgs_received_level=logging.INFO,\n mgs_succeeded_level=logging.INFO,\n mgs_failed_level=logging.CRITICAL\n)\nlogging_middleware = get_logger_middleware(logger, logging_middleware_config)\n```\n\n### \"default\" singletons\n\nBecause most of the use cases of those buses rely on a single instance of the bus, for commodity you can also use singletons for both the MessageBus and CommandBus, accessible from a \"default\" subpackage.\n\nThese versions also expose a very handy `register_handler(message_class: type)` decorator.\n\nSynopsis:\n\n```python\n# domain.py\nimport typing as t\n\nclass CreateCustomerCommand(t.NamedTuple):\n first_name: str\n last_name: str\n\n# command_handlers.py\nfrom pymessagebus.default import commandbus\nimport domain\n\n@commandbus.register_handler(domain.CreateCustomerCommand)\ndef handle_customer_creation(command) -> int:\n customer = OrmCustomer()\n customer.full_name = f\"{command.first_name} {command.last_name}\"\n customer.creation_date = datetime.now()\n customer.save()\n return customer.id\n\n# api.py\nfrom pymessagebus.default import commandbus\nimport domain\n\n@post(\"/customer)\ndef post_customer(params):\n # Note that the implmentation (the \"handle_customer_creation\" function)\n # is completely invisible here, we only know about the (agnostic) CommandBus\n # and the class that describe the business action (the Command)\n command = CreateCustomerCommand(params[\"first_name\"], params[\"last_name\"])\n customer_id = command_bus.handle(command)\n return customer_id\n```\n\nYou can notice that the difference with the first synopsis is that here we don't have to instantiate the CommandBus, and that the `handle_customer_creation` function is registered to it automatically by using the decorator.\n\n## Code quality\n\nThe code itself is formatted with Black and checked with PyLint and MyPy.\n\nThe whole package comes with a full test suite, managed by PyTest.\n\n```bash\n$ make test\n```\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/DrBenton/pymessagebus", "keywords": "CommandBus MessageBus CommandHandler DDD domain-driven-design design-pattern decoupling", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "pymessagebus", "package_url": "https://pypi.org/project/pymessagebus/", "platform": "", "project_url": "https://pypi.org/project/pymessagebus/", "project_urls": { "Homepage": "https://github.com/DrBenton/pymessagebus" }, "release_url": "https://pypi.org/project/pymessagebus/1.2.2/", "requires_dist": null, "requires_python": ">=3.6", "summary": "A simple implementation of the MessageBus / CommandBus pattern", "version": "1.2.2" }, "last_serial": 4326193, "releases": { "1.0.0": [ { "comment_text": "", "digests": { "md5": "09cdb5f907016d6b051ba65db44ac8d3", "sha256": "49be18b7e981171fd89b0a5d0f835cb62424be9809f1614857748439d860a848" }, "downloads": -1, "filename": "pymessagebus-1.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "09cdb5f907016d6b051ba65db44ac8d3", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 9867, "upload_time": "2018-09-26T19:11:30", "url": "https://files.pythonhosted.org/packages/a6/47/8472d30a3c4bcd749fea6e0e3562d9a359269e222cf5d1f292a531687c8d/pymessagebus-1.0.0-py3-none-any.whl" } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "8521e8e723765dffbc9d6825d40d0eba", "sha256": "20ffaa5ef9621a33c3a14c123f481d45565e459dc5020d836c3d76fb078be76e" }, "downloads": -1, "filename": "pymessagebus-1.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "8521e8e723765dffbc9d6825d40d0eba", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 9804, "upload_time": "2018-09-27T13:02:47", "url": "https://files.pythonhosted.org/packages/dc/f5/09f1d5835a52231087343f3fe2537cdaa582c4b50ac8b8a26304badc5417/pymessagebus-1.1.0-py3-none-any.whl" } ], "1.2.0": [ { "comment_text": "", "digests": { "md5": "23d03ad210c6d868fbf20683b2515963", "sha256": "9ceeec503a28ebb765adb7a178d13cac9ed7a20039a9fb4f6d87cf0aacfead88" }, "downloads": -1, "filename": "pymessagebus-1.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "23d03ad210c6d868fbf20683b2515963", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 9802, "upload_time": "2018-09-30T11:47:29", "url": "https://files.pythonhosted.org/packages/18/6c/473ee11ab35b46a4e1fe32b0c1ee2dd7b98a92b7fdab579acb0d5ae7687a/pymessagebus-1.2.0-py3-none-any.whl" } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "8ed0426f6eac5127f5fcda6cd3463fb1", "sha256": "81e47aafd1d4399fa7f8d82a10018a9fd3ec6c7cd912f72112c0500ef1e4effb" }, "downloads": -1, "filename": "pymessagebus-1.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "8ed0426f6eac5127f5fcda6cd3463fb1", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 9798, "upload_time": "2018-09-30T13:45:10", "url": "https://files.pythonhosted.org/packages/9b/83/85f91780c8eaded1d2c160385942106e84df7db0fc12086d9c8299c68ef2/pymessagebus-1.2.1-py3-none-any.whl" } ], "1.2.2": [ { "comment_text": "", "digests": { "md5": "cf14bf5c7e0a7682f15ba32c067e52ec", "sha256": "76f69cec60367d9ab56a8d7fcb1731f01a9ae44acf89282fb13f6c2224caebdc" }, "downloads": -1, "filename": "pymessagebus-1.2.2-py3-none-any.whl", "has_sig": false, "md5_digest": "cf14bf5c7e0a7682f15ba32c067e52ec", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 9803, "upload_time": "2018-09-30T22:58:32", "url": "https://files.pythonhosted.org/packages/f5/ee/2a3c84b45f01345db206d50c4014843280dfc485d27a711577ce30649d6d/pymessagebus-1.2.2-py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "cf14bf5c7e0a7682f15ba32c067e52ec", "sha256": "76f69cec60367d9ab56a8d7fcb1731f01a9ae44acf89282fb13f6c2224caebdc" }, "downloads": -1, "filename": "pymessagebus-1.2.2-py3-none-any.whl", "has_sig": false, "md5_digest": "cf14bf5c7e0a7682f15ba32c067e52ec", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 9803, "upload_time": "2018-09-30T22:58:32", "url": "https://files.pythonhosted.org/packages/f5/ee/2a3c84b45f01345db206d50c4014843280dfc485d27a711577ce30649d6d/pymessagebus-1.2.2-py3-none-any.whl" } ] }