python-cqrs 5.1.0__tar.gz → 5.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/PKG-INFO +82 -36
  2. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/README.md +78 -33
  3. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/pyproject.toml +7 -3
  4. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/__init__.py +104 -0
  5. python_cqrs-5.3.0/src/cqrs/adapters/__init__.py +10 -0
  6. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/circuit_breaker.py +6 -0
  7. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/kafka.py +10 -0
  8. python_cqrs-5.3.0/src/cqrs/bootstrap/__init__.py +21 -0
  9. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/bootstrap/events.py +13 -2
  10. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/bootstrap/requests.py +41 -0
  11. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/bootstrap/saga.py +14 -1
  12. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/circuit_breaker.py +10 -0
  13. python_cqrs-5.3.0/src/cqrs/compressors/protocol.py +30 -0
  14. python_cqrs-5.3.0/src/cqrs/compressors/zlib.py +18 -0
  15. python_cqrs-5.3.0/src/cqrs/container/__init__.py +30 -0
  16. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/dependency_injector.py +6 -0
  17. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/di.py +7 -0
  18. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/dishka.py +14 -1
  19. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/protocol.py +18 -0
  20. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/scope.py +29 -1
  21. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/__init__.py +9 -0
  22. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/__init__.py +9 -1
  23. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/event_emitter.py +14 -0
  24. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/fallback.py +6 -0
  25. python_cqrs-5.3.0/src/cqrs/generic_utils.py +119 -0
  26. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/__init__.py +11 -0
  27. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/cor.py +59 -1
  28. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/event.py +17 -0
  29. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/request.py +69 -1
  30. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/saga.py +30 -0
  31. python_cqrs-5.3.0/src/cqrs/inbox/__init__.py +21 -0
  32. python_cqrs-5.3.0/src/cqrs/inbox/map.py +201 -0
  33. python_cqrs-5.3.0/src/cqrs/inbox/memory.py +110 -0
  34. python_cqrs-5.3.0/src/cqrs/inbox/processor.py +116 -0
  35. python_cqrs-5.3.0/src/cqrs/inbox/repository.py +149 -0
  36. python_cqrs-5.3.0/src/cqrs/inbox/sqlalchemy.py +322 -0
  37. python_cqrs-5.3.0/src/cqrs/inbox/sqlmodel.py +164 -0
  38. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/events.py +13 -0
  39. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/outbox.py +40 -0
  40. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/requests.py +37 -0
  41. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/event.py +12 -0
  42. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/request.py +19 -10
  43. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/saga.py +16 -1
  44. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/streaming.py +23 -3
  45. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mermaid/cor.py +13 -0
  46. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mermaid/saga.py +11 -0
  47. python_cqrs-5.3.0/src/cqrs/message_brokers/__init__.py +17 -0
  48. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/amqp.py +9 -0
  49. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/kafka.py +9 -0
  50. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/producer.py +27 -1
  51. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/protocol.py +17 -0
  52. python_cqrs-5.3.0/src/cqrs/middlewares/__init__.py +14 -0
  53. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/middlewares/base.py +29 -0
  54. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/middlewares/logging.py +7 -0
  55. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/event.py +54 -0
  56. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/pydantic.py +34 -4
  57. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/request.py +38 -4
  58. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/response.py +20 -0
  59. python_cqrs-5.3.0/src/cqrs/outbox/repository.py +156 -0
  60. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/outbox/sqlalchemy.py +26 -0
  61. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/outbox/sqlmodel.py +12 -1
  62. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/requests/fallback.py +8 -0
  63. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/fallback.py +15 -1
  64. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/models.py +15 -0
  65. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/recovery.py +18 -0
  66. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/saga.py +37 -14
  67. python_cqrs-5.3.0/src/cqrs/saga/storage/__init__.py +23 -0
  68. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/memory.py +18 -1
  69. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/protocol.py +8 -0
  70. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/sqlalchemy.py +16 -0
  71. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/sqlmodel.py +13 -1
  72. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/json.py +8 -1
  73. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/protobuf.py +9 -1
  74. python_cqrs-5.3.0/src/cqrs/serializers/protocol.py +55 -0
  75. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/PKG-INFO +82 -36
  76. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/SOURCES.txt +8 -0
  77. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/requires.txt +2 -2
  78. python_cqrs-5.1.0/src/cqrs/bootstrap/__init__.py +0 -5
  79. python_cqrs-5.1.0/src/cqrs/compressors/protocol.py +0 -11
  80. python_cqrs-5.1.0/src/cqrs/compressors/zlib.py +0 -9
  81. python_cqrs-5.1.0/src/cqrs/container/__init__.py +0 -18
  82. python_cqrs-5.1.0/src/cqrs/generic_utils.py +0 -43
  83. python_cqrs-5.1.0/src/cqrs/message_brokers/__init__.py +0 -6
  84. python_cqrs-5.1.0/src/cqrs/middlewares/__init__.py +0 -3
  85. python_cqrs-5.1.0/src/cqrs/outbox/repository.py +0 -101
  86. python_cqrs-5.1.0/src/cqrs/saga/storage/__init__.py +0 -11
  87. python_cqrs-5.1.0/src/cqrs/serializers/protocol.py +0 -21
  88. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/LICENSE +0 -0
  89. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/setup.cfg +0 -0
  90. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/_dataclass_utils.py +0 -0
  91. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/amqp.py +0 -0
  92. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/protocol.py +0 -0
  93. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/compressors/__init__.py +0 -0
  94. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/__init__.py +0 -0
  95. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/exceptions.py +0 -0
  96. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/json.py +0 -0
  97. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/protobuf.py +0 -0
  98. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/event.py +0 -0
  99. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/exceptions.py +0 -0
  100. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/models.py +0 -0
  101. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/request.py +0 -0
  102. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/saga.py +0 -0
  103. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/streaming.py +0 -0
  104. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/event_processor.py +0 -0
  105. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/__init__.py +0 -0
  106. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/__init__.py +0 -0
  107. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/_common.py +0 -0
  108. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mermaid/__init__.py +0 -0
  109. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/devnull.py +0 -0
  110. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/__init__.py +0 -0
  111. {python_cqrs-5.1.0/src/cqrs/adapters → python_cqrs-5.3.0/src/cqrs/outbox}/__init__.py +0 -0
  112. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/outbox/mock.py +0 -0
  113. /python_cqrs-5.1.0/src/cqrs/outbox/__init__.py → /python_cqrs-5.3.0/src/cqrs/py.typed +0 -0
  114. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/requests/__init__.py +0 -0
  115. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/__init__.py +0 -0
  116. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/circuit_breaker.py +0 -0
  117. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/compensation.py +0 -0
  118. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/execution.py +0 -0
  119. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/enums.py +0 -0
  120. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/models.py +0 -0
  121. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/validation.py +0 -0
  122. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/__init__.py +0 -0
  123. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/default.py +0 -0
  124. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/sqlalchemy_types.py +0 -0
  125. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/sqlmodel_base.py +0 -0
  126. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/types.py +0 -0
  127. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  128. {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cqrs
3
- Version: 5.1.0
3
+ Version: 5.3.0
4
4
  Summary: Event-Driven Architecture Framework for Distributed Systems
5
5
  Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
6
6
  Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
17
18
  Requires-Python: >=3.10
18
19
  Description-Content-Type: text/markdown
19
20
  License-File: LICENSE
@@ -22,7 +23,7 @@ Requires-Dist: di[anyio]==0.*
22
23
  Requires-Dist: dependency-injector>=4.0
23
24
  Requires-Dist: orjson==3.*
24
25
  Requires-Dist: python-dotenv==1.*
25
- Requires-Dist: typing-extensions>=4.0
26
+ Requires-Dist: typing-extensions>=4.7
26
27
  Provides-Extra: aiobreaker
27
28
  Requires-Dist: aiobreaker>=0.3.0; extra == "aiobreaker"
28
29
  Provides-Extra: pydantic
@@ -64,7 +65,7 @@ Requires-Dist: fastapi==0.109.*; extra == "examples"
64
65
  Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
65
66
  Requires-Dist: faker>=37.12.0; extra == "examples"
66
67
  Requires-Dist: uvicorn==0.32.0; extra == "examples"
67
- Requires-Dist: aiohttp==3.14.3; extra == "examples"
68
+ Requires-Dist: aiohttp==3.14.4; extra == "examples"
68
69
  Requires-Dist: protobuf>=4.25.8; extra == "examples"
69
70
  Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
70
71
  Requires-Dist: dishka>=1.0; extra == "examples"
@@ -103,12 +104,6 @@ Dynamic: license-file
103
104
  <a href="https://codecov.io/gh/vadikko2/python-cqrs">
104
105
  <img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
105
106
  </a>
106
- <a href="https://mkdocs.python-cqrs.dev/">
107
- <img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
108
- </a>
109
- <a href="https://deepwiki.com/vadikko2/python-cqrs">
110
- <img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
111
- </a>
112
107
  <a href="https://github.com/mjhea0/awesome-fastapi#developer-tools">
113
108
  <img src="https://img.shields.io/badge/Awesome%20FastAPI-✓%20in%20list-purple?logo=eye&logoColor=white&style=flat-square" alt="Included in Awesome FastAPI">
114
109
  </a>
@@ -144,9 +139,6 @@ Dynamic: license-file
144
139
 
145
140
  An event-driven framework for building distributed systems in Python. It centers on CQRS (Command Query Responsibility Segregation) and extends into messaging, sagas, and reliable event delivery — so you can separate read and write flows, react to events from the bus, run distributed transactions with compensation, and publish events via Transaction Outbox. The result is clearer structure, better scalability, and easier evolution of the application.
146
141
 
147
- This package is a fork of the [diator](https://github.com/akhundMurad/diator)
148
- project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements, ordered by importance:
149
-
150
142
  **Core framework**
151
143
 
152
144
  1. Redesigned the event and request mapping mechanism to handlers;
@@ -200,6 +192,8 @@ Default `Request` / `Response` / `Event` / `DomainEvent` / `NotificationEvent` a
200
192
  are dataclass-based (`DC*`). Use `Pydantic*` types with the pydantic extra when you
201
193
  need validation.
202
194
 
195
+ Coding agents in an application can follow [`templates/AGENTS.md`](templates/AGENTS.md). Copy that file into the application repository.
196
+
203
197
 
204
198
  ## Quick Start
205
199
 
@@ -210,12 +204,12 @@ import di
210
204
  import cqrs
211
205
  from cqrs.bootstrap import requests as bootstrap
212
206
 
213
- class CreateOrderCommand(cqrs.Request):
207
+ class CreateOrderCommand(cqrs.Request[None]):
214
208
  order_id: str
215
209
  amount: float
216
210
 
217
- class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand, None]):
218
- async def handle(self, request: CreateOrderCommand) -> None:
211
+ class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand]):
212
+ async def handle(self, request: CreateOrderCommand):
219
213
  print(f"Order {request.order_id}, amount {request.amount}")
