python-cqrs 5.0.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.0.0/src/python_cqrs.egg-info → python_cqrs-5.2.0}/PKG-INFO +88 -77
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/README.md +80 -74
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/pyproject.toml +10 -3
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/__init__.py +85 -0
- python_cqrs-5.2.0/src/cqrs/adapters/__init__.py +10 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/adapters/circuit_breaker.py +6 -0
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/bootstrap/events.py +13 -2
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/bootstrap/requests.py +41 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/bootstrap/saga.py +14 -1
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/container/dependency_injector.py +6 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/container/di.py +7 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/container/dishka.py +14 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/container/protocol.py +18 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/container/scope.py +29 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/__init__.py +9 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/events/__init__.py +9 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/events/event_emitter.py +14 -0
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/handlers/__init__.py +11 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/handlers/cor.py +59 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/handlers/event.py +17 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/handlers/request.py +69 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/handlers/saga.py +30 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mapping/events.py +13 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mapping/outbox.py +40 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mapping/requests.py +37 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mediators/event.py +12 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mediators/request.py +19 -10
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mediators/saga.py +16 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mediators/streaming.py +23 -3
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mermaid/cor.py +13 -0
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/amqp.py +9 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/kafka.py +9 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/producer.py +27 -1
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/middlewares/base.py +29 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/middlewares/logging.py +7 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/models/event.py +54 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/models/pydantic.py +34 -4
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/models/request.py +38 -4
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/outbox/sqlalchemy.py +33 -5
- python_cqrs-5.2.0/src/cqrs/outbox/sqlmodel.py +200 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/requests/fallback.py +8 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/fallback.py +15 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/models.py +15 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/recovery.py +18 -0
- {python_cqrs-5.0.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.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/memory.py +18 -1
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/protocol.py +8 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/sqlalchemy.py +88 -45
- python_cqrs-5.2.0/src/cqrs/saga/storage/sqlmodel.py +215 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/serializers/json.py +8 -1
- {python_cqrs-5.0.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.2.0/src/cqrs/sqlmodel_base.py +45 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0/src/python_cqrs.egg-info}/PKG-INFO +88 -77
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/python_cqrs.egg-info/SOURCES.txt +4 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/python_cqrs.egg-info/requires.txt +8 -2
- python_cqrs-5.0.0/src/cqrs/bootstrap/__init__.py +0 -5
- python_cqrs-5.0.0/src/cqrs/compressors/protocol.py +0 -11
- python_cqrs-5.0.0/src/cqrs/compressors/zlib.py +0 -9
- python_cqrs-5.0.0/src/cqrs/container/__init__.py +0 -18
- python_cqrs-5.0.0/src/cqrs/generic_utils.py +0 -43
- python_cqrs-5.0.0/src/cqrs/message_brokers/__init__.py +0 -6
- python_cqrs-5.0.0/src/cqrs/middlewares/__init__.py +0 -3
- python_cqrs-5.0.0/src/cqrs/outbox/repository.py +0 -101
- python_cqrs-5.0.0/src/cqrs/saga/storage/__init__.py +0 -11
- python_cqrs-5.0.0/src/cqrs/serializers/protocol.py +0 -21
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/LICENSE +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/setup.cfg +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/_dataclass_utils.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/deserializers/protobuf.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mapping/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mediators/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mediators/_common.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/mermaid/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/models/__init__.py +0 -0
- {python_cqrs-5.0.0/src/cqrs/adapters → python_cqrs-5.2.0/src/cqrs/outbox}/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/outbox/mock.py +0 -0
- /python_cqrs-5.0.0/src/cqrs/outbox/__init__.py → /python_cqrs-5.2.0/src/cqrs/py.typed +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/cqrs/types.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.2.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-5.0.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,13 +22,16 @@ 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
|
|
29
29
|
Requires-Dist: pydantic==2.*; extra == "pydantic"
|
|
30
30
|
Provides-Extra: sqlalchemy
|
|
31
31
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "sqlalchemy"
|
|
32
|
+
Provides-Extra: sqlmodel
|
|
33
|
+
Requires-Dist: sqlmodel>=0.0.22; extra == "sqlmodel"
|
|
34
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "sqlmodel"
|
|
32
35
|
Provides-Extra: dev
|
|
33
36
|
Requires-Dist: pycln==2.5.0; extra == "dev"
|
|
34
37
|
Requires-Dist: pre-commit==3.8.0; extra == "dev"
|
|
@@ -54,12 +57,14 @@ Requires-Dist: dishka>=1.0; extra == "dev"
|
|
|
54
57
|
Requires-Dist: protobuf>=4.25.8; extra == "dev"
|
|
55
58
|
Requires-Dist: pydantic==2.*; extra == "dev"
|
|
56
59
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "dev"
|
|
60
|
+
Requires-Dist: sqlmodel>=0.0.22; extra == "dev"
|
|
61
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == "dev"
|
|
57
62
|
Provides-Extra: examples
|
|
58
63
|
Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
59
64
|
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
60
65
|
Requires-Dist: faker>=37.12.0; extra == "examples"
|
|
61
66
|
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
62
|
-
Requires-Dist: aiohttp==3.14.
|
|
67
|
+
Requires-Dist: aiohttp==3.14.4; extra == "examples"
|
|
63
68
|
Requires-Dist: protobuf>=4.25.8; extra == "examples"
|
|
64
69
|
Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
|
|
65
70
|
Requires-Dist: dishka>=1.0; extra == "examples"
|
|
@@ -98,32 +103,17 @@ Dynamic: license-file
|
|
|
98
103
|
<a href="https://codecov.io/gh/vadikko2/python-cqrs">
|
|
99
104
|
<img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
|
|
100
105
|
</a>
|
|
101
|
-
<a href="https://codspeed.io/vadikko2/python-cqrs?utm_source=badge">
|
|
102
|
-
<img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed">
|
|
103
|
-
</a>
|
|
104
|
-
<a href="https://mkdocs.python-cqrs.dev/">
|
|
105
|
-
<img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
|
|
106
|
-
</a>
|
|
107
|
-
<a href="https://deepwiki.com/vadikko2/python-cqrs">
|
|
108
|
-
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
|
|
109
|
-
</a>
|
|
110
106
|
<a href="https://github.com/mjhea0/awesome-fastapi#developer-tools">
|
|
111
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">
|
|
112
108
|
</a>
|
|
113
109
|
</p>
|
|
114
110
|
</div>
|
|
115
111
|
|
|
116
|
-
> [!WARNING]
|
|
117
|
-
> **Breaking Changes in v5.0.0**
|
|
118
|
-
>
|
|
119
|
-
> Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
|
|
120
|
-
|
|
121
112
|
## Table of Contents
|
|
122
113
|
|
|
123
114
|
- [Overview](#overview)
|
|
124
115
|
- [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
|
|
125
116
|
- [Installation](#installation)
|
|
126
|
-
- [Version Support](#version-support)
|
|
127
117
|
- [Quick Start](#quick-start)
|
|
128
118
|
- [Request and Response Types](#request-and-response-types)
|
|
129
119
|
- [Request Handlers](#request-handlers)
|
|
@@ -148,9 +138,6 @@ Dynamic: license-file
|
|
|
148
138
|
|
|
149
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.
|
|
150
140
|
|
|
151
|
-
This package is a fork of the [diator](https://github.com/akhundMurad/diator)
|
|
152
|
-
project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements, ordered by importance:
|
|
153
|
-
|
|
154
141
|
**Core framework**
|
|
155
142
|
|
|
156
143
|
1. Redesigned the event and request mapping mechanism to handlers;
|
|
@@ -166,7 +153,7 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
166
153
|
|
|
167
154
|
- **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
|
|
168
155
|
- **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
|
|
169
|
-
- **Storage:** SQLAlchemy
|
|
156
|
+
- **Storage:** SQLAlchemy and SQLModel outbox and saga storage on MySQL/MariaDB and PostgreSQL.
|
|
170
157
|
- **Integration:** Ready for integration with FastAPI and FastStream.
|
|
171
158
|
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
172
159
|
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
@@ -189,11 +176,12 @@ Do not add a Saga for a single `UPDATE` in one service, and do not use a Saga as
|
|
|
189
176
|
pip install python-cqrs
|
|
190
177
|
```
|
|
191
178
|
|
|
192
|
-
Optional extras (5.0+ — pydantic and SQLAlchemy are no longer core dependencies):
|
|
179
|
+
Optional extras (5.0+ — pydantic and SQLAlchemy are no longer core dependencies; SQLModel storage is 5.1+):
|
|
193
180
|
|
|
194
181
|
```bash
|
|
195
182
|
pip install "python-cqrs[pydantic]" # PydanticRequest / Pydantic* events
|
|
196
183
|
pip install "python-cqrs[sqlalchemy]" # Outbox / Saga SQLAlchemy storage
|
|
184
|
+
pip install "python-cqrs[sqlmodel]" # Outbox / Saga SQLModel storage
|
|
197
185
|
pip install "python-cqrs[kafka]" # Kafka broker (aiokafka)
|
|
198
186
|
pip install "python-cqrs[examples]" # FastAPI, FastStream, uvicorn, etc.
|
|
199
187
|
pip install "python-cqrs[aiobreaker]" # Circuit breaker for saga fallbacks
|
|
@@ -203,37 +191,8 @@ Default `Request` / `Response` / `Event` / `DomainEvent` / `NotificationEvent` a
|
|
|
203
191
|
are dataclass-based (`DC*`). Use `Pydantic*` types with the pydantic extra when you
|
|
204
192
|
need validation.
|
|
205
193
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
| Area | Import |
|
|
209
|
-
|------|--------|
|
|
210
|
-
| Bootstrap | `from cqrs.bootstrap import requests, events, saga` |
|
|
211
|
-
| Mediators | `from cqrs.mediators import RequestMediator, SagaMediator, …` |
|
|
212
|
-
| Handlers | `from cqrs.handlers import RequestHandler, EventHandler, …` |
|
|
213
|
-
| Models | `from cqrs.models import Request, Event, …` / `cqrs.models.pydantic` |
|
|
214
|
-
| Maps | `from cqrs.mapping import RequestMap, EventMap, OutboxedEventMap` |
|
|
215
|
-
| Mermaid | `from cqrs.mermaid import CoRMermaid, SagaMermaid` |
|
|
216
|
-
| Outbox producer | `from cqrs.message_brokers.producer import EventProducer` |
|
|
217
|
-
|
|
218
|
-
`SagaMediator.execute(...)` replaces the former `SagaMediator.stream(...)`
|
|
219
|
-
(StreamingRequestMediator.stream is unchanged).
|
|
220
|
-
|
|
221
|
-
## Version Support
|
|
222
|
-
|
|
223
|
-
| Line | Git branch | What changes |
|
|
224
|
-
|------|------------|--------------|
|
|
225
|
-
| **4.x** | `4.x` | Bug fixes and security only (~12 months after 5.0.0) |
|
|
226
|
-
| **5.x** | `master` | Features and breaking changes |
|
|
227
|
-
|
|
228
|
-
Stay on 4.x until you migrate:
|
|
194
|
+
Coding agents in an application can follow [`templates/AGENTS.md`](templates/AGENTS.md). Copy that file into the application repository.
|
|
229
195
|
|
|
230
|
-
```bash
|
|
231
|
-
pip install "python-cqrs>=4,<5"
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Docs: [4.x](https://mkdocs.python-cqrs.dev/4.0/) and [5.x / latest](https://mkdocs.python-cqrs.dev/latest/) (version switcher after mike deploy). See [SECURITY.md](SECURITY.md) for the support window and vulnerability reporting, and [CONTRIBUTING.md](CONTRIBUTING.md#8-maintaining--fixing-4x) for how to contribute 4.x bugfixes.
|
|
235
|
-
|
|
236
|
-
A package-level EOL `DeprecationWarning` toward 5.x will be added on the **first 4.x patch after 5.0.0** — not before.
|
|
237
196
|
|
|
238
197
|
## Quick Start
|
|
239
198
|
|
|
@@ -244,12 +203,12 @@ import di
|
|
|
244
203
|
import cqrs
|
|
245
204
|
from cqrs.bootstrap import requests as bootstrap
|
|
246
205
|
|
|
247
|
-
class CreateOrderCommand(cqrs.Request):
|
|
206
|
+
class CreateOrderCommand(cqrs.Request[None]):
|
|
248
207
|
order_id: str
|
|
249
208
|
amount: float
|
|
250
209
|
|
|
251
|
-
class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand
|
|
252
|
-
async def handle(self, request: CreateOrderCommand)
|
|
210
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand]):
|
|
211
|
+
async def handle(self, request: CreateOrderCommand):
|
|
253
212
|
print(f"Order {request.order_id}, amount {request.amount}")
|
|
254
213
|
|
|
255
214
|
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
@@ -326,14 +285,20 @@ Request handlers can be divided into two main types:
|
|
|
326
285
|
Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of
|
|
327
286
|
the domain model.
|
|
328
287
|
As a result of executing the command, an event may be produced to the broker.
|
|
288
|
+
|
|
329
289
|
> [!TIP]
|
|
330
290
|
> By default, the command handler does not return any result, but it is not mandatory.
|
|
331
291
|
|
|
332
292
|
```python
|
|
293
|
+
import cqrs
|
|
333
294
|
from cqrs.handlers.request import RequestHandler
|
|
334
295
|
from cqrs.models.event import Event
|
|
335
296
|
|
|
336
|
-
class
|
|
297
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
298
|
+
user_id: str
|
|
299
|
+
meeting_id: str
|
|
300
|
+
|
|
301
|
+
class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand]):
|
|
337
302
|
|
|
338
303
|
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
339
304
|
self._meetings_api = meetings_api
|
|
@@ -343,25 +308,36 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
343
308
|
def events(self) -> typing.List[events.Event]:
|
|
344
309
|
return self._events
|
|
345
310
|
|
|
346
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
311
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
347
312
|
await self._meetings_api.join_user(request.user_id, request.meeting_id)
|
|
348
313
|
```
|
|
349
314
|
|
|
350
315
|
A complete example can be found in
|
|
351
|
-
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).
|
|
352
319
|
|
|
353
320
|
### Query handler
|
|
354
321
|
|
|
355
322
|
Query Handler returns a representation of the requested data, for example, from
|
|
356
323
|
the [read model](https://radekmaziarka.pl/2018/01/08/cqrs-third-step-simple-read-model/#simple-read-model---to-the-rescue).
|
|
324
|
+
|
|
357
325
|
> [!TIP]
|
|
358
326
|
> The read model can be constructed based on domain events produced by the `Command Handler`.
|
|
359
327
|
|
|
360
328
|
```python
|
|
329
|
+
import cqrs
|
|
361
330
|
from cqrs.handlers.request import RequestHandler
|
|
362
331
|
from cqrs.models.event import Event
|
|
363
332
|
|
|
364
|
-
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]):
|
|
365
341
|
|
|
366
342
|
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
367
343
|
self._meetings_api = meetings_api
|
|
@@ -371,14 +347,16 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
371
347
|
def events(self) -> typing.List[events.Event]:
|
|
372
348
|
return self._events
|
|
373
349
|
|
|
374
|
-
async def handle(self, request: ReadMeetingQuery)
|
|
350
|
+
async def handle(self, request: ReadMeetingQuery):
|
|
375
351
|
link = await self._meetings_api.get_link(request.meeting_id)
|
|
376
352
|
return ReadMeetingQueryResult(link=link, meeting_id=request.meeting_id)
|
|
377
353
|
|
|
378
354
|
```
|
|
379
355
|
|
|
380
356
|
A complete example can be found in
|
|
381
|
-
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).
|
|
382
360
|
|
|
383
361
|
### Streaming Request Handler
|
|
384
362
|
|
|
@@ -389,11 +367,18 @@ real-time progress updates.
|
|
|
389
367
|
`StreamingRequestHandler` works with `StreamingRequestMediator` that streams results to clients in real-time.
|
|
390
368
|
|
|
391
369
|
```python
|
|
392
|
-
import
|
|
370
|
+
import cqrs
|
|
393
371
|
from cqrs.handlers.request import StreamingRequestHandler
|
|
394
372
|
from cqrs.models.event import Event
|
|
395
373
|
|
|
396
|
-
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]):
|
|
397
382
|
def __init__(self):
|
|
398
383
|
self._events: list[Event] = []
|
|
399
384
|
|
|
@@ -404,7 +389,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
404
389
|
def clear_events(self) -> None:
|
|
405
390
|
self._events.clear()
|
|
406
391
|
|
|
407
|
-
async def handle(self, request: ProcessFilesCommand)
|
|
392
|
+
async def handle(self, request: ProcessFilesCommand):
|
|
408
393
|
for file_id in request.file_ids:
|
|
409
394
|
# Process file
|
|
410
395
|
result = FileProcessedResult(file_id=file_id, status="completed", ...)
|
|
@@ -414,7 +399,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
414
399
|
```
|
|
415
400
|
|
|
416
401
|
A complete example can be found in
|
|
417
|
-
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).
|
|
418
405
|
|
|
419
406
|
### Chain of Responsibility Request Handler
|
|
420
407
|
|
|
@@ -427,10 +414,18 @@ when a handler successfully processes the request or when all handlers have been
|
|
|
427
414
|
|
|
428
415
|
```python
|
|
429
416
|
import typing
|
|
417
|
+
import cqrs
|
|
430
418
|
from cqrs.handlers.cor import CORRequestHandler
|
|
431
419
|
from cqrs.models.event import Event
|
|
432
420
|
|
|
433
|
-
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]):
|
|
434
429
|
def __init__(self, payment_service: PaymentServiceProtocol) -> None:
|
|
435
430
|
self._payment_service = payment_service
|
|
436
431
|
self._events: typing.List[Event] = []
|
|
@@ -439,7 +434,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
|
|
|
439
434
|
def events(self) -> typing.List[Event]:
|
|
440
435
|
return self._events
|
|
441
436
|
|
|
442
|
-
async def handle(self, request: ProcessPaymentCommand)
|
|
437
|
+
async def handle(self, request: ProcessPaymentCommand):
|
|
443
438
|
if request.payment_method == "credit_card":
|
|
444
439
|
# Process credit card payment
|
|
445
440
|
result = await self._payment_service.process_credit_card(request)
|
|
@@ -449,7 +444,7 @@ class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentR
|
|
|
449
444
|
# Pass to next handler
|
|
450
445
|
return await self.next(request)
|
|
451
446
|
|
|
452
|
-
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand
|
|
447
|
+
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand]):
|
|
453
448
|
def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
|
|
454
449
|
self._paypal_service = paypal_service
|
|
455
450
|
self._events: typing.List[Event] = []
|
|
@@ -458,7 +453,7 @@ class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResul
|
|
|
458
453
|
def events(self) -> typing.List[Event]:
|
|
459
454
|
return self._events
|
|
460
455
|
|
|
461
|
-
async def handle(self, request: ProcessPaymentCommand)
|
|
456
|
+
async def handle(self, request: ProcessPaymentCommand):
|
|
462
457
|
if request.payment_method == "paypal":
|
|
463
458
|
# Process PayPal payment
|
|
464
459
|
result = await self._paypal_service.process_payment(request)
|
|
@@ -477,7 +472,9 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
|
477
472
|
```
|
|
478
473
|
|
|
479
474
|
A complete example can be found in
|
|
480
|
-
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).
|
|
481
478
|
|
|
482
479
|
#### Mermaid Diagram Generation
|
|
483
480
|
|
|
@@ -849,7 +846,11 @@ Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqr
|
|
|
849
846
|
During the handling of a command, `cqrs.NotificationEvent` events may be generated and then sent to the broker.
|
|
850
847
|
|
|
851
848
|
```python
|
|
852
|
-
class
|
|
849
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
850
|
+
user_id: str
|
|
851
|
+
meeting_id: str
|
|
852
|
+
|
|
853
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
|
|
853
854
|
def __init__(self):
|
|
854
855
|
self._events = []
|
|
855
856
|
|
|
@@ -857,7 +858,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
857
858
|
def events(self):
|
|
858
859
|
return self._events
|
|
859
860
|
|
|
860
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
861
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
861
862
|
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
862
863
|
self._events.append(
|
|
863
864
|
cqrs.NotificationEvent[UserJoinedNotificationPayload](
|
|
@@ -917,7 +918,11 @@ The package implements the [Transactional Outbox](https://microservices.io/patte
|
|
|
917
918
|
pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
|
|
918
919
|
|
|
919
920
|
```python
|
|
920
|
-
class
|
|
921
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
922
|
+
user_id: str
|
|
923
|
+
meeting_id: str
|
|
924
|
+
|
|
925
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
|
|
921
926
|
def __init__(self, outbox: cqrs.OutboxedEventRepository):
|
|
922
927
|
self.outbox = outbox
|
|
923
928
|
|
|
@@ -925,7 +930,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
925
930
|
def events(self):
|
|
926
931
|
return []
|
|
927
932
|
|
|
928
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
933
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
929
934
|
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
930
935
|
# Outbox repository is bound to a session (e.g. via DI request scope).
|
|
931
936
|
# add() takes only the event; commit() persists the outbox and your changes.
|
|
@@ -962,12 +967,14 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
|
|
|
962
967
|
> The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
|
|
963
968
|
> base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
|
|
964
969
|
> `create_all()`, before generating migrations and before the first query.
|
|
970
|
+
|
|
965
971
|
> [!TIP]
|
|
966
972
|
> `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
|
|
967
973
|
> rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
|
|
968
974
|
> `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
|
|
969
975
|
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
970
976
|
> how to register a native type for your own database.
|
|
977
|
+
|
|
971
978
|
> [!TIP]
|
|
972
979
|
> For Protobuf events, implement `proto()` / `from_proto()` and register a
|
|
973
980
|
> `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
|
|
@@ -1032,7 +1039,11 @@ To configure event handling, you need to implement a broker consumer on the side
|
|
|
1032
1039
|
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
1033
1040
|
|
|
1034
1041
|
```python
|
|
1035
|
-
class
|
|
1042
|
+
class JoinMeetingCommand(cqrs.Request[None]):
|
|
1043
|
+
user_id: str
|
|
1044
|
+
meeting_id: str
|
|
1045
|
+
|
|
1046
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand]):
|
|
1036
1047
|
def __init__(self):
|
|
1037
1048
|
self._events = []
|
|
1038
1049
|
|
|
@@ -1040,7 +1051,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
1040
1051
|
def events(self):
|
|
1041
1052
|
return self._events
|
|
1042
1053
|
|
|
1043
|
-
async def handle(self, request: JoinMeetingCommand)
|
|
1054
|
+
async def handle(self, request: JoinMeetingCommand):
|
|
1044
1055
|
STORAGE[request.meeting_id].append(request.user_id)
|
|
1045
1056
|
self._events.append(
|
|
1046
1057
|
UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
|