python-cqrs 5.1.0__tar.gz → 5.2.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.2.0}/PKG-INFO +81 -36
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/README.md +78 -33
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/pyproject.toml +6 -3
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/__init__.py +71 -0
- python_cqrs-5.2.0/src/cqrs/adapters/__init__.py +10 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/adapters/circuit_breaker.py +6 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/adapters/kafka.py +10 -0
- python_cqrs-5.2.0/src/cqrs/bootstrap/__init__.py +21 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/bootstrap/events.py +13 -2
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/bootstrap/requests.py +41 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/bootstrap/saga.py +14 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/circuit_breaker.py +10 -0
- python_cqrs-5.2.0/src/cqrs/compressors/protocol.py +30 -0
- python_cqrs-5.2.0/src/cqrs/compressors/zlib.py +18 -0
- python_cqrs-5.2.0/src/cqrs/container/__init__.py +30 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/container/dependency_injector.py +6 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/container/di.py +7 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/container/dishka.py +14 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/container/protocol.py +18 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/container/scope.py +29 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/__init__.py +9 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/events/__init__.py +9 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/events/event_emitter.py +14 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/events/fallback.py +6 -0
- python_cqrs-5.2.0/src/cqrs/generic_utils.py +119 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/handlers/__init__.py +11 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/handlers/cor.py +59 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/handlers/event.py +17 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/handlers/request.py +69 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/handlers/saga.py +30 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mapping/events.py +13 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mapping/outbox.py +40 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mapping/requests.py +37 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mediators/event.py +12 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mediators/request.py +19 -10
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mediators/saga.py +16 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mediators/streaming.py +23 -3
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mermaid/cor.py +13 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mermaid/saga.py +11 -0
- python_cqrs-5.2.0/src/cqrs/message_brokers/__init__.py +17 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/amqp.py +9 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/kafka.py +9 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/producer.py +27 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/protocol.py +17 -0
- python_cqrs-5.2.0/src/cqrs/middlewares/__init__.py +14 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/middlewares/base.py +29 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/middlewares/logging.py +7 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/models/event.py +54 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/models/pydantic.py +34 -4
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/models/request.py +38 -4
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/models/response.py +20 -0
- python_cqrs-5.2.0/src/cqrs/outbox/repository.py +156 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/outbox/sqlalchemy.py +26 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/outbox/sqlmodel.py +12 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/requests/fallback.py +8 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/fallback.py +15 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/models.py +15 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/recovery.py +18 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/saga.py +37 -14
- python_cqrs-5.2.0/src/cqrs/saga/storage/__init__.py +23 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/memory.py +18 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/protocol.py +8 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/sqlalchemy.py +16 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/sqlmodel.py +13 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/serializers/json.py +8 -1
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/serializers/protobuf.py +9 -1
- python_cqrs-5.2.0/src/cqrs/serializers/protocol.py +55 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/python_cqrs.egg-info/PKG-INFO +81 -36
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/python_cqrs.egg-info/SOURCES.txt +1 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.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.2.0}/LICENSE +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/setup.cfg +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/_dataclass_utils.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/protobuf.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mapping/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mediators/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mediators/_common.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/mermaid/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/models/__init__.py +0 -0
- {python_cqrs-5.1.0/src/cqrs/adapters → python_cqrs-5.2.0/src/cqrs/outbox}/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/outbox/mock.py +0 -0
- /python_cqrs-5.1.0/src/cqrs/outbox/__init__.py → /python_cqrs-5.2.0/src/cqrs/py.typed +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/sqlmodel_base.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/cqrs/types.py +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-5.1.0 → python_cqrs-5.2.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.2.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>
|
|
@@ -22,7 +22,7 @@ Requires-Dist: di[anyio]==0.*
|
|
|
22
22
|
Requires-Dist: dependency-injector>=4.0
|
|
23
23
|
Requires-Dist: orjson==3.*
|
|
24
24
|
Requires-Dist: python-dotenv==1.*
|
|
25
|
-
Requires-Dist: typing-extensions>=4.
|
|
25
|
+
Requires-Dist: typing-extensions>=4.7
|
|
26
26
|
Provides-Extra: aiobreaker
|
|
27
27
|
Requires-Dist: aiobreaker>=0.3.0; extra == "aiobreaker"
|
|
28
28
|
Provides-Extra: pydantic
|
|
@@ -64,7 +64,7 @@ Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
|
64
64
|
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
65
65
|
Requires-Dist: faker>=37.12.0; extra == "examples"
|
|
66
66
|
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
67
|
-
Requires-Dist: aiohttp==3.14.
|
|
67
|
+
Requires-Dist: aiohttp==3.14.4; extra == "examples"
|
|
68
68
|
Requires-Dist: protobuf>=4.25.8; extra == "examples"
|
|
69
69
|
Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
|
|
70
70
|
Requires-Dist: dishka>=1.0; extra == "examples"
|
|
@@ -103,12 +103,6 @@ Dynamic: license-file
|
|
|
103
103
|
<a href="https://codecov.io/gh/vadikko2/python-cqrs">
|
|
104
104
|
<img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
|
|
105
105
|
</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
106
|
<a href="https://github.com/mjhea0/awesome-fastapi#developer-tools">
|
|
113
107
|
<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
108
|
</a>
|
|
@@ -144,9 +138,6 @@ Dynamic: license-file
|
|
|
144
138
|
|
|
145
139
|
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
140
|
|
|
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
141
|
**Core framework**
|
|
151
142
|
|
|
152
143
|
1. Redesigned the event and request mapping mechanism to handlers;
|
|
@@ -200,6 +191,8 @@ Default `Request` / `Response` / `Event` / `DomainEvent` / `NotificationEvent` a
|
|
|
200
191
|
are dataclass-based (`DC*`). Use `Pydantic*` types with the pydantic extra when you
|
|
201
192
|
need validation.
|
|
202
193
|
|
|
194
|
+
Coding agents in an application can follow [`templates/AGENTS.md`](templates/AGENTS.md). Copy that file into the application repository.
|
|
195
|
+
|
|
203
196
|
|
|
204
197
|
## Quick Start
|
|
205
198
|
|
|
@@ -210,12 +203,12 @@ import di
|
|
|
210
203
|
import cqrs
|
|
211
204
|
from cqrs.bootstrap import requests as bootstrap
|
|
212
205
|
|
|
213
|
-
class CreateOrderCommand(cqrs.Request):
|
|
206
|
+
class CreateOrderCommand(cqrs.Request[None]):
|
|
214
207
|
order_id: str
|
|
215
208
|
amount: float
|
|
216
209
|
|
|
217
|
-
class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand
|
|
218
|
-
async def handle(self, request: CreateOrderCommand)
|
|
210
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand]):
|
|
211
|
+
async def handle(self, request: CreateOrderCommand):
|
|
219
212
|
print(f"Order {request.order_id}, amount {request.amount}")
|
|
220
213
|
|
|
221
214
|
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
@@ -292,14 +285,20 @@ Request handlers can be divided into two main types:
|
|
|
292
285
|
Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of
|
|
293
286
|
the domain model.
|
|
294
287
|
As a result of executing the command, an event may be produced to the broker.
|
|
288
|
+
|
|
295
289
|
> [!TIP]
|
|
296
290
|
> By default, the command handler does not return any result, but it is not mandatory.
|
|
297
291
|
|
|
298
292
|
```python
|
|
293
|
+
import cqrs
|
|
299
294
|
from cqrs.handlers.request import RequestHandler
|
|
300
295
|
from cqrs.models.event import Event
|
|
301
296
|
|
|
302
|
-
class
|
|
297
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
298
|
+
user_id: str
|
|
299
|
+
meeting_id: str
|
|
300
|
+
|
|
301
|
+
class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand]):
|
|
303
302
|
|
|
304
303
|
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
305
304
|
self._meetings_api = meetings_api
|
|
@@ -309,25 +308,36 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
309
308
|
def events(self) -> typing.List[events.Event]:
|
|
310
309
|
return self._events
|
|
311
310
|
|
|
312
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
311
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
313
312
|
await self._meetings_api.join_user(request.user_id, request.meeting_id)
|
|
314
313
|
```
|
|
315
314
|
|
|
316
315
|
A complete example can be found in
|
|
317
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
|
|
316
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py).
|
|
317
|
+
The one-argument form is in
|
|
318
|
+
[`examples/events/typed_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/typed_request_handler.py).
|
|
318
319
|
|
|
319
320
|
### Query handler
|
|
320
321
|
|
|
321
322
|
Query Handler returns a representation of the requested data, for example, from
|
|
322
323
|
the [read model](https://radekmaziarka.pl/2018/01/08/cqrs-third-step-simple-read-model/#simple-read-model---to-the-rescue).
|
|
324
|
+
|
|
323
325
|
> [!TIP]
|
|
324
326
|
> The read model can be constructed based on domain events produced by the `Command Handler`.
|
|
325
327
|
|
|
326
328
|
```python
|
|
329
|
+
import cqrs
|
|
327
330
|
from cqrs.handlers.request import RequestHandler
|
|
328
331
|
from cqrs.models.event import Event
|
|
329
332
|
|
|
330
|
-
class
|
|
333
|
+
class ReadMeetingQueryResult(cqrs.Response):
|
|
334
|
+
link: str
|
|
335
|
+
meeting_id: str
|
|
336
|
+
|
|
337
|
+
class ReadMeetingQuery(cqrs.Request[ReadMeetingQueryResult]):
|
|
338
|
+
meeting_id: str
|
|
339
|
+
|
|
340
|
+
class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery]):
|
|
331
341
|
|
|
332
342
|
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
333
343
|
self._meetings_api = meetings_api
|
|
@@ -337,14 +347,16 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
337
347
|
def events(self) -> typing.List[events.Event]:
|
|
338
348
|
return self._events
|
|
339
349
|
|
|
340
|
-
async def handle(self, request: ReadMeetingQuery)
|
|
350
|
+
async def handle(self, request: ReadMeetingQuery):
|
|
341
351
|
link = await self._meetings_api.get_link(request.meeting_id)
|
|
342
352
|
return ReadMeetingQueryResult(link=link, meeting_id=request.meeting_id)
|
|
343
353
|
|
|
344
354
|
```
|
|
345
355
|
|
|
346
356
|
A complete example can be found in
|
|
347
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
|
|
357
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py).
|
|
358
|
+
The one-argument form is in
|
|
359
|
+
[`examples/events/typed_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/typed_request_handler.py).
|
|
348
360
|
|
|
349
361
|
### Streaming Request Handler
|
|
350
362
|
|
|
@@ -355,11 +367,18 @@ real-time progress updates.
|
|
|
355
367
|
`StreamingRequestHandler` works with `StreamingRequestMediator` that streams results to clients in real-time.
|
|
356
368
|
|
|
357
369
|
```python
|
|
358
|
-
import
|
|
370
|
+
import cqrs
|
|
359
371
|
from cqrs.handlers.request import StreamingRequestHandler
|
|
360
372
|
from cqrs.models.event import Event
|
|
361
373
|
|
|
362
|
-
class
|
|
374
|
+
class FileProcessedResult(cqrs.Response):
|
|
375
|
+
file_id: str
|
|
376
|
+
status: str
|
|
377
|
+
|
|
378
|
+
class ProcessFilesCommand(cqrs.Request[FileProcessedResult]):
|
|
379
|
+
file_ids: list[str]
|
|
380
|
+
|
|
381
|
+
class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand]):
|
|
363
382
|
def __init__(self):
|
|
364
383
|
self._events: list[Event] = []
|
|
365
384
|
|
|
@@ -370,7 +389,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
370
389
|
def clear_events(self) -> None:
|
|
371
390
|
self._events.clear()
|
|
372
391
|
|
|
373
|
-
async def handle(self, request: ProcessFilesCommand)
|
|
392
|
+
async def handle(self, request: ProcessFilesCommand):
|
|
374
393
|
for file_id in request.file_ids:
|
|
375
394
|
# Process file
|
|
376
395
|
result = FileProcessedResult(file_id=file_id, status="completed", ...)
|
|
@@ -380,7 +399,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
380
399
|
```
|
|
381
400
|
|
|
382
401
|
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)
|
|
402
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py).
|
|
403
|
+
The one-argument form is in
|
|
404
|
+
[`examples/streaming/typed_streaming_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/typed_streaming_handler.py).
|
|
384
405
|
|
|
385
406
|
### Chain of Responsibility Request Handler
|
|
386
407
|
|
|
@@ -393,10 +414,18 @@ when a handler successfully processes the request or when all handlers have been
|
|
|
393
414
|
|
|
394
415
|
```python
|
|
395
416
|
import typing
|
|
417
|
+
import cqrs
|
|
396
418
|
from cqrs.handlers.cor import CORRequestHandler
|
|
397
419
|
from cqrs.models.event import Event
|
|
398
420
|
|
|
399
|
-
class
|
|
421
|
+
class PaymentResult(cqrs.Response):
|
|
422
|
+
success: bool
|
|
423
|
+
transaction_id: str
|
|
424
|
+
|
|
425
|
+
class ProcessPaymentCommand(cqrs.Request[PaymentResult]):
|
|
426
|
+
payment_method: str
|
|
427
|
+
|
|
428
|
+
class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
|
|
400
429
|
def __init__(self, payment_service: PaymentServiceProtocol) -> None:
|
|
401
430
|
self._payment_service = payment_service
|
|
402
431
|
self._events: typing.List[Event] = []
|
|
@@ -405,7 +434,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
|
|
|
405
434
|
def events(self) -> typing.List[Event]:
|
|
406
435
|
return self._events
|
|
407
436
|
|
|
408
|
-
async def handle(self, request: ProcessPaymentCommand)
|
|
437
|
+
async def handle(self, request: ProcessPaymentCommand):
|
|
409
438
|
if request.payment_method == "credit_card":
|
|
410
439
|
# Process credit card payment
|
|
411
440
|
result = await self._payment_service.process_credit_card(request)
|
|
@@ -415,7 +444,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
|
|
|
415
444
|
# Pass to next handler
|
|
416
445
|
return await self.next(request)
|
|
417
446
|
|
|
418
|
-
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand
|
|
447
|
+
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
|
|
419
448
|
def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
|
|
420
449
|
self._paypal_service = paypal_service
|
|
421
450
|
self._events: typing.List[Event] = []
|
|
@@ -424,7 +453,7 @@ class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResul
|
|
|
424
453
|
def events(self) -> typing.List[Event]:
|
|
425
454
|
return self._events
|
|
426
455
|
|
|
427
|
-
async def handle(self, request: ProcessPaymentCommand)
|
|
456
|
+
async def handle(self, request: ProcessPaymentCommand):
|
|
428
457
|
if request.payment_method == "paypal":
|
|
429
458
|
# Process PayPal payment
|
|
430
459
|
result = await self._paypal_service.process_payment(request)
|
|
@@ -443,7 +472,9 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
|
443
472
|
```
|
|
444
473
|
|
|
445
474
|
A complete example can be found in
|
|
446
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py)
|
|
475
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py).
|
|
476
|
+
The one-argument form is in
|
|
477
|
+
[`examples/cor/typed_cor_request_handler.py`](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/typed_cor_request_handler.py).
|
|
447
478
|
|
|
448
479
|
#### Mermaid Diagram Generation
|
|
449
480
|
|
|
@@ -815,7 +846,11 @@ Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqr
|
|
|
815
846
|
During the handling of a command, `cqrs.NotificationEvent` events may be generated and then sent to the broker.
|
|
816
847
|
|
|
817
848
|
```python
|
|
818
|
-
class
|
|
849
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
850
|
+
user_id: str
|
|
851
|
+
meeting_id: str
|
|
852
|
+
|
|
853
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
|
|
819
854
|
def __init__(self):
|
|
820
855
|
self._events = []
|
|
821
856
|
|
|
@@ -823,7 +858,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
823
858
|
def events(self):
|
|
824
859
|
return self._events
|
|
825
860
|
|
|
826
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
861
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
827
862
|
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
828
863
|
self._events.append(
|
|
829
864
|
cqrs.NotificationEvent[UserJoinedNotificationPayload](
|
|
@@ -883,7 +918,11 @@ The package implements the [Transactional Outbox](https://microservices.io/patte
|
|
|
883
918
|
pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
|
|
884
919
|
|
|
885
920
|
```python
|
|
886
|
-
class
|
|
921
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
922
|
+
user_id: str
|
|
923
|
+
meeting_id: str
|
|
924
|
+
|
|
925
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
|
|
887
926
|
def __init__(self, outbox: cqrs.OutboxedEventRepository):
|
|
888
927
|
self.outbox = outbox
|
|
889
928
|
|
|
@@ -891,7 +930,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
891
930
|
def events(self):
|
|
892
931
|
return []
|
|
893
932
|
|
|
894
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
933
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
895
934
|
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
896
935
|
# Outbox repository is bound to a session (e.g. via DI request scope).
|
|
897
936
|
# add() takes only the event; commit() persists the outbox and your changes.
|
|
@@ -928,12 +967,14 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
|
|
|
928
967
|
> The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
|
|
929
968
|
> base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
|
|
930
969
|
> `create_all()`, before generating migrations and before the first query.
|
|
970
|
+
|
|
931
971
|
> [!TIP]
|
|
932
972
|
> `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
|
|
933
973
|
> rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
|
|
934
974
|
> `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
|
|
935
975
|
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
936
976
|
> how to register a native type for your own database.
|
|
977
|
+
|
|
937
978
|
> [!TIP]
|
|
938
979
|
> For Protobuf events, implement `proto()` / `from_proto()` and register a
|
|
939
980
|
> `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
|
|
@@ -998,7 +1039,11 @@ To configure event handling, you need to implement a broker consumer on the side
|
|
|
998
1039
|
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
999
1040
|
|
|
1000
1041
|
```python
|
|
1001
|
-
class
|
|
1042
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
1043
|
+
user_id: str
|
|
1044
|
+
meeting_id: str
|
|
1045
|
+
|
|
1046
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
|
|
1002
1047
|
def __init__(self):
|
|
1003
1048
|
self._events = []
|
|
1004
1049
|
|
|
@@ -1006,7 +1051,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
1006
1051
|
def events(self):
|
|
1007
1052
|
return self._events
|
|
1008
1053
|
|
|
1009
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
1054
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
1010
1055
|
STORAGE[request.meeting_id].append(request.user_id)
|
|
1011
1056
|
self._events.append(
|
|
1012
1057
|
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),
|
|
@@ -22,14 +22,14 @@ dependencies = [
|
|
|
22
22
|
"dependency-injector>=4.0",
|
|
23
23
|
"orjson==3.*",
|
|
24
24
|
"python-dotenv==1.*",
|
|
25
|
-
"typing-extensions>=4.
|
|
25
|
+
"typing-extensions>=4.7",
|
|
26
26
|
]
|
|
27
27
|
description = "Event-Driven Architecture Framework for Distributed Systems"
|
|
28
28
|
maintainers = [{ name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru" }]
|
|
29
29
|
name = "python-cqrs"
|
|
30
30
|
readme = "README.md"
|
|
31
31
|
requires-python = ">=3.10"
|
|
32
|
-
version = "5.
|
|
32
|
+
version = "5.2.0"
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
aiobreaker = ["aiobreaker>=0.3.0"]
|
|
@@ -73,7 +73,7 @@ examples = [
|
|
|
73
73
|
"faststream[kafka]==0.5.28",
|
|
74
74
|
"faker>=37.12.0",
|
|
75
75
|
"uvicorn==0.32.0",
|
|
76
|
-
"aiohttp==3.14.
|
|
76
|
+
"aiohttp==3.14.4",
|
|
77
77
|
"protobuf>=4.25.8",
|
|
78
78
|
"aiosqlite>=0.20.0",
|
|
79
79
|
"dishka>=1.0",
|
|
@@ -103,3 +103,6 @@ markers = [
|
|
|
103
103
|
|
|
104
104
|
[tool.setuptools.packages.find]
|
|
105
105
|
where = ["src"]
|
|
106
|
+
|
|
107
|
+
[tool.setuptools.package-data]
|
|
108
|
+
cqrs = ["py.typed"]
|