220
214
 
221
215
  def commands_mapper(mapper: cqrs.RequestMap) -> None:
@@ -292,14 +286,20 @@ Request handlers can be divided into two main types:
292
286
  Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of
293
287
  the domain model.
294
288
  As a result of executing the command, an event may be produced to the broker.
289
+
295
290
  > [!TIP]
296
291
  > By default, the command handler does not return any result, but it is not mandatory.
297
292
 
298
293
  ```python
294
+ import cqrs
299
295
  from cqrs.handlers.request import RequestHandler
300
296
  from cqrs.models.event import Event
301
297
 
302
- class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
298
+ class JoinMeetingCommand(cqrs.Request[None]):
299
+ user_id: str
300
+ meeting_id: str
301
+
302
+ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand]):
303
303
 
304
304
  def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
305
305
  self._meetings_api = meetings_api
@@ -309,25 +309,36 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
309
309
  def events(self) -> typing.List[events.Event]:
310
310
  return self._events
311
311
 
312
- async def handle(self, request: JoinMeetingCommand) -> None:
312
+ async def handle(self, request: JoinMeetingCommand):
313
313
  await self._meetings_api.join_user(request.user_id, request.meeting_id)
314
314
  ```
315
315
 
316
316
  A complete example can be found in
317
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
317
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py).
318
+ The one-argument form is in
319
+ [`examples/events/typed_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/typed_request_handler.py).
318
320
 
319
321
  ### Query handler
320
322
 
321
323
  Query Handler returns a representation of the requested data, for example, from
322
324
  the [read model](https://radekmaziarka.pl/2018/01/08/cqrs-third-step-simple-read-model/#simple-read-model---to-the-rescue).
325
+
323
326
  > [!TIP]
324
327
  > The read model can be constructed based on domain events produced by the `Command Handler`.
325
328
 
326
329
  ```python
330
+ import cqrs
327
331
  from cqrs.handlers.request import RequestHandler
328
332
  from cqrs.models.event import Event
329
333
 
330
- class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryResult]):
334
+ class ReadMeetingQueryResult(cqrs.Response):
335
+ link: str
336
+ meeting_id: str
337
+
338
+ class ReadMeetingQuery(cqrs.Request[ReadMeetingQueryResult]):
339
+ meeting_id: str
340
+
341
+ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery]):
331
342
 
