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.
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/PKG-INFO +82 -36
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/README.md +78 -33
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/pyproject.toml +7 -3
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/__init__.py +104 -0
- python_cqrs-5.3.0/src/cqrs/adapters/__init__.py +10 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/circuit_breaker.py +6 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/kafka.py +10 -0
- python_cqrs-5.3.0/src/cqrs/bootstrap/__init__.py +21 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/bootstrap/events.py +13 -2
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/bootstrap/requests.py +41 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/bootstrap/saga.py +14 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/circuit_breaker.py +10 -0
- python_cqrs-5.3.0/src/cqrs/compressors/protocol.py +30 -0
- python_cqrs-5.3.0/src/cqrs/compressors/zlib.py +18 -0
- python_cqrs-5.3.0/src/cqrs/container/__init__.py +30 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/dependency_injector.py +6 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/di.py +7 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/dishka.py +14 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/protocol.py +18 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/container/scope.py +29 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/__init__.py +9 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/__init__.py +9 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/event_emitter.py +14 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/fallback.py +6 -0
- python_cqrs-5.3.0/src/cqrs/generic_utils.py +119 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/__init__.py +11 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/cor.py +59 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/event.py +17 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/request.py +69 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/handlers/saga.py +30 -0
- python_cqrs-5.3.0/src/cqrs/inbox/__init__.py +21 -0
- python_cqrs-5.3.0/src/cqrs/inbox/map.py +201 -0
- python_cqrs-5.3.0/src/cqrs/inbox/memory.py +110 -0
- python_cqrs-5.3.0/src/cqrs/inbox/processor.py +116 -0
- python_cqrs-5.3.0/src/cqrs/inbox/repository.py +149 -0
- python_cqrs-5.3.0/src/cqrs/inbox/sqlalchemy.py +322 -0
- python_cqrs-5.3.0/src/cqrs/inbox/sqlmodel.py +164 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/events.py +13 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/outbox.py +40 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/requests.py +37 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/event.py +12 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/request.py +19 -10
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/saga.py +16 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/streaming.py +23 -3
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mermaid/cor.py +13 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mermaid/saga.py +11 -0
- python_cqrs-5.3.0/src/cqrs/message_brokers/__init__.py +17 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/amqp.py +9 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/kafka.py +9 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/producer.py +27 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/protocol.py +17 -0
- python_cqrs-5.3.0/src/cqrs/middlewares/__init__.py +14 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/middlewares/base.py +29 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/middlewares/logging.py +7 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/event.py +54 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/pydantic.py +34 -4
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/request.py +38 -4
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/response.py +20 -0
- python_cqrs-5.3.0/src/cqrs/outbox/repository.py +156 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/outbox/sqlalchemy.py +26 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/outbox/sqlmodel.py +12 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/requests/fallback.py +8 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/fallback.py +15 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/models.py +15 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/recovery.py +18 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/saga.py +37 -14
- python_cqrs-5.3.0/src/cqrs/saga/storage/__init__.py +23 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/memory.py +18 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/protocol.py +8 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/sqlalchemy.py +16 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/sqlmodel.py +13 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/json.py +8 -1
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/protobuf.py +9 -1
- python_cqrs-5.3.0/src/cqrs/serializers/protocol.py +55 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/PKG-INFO +82 -36
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/SOURCES.txt +8 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/requires.txt +2 -2
- python_cqrs-5.1.0/src/cqrs/bootstrap/__init__.py +0 -5
- python_cqrs-5.1.0/src/cqrs/compressors/protocol.py +0 -11
- python_cqrs-5.1.0/src/cqrs/compressors/zlib.py +0 -9
- python_cqrs-5.1.0/src/cqrs/container/__init__.py +0 -18
- python_cqrs-5.1.0/src/cqrs/generic_utils.py +0 -43
- python_cqrs-5.1.0/src/cqrs/message_brokers/__init__.py +0 -6
- python_cqrs-5.1.0/src/cqrs/middlewares/__init__.py +0 -3
- python_cqrs-5.1.0/src/cqrs/outbox/repository.py +0 -101
- python_cqrs-5.1.0/src/cqrs/saga/storage/__init__.py +0 -11
- python_cqrs-5.1.0/src/cqrs/serializers/protocol.py +0 -21
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/LICENSE +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/setup.cfg +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/_dataclass_utils.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/deserializers/protobuf.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mapping/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mediators/_common.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/mermaid/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/models/__init__.py +0 -0
- {python_cqrs-5.1.0/src/cqrs/adapters → python_cqrs-5.3.0/src/cqrs/outbox}/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/outbox/mock.py +0 -0
- /python_cqrs-5.1.0/src/cqrs/outbox/__init__.py → /python_cqrs-5.3.0/src/cqrs/py.typed +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/sqlmodel_base.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/cqrs/types.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.3.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
218
|
-
async def handle(self, request: CreateOrderCommand)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
371
|
+
import cqrs
|
|
359
372
|
from cqrs.handlers.request import StreamingRequestHandler
|
|
360
373
|
from cqrs.models.event import Event
|
|
361
374
|
|
|
362
|
-
class
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
135
|
-
async def handle(self, request: CreateOrderCommand)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
287
|
+
import cqrs
|
|
276
288
|
from cqrs.handlers.request import StreamingRequestHandler
|
|
277
289
|
from cqrs.models.event import Event
|
|
278
290
|
|
|
279
|
-
class
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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),
|