332
343
  def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
333
344
  self._meetings_api = meetings_api
@@ -337,14 +348,16 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
337
348
  def events(self) -> typing.List[events.Event]:
338
349
  return self._events
339
350
 
340
- async def handle(self, request: ReadMeetingQuery) -> ReadMeetingQueryResult:
351
+ async def handle(self, request: ReadMeetingQuery):
341
352
  link = await self._meetings_api.get_link(request.meeting_id)
342
353
  return ReadMeetingQueryResult(link=link, meeting_id=request.meeting_id)
343
354
 
344
355
  ```
345
356
 
346
357
  A complete example can be found in
347
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
358
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py).
359
+ The one-argument form is in
360
+ [`examples/events/typed_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/typed_request_handler.py).
348
361
 
349
362
  ### Streaming Request Handler
350
363
 
@@ -355,11 +368,18 @@ real-time progress updates.
355
368
  `StreamingRequestHandler` works with `StreamingRequestMediator` that streams results to clients in real-time.
356
369
 
357
370
  ```python
358
- import typing
371
+ import cqrs
359
372
  from cqrs.handlers.request import StreamingRequestHandler
360
373
  from cqrs.models.event import Event
361
374
 
362
- class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, FileProcessedResult]):
375
+ class FileProcessedResult(cqrs.Response):
376
+ file_id: str
377
+ status: str
378
+
379
+ class ProcessFilesCommand(cqrs.Request[FileProcessedResult]):
380
+ file_ids: list[str]
381
+
382
+ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand]):
363
383
  def __init__(self):
364
384
  self._events: list[Event] = []
365
385
 
@@ -370,7 +390,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
370
390
  def clear_events(self) -> None:
371
391
  self._events.clear()
372
392
 
373
- async def handle(self, request: ProcessFilesCommand) -> typing.AsyncIterator[FileProcessedResult]:
393
+ async def handle(self, request: ProcessFilesCommand):
374
394
  for file_id in request.file_ids:
375
395
  # Process file
376
396
  result = FileProcessedResult(file_id=file_id, status="completed", ...)
@@ -380,7 +400,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
380
400
  ```
381
401
 
382
402
  A complete example can be found in
383
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py)
403
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py).
404
+ The one-argument form is in
405
+ [`examples/streaming/typed_streaming_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/typed_streaming_handler.py).
384
406
 
385
407
  ### Chain of Responsibility Request Handler
386
408
 
@@ -393,10 +415,18 @@ when a handler successfully processes the request or when all handlers have been
393
415
 
394
416
  ```python
395
417
  import typing
418
+ import cqrs
396
419
  from cqrs.handlers.cor import CORRequestHandler
397
420
  from cqrs.models.event import Event
398
421
 
399
- class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
422
+ class PaymentResult(cqrs.Response):
423
+ success: bool
424
+ transaction_id: str
425
+
426
+ class ProcessPaymentCommand(cqrs.Request[PaymentResult]):
427
+ payment_method: str
428
+
429
+ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
400
430
  def __init__(self, payment_service: PaymentServiceProtocol) -> None:
401
431
  self._payment_service = payment_service
402
432
  self._events: typing.List[Event] = []
@@ -405,7 +435,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
405
435
  def events(self) -> typing.List[Event]:
406
436
  return self._events
407
437
 
408
- async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
438
+ async def handle(self, request: ProcessPaymentCommand):
409
439
  if request.payment_method == "credit_card":
410
440
  # Process credit card payment
411
441
  result = await self._payment_service.process_credit_card(request)
@@ -415,7 +445,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
415
445
  # Pass to next handler
416
446
  return await self.next(request)
417
447
 
418
- class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
448
+ class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
419
449
  def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
420
450
  self._paypal_service = paypal_service
421
451
  self._events: typing.List[Event] = []
@@ -424,7 +454,7 @@ class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResul
424
454
  def events(self) -> typing.List[Event]:
425
455
  return self._events
426
456
 
427
- async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
457
+ async def handle(self, request: ProcessPaymentCommand):
428
458
  if request.payment_method == "paypal":
429
459
  # Process PayPal payment
430
460
  result = await self._paypal_service.process_payment(request)
@@ -443,7 +473,9 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
443
473
  ```
444
474
 
445
475
  A complete example can be found in
446
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py)
476
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py).
477
+ The one-argument form is in
478
+ [`examples/cor/typed_cor_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/typed_cor_request_handler.py).
447
479
 
448
480
  #### Mermaid Diagram Generation
449
481
 
@@ -815,7 +847,11 @@ Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqr
815
847
  During the handling of a command, `cqrs.NotificationEvent` events may be generated and then sent to the broker.
816
848
 
817
849
  ```python
818
- class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
850
+ class JoinMeetingCommand(cqrs.Request[None]):
851
+ user_id: str
852
+ meeting_id: str
853
+
854
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
819
855
  def __init__(self):
820
856
  self._events = []
821
857
 
@@ -823,7 +859,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
823
859
  def events(self):
824
860
  return self._events
825
861
 
826
- async def handle(self, request: JoinMeetingCommand) -> None:
862
+ async def handle(self, request: JoinMeetingCommand):
827
863
  print(f"User {request.user_id} joined meeting {request.meeting_id}")
828
864
  self._events.append(
829
865
  cqrs.NotificationEvent[UserJoinedNotificationPayload](
@@ -883,7 +919,11 @@ The package implements the [Transactional Outbox](https://microservices.io/patte
883
919
  pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
884
920
 
885
921
  ```python
886
- class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
922
+ class JoinMeetingCommand(cqrs.Request[None]):
923
+ user_id: str
924
+ meeting_id: str
925
+
926
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
887
927
  def __init__(self, outbox: cqrs.OutboxedEventRepository):
888
928
  self.outbox = outbox
889
929
 
@@ -891,7 +931,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
891
931
  def events(self):
892
932
  return []
893
933
 
894
- async def handle(self, request: JoinMeetingCommand) -> None:
934
+ async def handle(self, request: JoinMeetingCommand):
895
935
  print(f"User {request.user_id} joined meeting {request.meeting_id}")
896
936
  # Outbox repository is bound to a session (e.g. via DI request scope).
897
937
  # add() takes only the event; commit() persists the outbox and your changes.
@@ -928,12 +968,14 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
928
968
  > The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
929
969
  > base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
930
970
  > `create_all()`, before generating migrations and before the first query.
971
+
931
972
  > [!TIP]
932
973
  > `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
933
974
  > rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
934
975
  > `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
935
976
  > [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
936
977
  > how to register a native type for your own database.
978
+
937
979
  > [!TIP]
938
980
  > For Protobuf events, implement `proto()` / `from_proto()` and register a
939
981
  > `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
@@ -998,7 +1040,11 @@ To configure event handling, you need to implement a broker consumer on the side
998
1040
  Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
999
1041
 
1000
1042
  ```python
1001
- class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
1043
+ class JoinMeetingCommand(cqrs.Request[None]):
1044
+ user_id: str
1045
+ meeting_id: str
1046
+
1047
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
1002
1048
  def __init__(self):
1003
1049
  self._events = []
1004
1050
 
@@ -1006,7 +1052,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
1006
1052
  def events(self):
1007
1053
  return self._events
1008
1054
 
1009
- async def handle(self, request: JoinMeetingCommand) -> None:
1055
+ async def handle(self, request: JoinMeetingCommand):
1010
1056
  STORAGE[request.meeting_id].append(request.user_id)
1011
1057
  self._events.append(
1012
1058
  UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
@@ -20,12 +20,6 @@
20
20
  <a href="https://codecov.io/gh/vadikko2/python-cqrs">
21
21
  <img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
22
22
  </a>
23
- <a href="https://mkdocs.python-cqrs.dev/">
24
- <img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
25
- </a>
26
- <a href="https://deepwiki.com/vadikko2/python-cqrs">
27
- <img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
28
- </a>
29
23
  <a href="https://github.com/mjhea0/awesome-fastapi#developer-tools">
30
24
  <img src="https://img.shields.io/badge/Awesome%20FastAPI-✓%20in%20list-purple?logo=eye&logoColor=white&style=flat-square" alt="Included in Awesome FastAPI">
31
25
  </a>
@@ -61,9 +55,6 @@
61
55
 
62
56
  An event-driven framework for building distributed systems in Python. It centers on CQRS (Command Query Responsibility Segregation) and extends into messaging, sagas, and reliable event delivery — so you can separate read and write flows, react to events from the bus, run distributed transactions with compensation, and publish events via Transaction Outbox. The result is clearer structure, better scalability, and easier evolution of the application.
63
57
 
64
- This package is a fork of the [diator](https://github.com/akhundMurad/diator)
65
- project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements, ordered by importance:
66
-
67
58
  **Core framework**
68
59
 
69
60
  1. Redesigned the event and request mapping mechanism to handlers;
@@ -117,6 +108,8 @@ Default `Request` / `Response` / `Event` / `DomainEvent` / `NotificationEvent` a
117
108
  are dataclass-based (`DC*`). Use `Pydantic*` types with the pydantic extra when you
118
109
  need validation.
119
110
 
111
+ Coding agents in an application can follow [`templates/AGENTS.md`](templates/AGENTS.md). Copy that file into the application repository.
112
+
120
113
 
121
114
  ## Quick Start
122
115
 
@@ -127,12 +120,12 @@ import di
127
120
  import cqrs
128
121
  from cqrs.bootstrap import requests as bootstrap
129
122
 
130
- class CreateOrderCommand(cqrs.Request):
123
+ class CreateOrderCommand(cqrs.Request[None]):
131
124
  order_id: str
132
125
  amount: float
133
126
 
134
- class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand, None]):
135
- async def handle(self, request: CreateOrderCommand) -> None:
127
+ class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand]):
128
+ async def handle(self, request: CreateOrderCommand):
136
129
  print(f"Order {request.order_id}, amount {request.amount}")
137
130
 
138
131
  def commands_mapper(mapper: cqrs.RequestMap) -> None:
@@ -209,14 +202,20 @@ Request handlers can be divided into two main types:
209
202
  Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of
210
203
  the domain model.
211
204
  As a result of executing the command, an event may be produced to the broker.
205
+
212
206
  > [!TIP]
213
207
  > By default, the command handler does not return any result, but it is not mandatory.
214
208
 
215
209
  ```python
210
+ import cqrs
216
211
  from cqrs.handlers.request import RequestHandler
217
212
  from cqrs.models.event import Event
218
213
 
219
- class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
214
+ class JoinMeetingCommand(cqrs.Request[None]):
215
+ user_id: str
216
+ meeting_id: str
217
+
218
+ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand]):
220
219
 
221
220
  def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
222
221
  self._meetings_api = meetings_api
@@ -226,25 +225,36 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
226
225
  def events(self) -> typing.List[events.Event]:
227
226
  return self._events
228
227
 
229
- async def handle(self, request: JoinMeetingCommand) -> None:
228
+ async def handle(self, request: JoinMeetingCommand):
230
229
  await self._meetings_api.join_user(request.user_id, request.meeting_id)
231
230
  ```
232
231
 
233
232
  A complete example can be found in
234
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
233
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py).
234
+ The one-argument form is in
235
+ [`examples/events/typed_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/typed_request_handler.py).
235
236
 
236
237
  ### Query handler
237
238
 
238
239
  Query Handler returns a representation of the requested data, for example, from
239
240
  the [read model](https://radekmaziarka.pl/2018/01/08/cqrs-third-step-simple-read-model/#simple-read-model---to-the-rescue).
241
+
240
242
  > [!TIP]
241
243
  > The read model can be constructed based on domain events produced by the `Command Handler`.
242
244
 
243
245
  ```python
246
+ import cqrs
244
247
  from cqrs.handlers.request import RequestHandler
245
248
  from cqrs.models.event import Event
246
249
 
247
- class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryResult]):
250
+ class ReadMeetingQueryResult(cqrs.Response):
251
+ link: str
252
+ meeting_id: str
253
+
254
+ class ReadMeetingQuery(cqrs.Request[ReadMeetingQueryResult]):
255
+ meeting_id: str
256
+
257
+ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery]):
248
258
 
249
259
  def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
250
260
  self._meetings_api = meetings_api
@@ -254,14 +264,16 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
254
264
  def events(self) -> typing.List[events.Event]:
255
265
  return self._events
256
266
 
257
- async def handle(self, request: ReadMeetingQuery) -> ReadMeetingQueryResult:
267
+ async def handle(self, request: ReadMeetingQuery):
258
268
  link = await self._meetings_api.get_link(request.meeting_id)
259
269
  return ReadMeetingQueryResult(link=link, meeting_id=request.meeting_id)
260
270
 
261
271
  ```
262
272
 
263
273
  A complete example can be found in
264
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
274
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py).
275
+ The one-argument form is in
276
+ [`examples/events/typed_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/typed_request_handler.py).
265
277
 
266
278
  ### Streaming Request Handler
267
279
 
@@ -272,11 +284,18 @@ real-time progress updates.
272
284
  `StreamingRequestHandler` works with `StreamingRequestMediator` that streams results to clients in real-time.
273
285
 
274
286
  ```python
275
- import typing
287
+ import cqrs
276
288
  from cqrs.handlers.request import StreamingRequestHandler
277
289
  from cqrs.models.event import Event
278
290
 
279
- class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, FileProcessedResult]):
291
+ class FileProcessedResult(cqrs.Response):
292
+ file_id: str
293
+ status: str
294
+
295
+ class ProcessFilesCommand(cqrs.Request[FileProcessedResult]):
296
+ file_ids: list[str]
297
+
298
+ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand]):
280
299
  def __init__(self):
281
300
  self._events: list[Event] = []
282
301
 
@@ -287,7 +306,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
287
306
  def clear_events(self) -> None:
288
307
  self._events.clear()
289
308
 
290
- async def handle(self, request: ProcessFilesCommand) -> typing.AsyncIterator[FileProcessedResult]:
309
+ async def handle(self, request: ProcessFilesCommand):
291
310
  for file_id in request.file_ids:
292
311
  # Process file
293
312
  result = FileProcessedResult(file_id=file_id, status="completed", ...)
@@ -297,7 +316,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
297
316
  ```
298
317
 
299
318
  A complete example can be found in
300
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py)
319
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py).
320
+ The one-argument form is in
321
+ [`examples/streaming/typed_streaming_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/typed_streaming_handler.py).
301
322
 
302
323
  ### Chain of Responsibility Request Handler
303
324
 
@@ -310,10 +331,18 @@ when a handler successfully processes the request or when all handlers have been
310
331
 
311
332
  ```python
312
333
  import typing
334
+ import cqrs
313
335
  from cqrs.handlers.cor import CORRequestHandler
314
336
  from cqrs.models.event import Event
315
337
 
316
- class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
338
+ class PaymentResult(cqrs.Response):
339
+ success: bool
340
+ transaction_id: str
341
+
342
+ class ProcessPaymentCommand(cqrs.Request[PaymentResult]):
343
+ payment_method: str
344
+
345
+ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
317
346
  def __init__(self, payment_service: PaymentServiceProtocol) -> None:
318
347
  self._payment_service = payment_service
319
348
  self._events: typing.List[Event] = []
@@ -322,7 +351,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
322
351
  def events(self) -> typing.List[Event]:
323
352
  return self._events
324
353
 
325
- async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
354
+ async def handle(self, request: ProcessPaymentCommand):
326
355
  if request.payment_method == "credit_card":
327
356
  # Process credit card payment
328
357
  result = await self._payment_service.process_credit_card(request)
@@ -332,7 +361,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
332
361
  # Pass to next handler
333
362
  return await self.next(request)
334
363
 
335
- class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
364
+ class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
336
365
  def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
337
366
  self._paypal_service = paypal_service
338
367
  self._events: typing.List[Event] = []
@@ -341,7 +370,7 @@ class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResul
341
370
  def events(self) -> typing.List[Event]:
342
371
  return self._events
343
372
 
344
- async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
373
+ async def handle(self, request: ProcessPaymentCommand):
345
374
  if request.payment_method == "paypal":
346
375
  # Process PayPal payment
347
376
  result = await self._paypal_service.process_payment(request)
@@ -360,7 +389,9 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
360
389
  ```
361
390
 
362
391
  A complete example can be found in
363
- the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py)
392
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py).
393
+ The one-argument form is in
394
+ [`examples/cor/typed_cor_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/typed_cor_request_handler.py).
364
395
 
365
396
  #### Mermaid Diagram Generation
366
397
 
@@ -732,7 +763,11 @@ Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqr
732
763
  During the handling of a command, `cqrs.NotificationEvent` events may be generated and then sent to the broker.
733
764
 
734
765
  ```python
735
- class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
766
+ class JoinMeetingCommand(cqrs.Request[None]):
767
+ user_id: str
768
+ meeting_id: str
769
+
770
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
736
771
  def __init__(self):
737
772
  self._events = []
738
773
 
@@ -740,7 +775,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
740
775
  def events(self):
741
776
  return self._events
742
777
 
743
- async def handle(self, request: JoinMeetingCommand) -> None:
778
+ async def handle(self, request: JoinMeetingCommand):
744
779
  print(f"User {request.user_id} joined meeting {request.meeting_id}")
745
780
  self._events.append(
746
781
  cqrs.NotificationEvent[UserJoinedNotificationPayload](
@@ -800,7 +835,11 @@ The package implements the [Transactional Outbox](https://microservices.io/patte
800
835
  pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
801
836
 
802
837
  ```python
803
- class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
838
+ class JoinMeetingCommand(cqrs.Request[None]):
839
+ user_id: str
840
+ meeting_id: str
841
+
842
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
804
843
  def __init__(self, outbox: cqrs.OutboxedEventRepository):
805
844
  self.outbox = outbox
806
845
 
@@ -808,7 +847,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
808
847
  def events(self):
809
848
  return []
810
849
 
811
- async def handle(self, request: JoinMeetingCommand) -> None:
850
+ async def handle(self, request: JoinMeetingCommand):
812
851
  print(f"User {request.user_id} joined meeting {request.meeting_id}")
813
852
  # Outbox repository is bound to a session (e.g. via DI request scope).
814
853
  # add() takes only the event; commit() persists the outbox and your changes.
@@ -845,12 +884,14 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
845
884
  > The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
846
885
  > base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
847
886
  > `create_all()`, before generating migrations and before the first query.
887
+
848
888
  > [!TIP]
849
889
  > `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
850
890
  > rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
851
891
  > `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
852
892
  > [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
853
893
  > how to register a native type for your own database.
894
+
854
895
  > [!TIP]
855
896
  > For Protobuf events, implement `proto()` / `from_proto()` and register a
856
897
  > `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
@@ -915,7 +956,11 @@ To configure event handling, you need to implement a broker consumer on the side
915
956
  Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
916
957
 
917
958
  ```python
918
- class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
959
+ class JoinMeetingCommand(cqrs.Request[None]):
960
+ user_id: str
961
+ meeting_id: str
962
+
963
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
919
964
  def __init__(self):
920
965
  self._events = []
921
966
 
@@ -923,7 +968,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
923
968
  def events(self):
924
969
  return self._events
925
970
 
926
- async def handle(self, request: JoinMeetingCommand) -> None:
971
+ async def handle(self, request: JoinMeetingCommand):
927
972
  STORAGE[request.meeting_id].append(request.user_id)
928
973
  self._events.append(
929
974
  UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),