python-cqrs 5.0.0__tar.gz → 5.1.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.1.0}/PKG-INFO +9 -43
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/README.md +3 -42
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/pyproject.toml +5 -1
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/__init__.py +14 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/outbox/sqlalchemy.py +7 -5
- python_cqrs-5.1.0/src/cqrs/outbox/sqlmodel.py +189 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/storage/sqlalchemy.py +72 -45
- python_cqrs-5.1.0/src/cqrs/saga/storage/sqlmodel.py +203 -0
- python_cqrs-5.1.0/src/cqrs/sqlmodel_base.py +45 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0/src/python_cqrs.egg-info}/PKG-INFO +9 -43
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/python_cqrs.egg-info/SOURCES.txt +3 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/python_cqrs.egg-info/requires.txt +6 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/LICENSE +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/setup.cfg +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/_dataclass_utils.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/bootstrap/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/bootstrap/events.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/bootstrap/requests.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/bootstrap/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/circuit_breaker.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/container/dishka.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/container/scope.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/deserializers/protobuf.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/events/event_emitter.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/events/fallback.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/generic_utils.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/handlers/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/handlers/cor.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/handlers/event.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/handlers/request.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/handlers/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mapping/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mapping/events.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mapping/outbox.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mapping/requests.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mediators/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mediators/_common.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mediators/event.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mediators/request.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mediators/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mediators/streaming.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mermaid/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mermaid/cor.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/mermaid/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/message_brokers/producer.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/models/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/models/event.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/models/pydantic.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/models/request.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/models/response.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/requests/fallback.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/serializers/json.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/serializers/protobuf.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/serializers/protocol.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/cqrs/types.py +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-5.0.0 → python_cqrs-5.1.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.1.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>
|
|
@@ -29,6 +29,9 @@ 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,6 +57,8 @@ 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"
|
|
@@ -98,9 +103,6 @@ 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
106
|
<a href="https://mkdocs.python-cqrs.dev/">
|
|
105
107
|
<img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
|
|
106
108
|
</a>
|
|
@@ -113,17 +115,11 @@ Dynamic: license-file
|
|
|
113
115
|
</p>
|
|
114
116
|
</div>
|
|
115
117
|
|
|
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
118
|
## Table of Contents
|
|
122
119
|
|
|
123
120
|
- [Overview](#overview)
|
|
124
121
|
- [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
|
|
125
122
|
- [Installation](#installation)
|
|
126
|
-
- [Version Support](#version-support)
|
|
127
123
|
- [Quick Start](#quick-start)
|
|
128
124
|
- [Request and Response Types](#request-and-response-types)
|
|
129
125
|
- [Request Handlers](#request-handlers)
|
|
@@ -166,7 +162,7 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
166
162
|
|
|
167
163
|
- **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
|
|
168
164
|
- **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
|
|
169
|
-
- **Storage:** SQLAlchemy
|
|
165
|
+
- **Storage:** SQLAlchemy and SQLModel outbox and saga storage on MySQL/MariaDB and PostgreSQL.
|
|
170
166
|
- **Integration:** Ready for integration with FastAPI and FastStream.
|
|
171
167
|
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
172
168
|
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
@@ -189,11 +185,12 @@ Do not add a Saga for a single `UPDATE` in one service, and do not use a Saga as
|
|
|
189
185
|
pip install python-cqrs
|
|
190
186
|
```
|
|
191
187
|
|
|
192
|
-
Optional extras (5.0+ — pydantic and SQLAlchemy are no longer core dependencies):
|
|
188
|
+
Optional extras (5.0+ — pydantic and SQLAlchemy are no longer core dependencies; SQLModel storage is 5.1+):
|
|
193
189
|
|
|
194
190
|
```bash
|
|
195
191
|
pip install "python-cqrs[pydantic]" # PydanticRequest / Pydantic* events
|
|
196
192
|
pip install "python-cqrs[sqlalchemy]" # Outbox / Saga SQLAlchemy storage
|
|
193
|
+
pip install "python-cqrs[sqlmodel]" # Outbox / Saga SQLModel storage
|
|
197
194
|
pip install "python-cqrs[kafka]" # Kafka broker (aiokafka)
|
|
198
195
|
pip install "python-cqrs[examples]" # FastAPI, FastStream, uvicorn, etc.
|
|
199
196
|
pip install "python-cqrs[aiobreaker]" # Circuit breaker for saga fallbacks
|
|
@@ -203,37 +200,6 @@ Default `Request` / `Response` / `Event` / `DomainEvent` / `NotificationEvent` a
|
|
|
203
200
|
are dataclass-based (`DC*`). Use `Pydantic*` types with the pydantic extra when you
|
|
204
201
|
need validation.
|
|
205
202
|
|
|
206
|
-
### Import paths (5.0+)
|
|
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:
|
|
229
|
-
|
|
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
203
|
|
|
238
204
|
## Quick Start
|
|
239
205
|
|
|
@@ -20,9 +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://codspeed.io/vadikko2/python-cqrs?utm_source=badge">
|
|
24
|
-
<img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed">
|
|
25
|
-
</a>
|
|
26
23
|
<a href="https://mkdocs.python-cqrs.dev/">
|
|
27
24
|
<img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
|
|
28
25
|
</a>
|
|
@@ -35,17 +32,11 @@
|
|
|
35
32
|
</p>
|
|
36
33
|
</div>
|
|
37
34
|
|
|
38
|
-
> [!WARNING]
|
|
39
|
-
> **Breaking Changes in v5.0.0**
|
|
40
|
-
>
|
|
41
|
-
> 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.
|
|
42
|
-
|
|
43
35
|
## Table of Contents
|
|
44
36
|
|
|
45
37
|
- [Overview](#overview)
|
|
46
38
|
- [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
|
|
47
39
|
- [Installation](#installation)
|
|
48
|
-
- [Version Support](#version-support)
|
|
49
40
|
- [Quick Start](#quick-start)
|
|
50
41
|
- [Request and Response Types](#request-and-response-types)
|
|
51
42
|
- [Request Handlers](#request-handlers)
|
|
@@ -88,7 +79,7 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
88
79
|
|
|
89
80
|
- **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
|
|
90
81
|
- **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
|
|
91
|
-
- **Storage:** SQLAlchemy
|
|
82
|
+
- **Storage:** SQLAlchemy and SQLModel outbox and saga storage on MySQL/MariaDB and PostgreSQL.
|
|
92
83
|
- **Integration:** Ready for integration with FastAPI and FastStream.
|
|
93
84
|
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
94
85
|
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
@@ -111,11 +102,12 @@ Do not add a Saga for a single `UPDATE` in one service, and do not use a Saga as
|
|
|
111
102
|
pip install python-cqrs
|
|
112
103
|
```
|
|
113
104
|
|
|
114
|
-
Optional extras (5.0+ — pydantic and SQLAlchemy are no longer core dependencies):
|
|
105
|
+
Optional extras (5.0+ — pydantic and SQLAlchemy are no longer core dependencies; SQLModel storage is 5.1+):
|
|
115
106
|
|
|
116
107
|
```bash
|
|
117
108
|
pip install "python-cqrs[pydantic]" # PydanticRequest / Pydantic* events
|
|
118
109
|
pip install "python-cqrs[sqlalchemy]" # Outbox / Saga SQLAlchemy storage
|
|
110
|
+
pip install "python-cqrs[sqlmodel]" # Outbox / Saga SQLModel storage
|
|
119
111
|
pip install "python-cqrs[kafka]" # Kafka broker (aiokafka)
|
|
120
112
|
pip install "python-cqrs[examples]" # FastAPI, FastStream, uvicorn, etc.
|
|
121
113
|
pip install "python-cqrs[aiobreaker]" # Circuit breaker for saga fallbacks
|
|
@@ -125,37 +117,6 @@ Default `Request` / `Response` / `Event` / `DomainEvent` / `NotificationEvent` a
|
|
|
125
117
|
are dataclass-based (`DC*`). Use `Pydantic*` types with the pydantic extra when you
|
|
126
118
|
need validation.
|
|
127
119
|
|
|
128
|
-
### Import paths (5.0+)
|
|
129
|
-
|
|
130
|
-
| Area | Import |
|
|
131
|
-
|------|--------|
|
|
132
|
-
| Bootstrap | `from cqrs.bootstrap import requests, events, saga` |
|
|
133
|
-
| Mediators | `from cqrs.mediators import RequestMediator, SagaMediator, …` |
|
|
134
|
-
| Handlers | `from cqrs.handlers import RequestHandler, EventHandler, …` |
|
|
135
|
-
| Models | `from cqrs.models import Request, Event, …` / `cqrs.models.pydantic` |
|
|
136
|
-
| Maps | `from cqrs.mapping import RequestMap, EventMap, OutboxedEventMap` |
|
|
137
|
-
| Mermaid | `from cqrs.mermaid import CoRMermaid, SagaMermaid` |
|
|
138
|
-
| Outbox producer | `from cqrs.message_brokers.producer import EventProducer` |
|
|
139
|
-
|
|
140
|
-
`SagaMediator.execute(...)` replaces the former `SagaMediator.stream(...)`
|
|
141
|
-
(StreamingRequestMediator.stream is unchanged).
|
|
142
|
-
|
|
143
|
-
## Version Support
|
|
144
|
-
|
|
145
|
-
| Line | Git branch | What changes |
|
|
146
|
-
|------|------------|--------------|
|
|
147
|
-
| **4.x** | `4.x` | Bug fixes and security only (~12 months after 5.0.0) |
|
|
148
|
-
| **5.x** | `master` | Features and breaking changes |
|
|
149
|
-
|
|
150
|
-
Stay on 4.x until you migrate:
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
pip install "python-cqrs>=4,<5"
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
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.
|
|
157
|
-
|
|
158
|
-
A package-level EOL `DeprecationWarning` toward 5.x will be added on the **first 4.x patch after 5.0.0** — not before.
|
|
159
120
|
|
|
160
121
|
## Quick Start
|
|
161
122
|
|
|
@@ -29,12 +29,13 @@ 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.1.0"
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
aiobreaker = ["aiobreaker>=0.3.0"]
|
|
36
36
|
pydantic = ["pydantic==2.*"]
|
|
37
37
|
sqlalchemy = ["sqlalchemy[asyncio]==2.0.*"]
|
|
38
|
+
sqlmodel = ["sqlmodel>=0.0.22", "sqlalchemy[asyncio]==2.0.*"]
|
|
38
39
|
dev = [
|
|
39
40
|
# Develope tools
|
|
40
41
|
"pycln==2.5.0",
|
|
@@ -64,6 +65,8 @@ dev = [
|
|
|
64
65
|
# Full suite extras
|
|
65
66
|
"pydantic==2.*",
|
|
66
67
|
"sqlalchemy[asyncio]==2.0.*",
|
|
68
|
+
"sqlmodel>=0.0.22",
|
|
69
|
+
"aiosqlite>=0.20.0",
|
|
67
70
|
]
|
|
68
71
|
examples = [
|
|
69
72
|
"fastapi==0.109.*",
|
|
@@ -95,6 +98,7 @@ testpaths = ["tests"]
|
|
|
95
98
|
markers = [
|
|
96
99
|
"pydantic: tests that require the pydantic optional extra",
|
|
97
100
|
"sqlalchemy: tests that require the sqlalchemy optional extra",
|
|
101
|
+
"sqlmodel: tests that require the sqlmodel optional extra",
|
|
98
102
|
]
|
|
99
103
|
|
|
100
104
|
[tool.setuptools.packages.find]
|
|
@@ -12,6 +12,7 @@ if typing.TYPE_CHECKING:
|
|
|
12
12
|
SqlAlchemyOutboxedEventRepository,
|
|
13
13
|
rebind_outbox_model,
|
|
14
14
|
)
|
|
15
|
+
from cqrs.outbox.sqlmodel import SqlModelOutboxedEventRepository
|
|
15
16
|
from cqrs.sqlalchemy_types import (
|
|
16
17
|
Binary16,
|
|
17
18
|
DialectAwareType,
|
|
@@ -129,6 +130,7 @@ __all__ = (
|
|
|
129
130
|
"PydanticResponse",
|
|
130
131
|
"OutboxedEventRepository",
|
|
131
132
|
"SqlAlchemyOutboxedEventRepository",
|
|
133
|
+
"SqlModelOutboxedEventRepository",
|
|
132
134
|
"EventProducer",
|
|
133
135
|
"EventSerializer",
|
|
134
136
|
"EventCodec",
|
|
@@ -187,6 +189,13 @@ _LAZY_SQLALCHEMY = {
|
|
|
187
189
|
"PayloadBinary": ("cqrs.sqlalchemy_types", "PayloadBinary"),
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
_LAZY_SQLMODEL = {
|
|
193
|
+
"SqlModelOutboxedEventRepository": (
|
|
194
|
+
"cqrs.outbox.sqlmodel",
|
|
195
|
+
"SqlModelOutboxedEventRepository",
|
|
196
|
+
),
|
|
197
|
+
}
|
|
198
|
+
|
|
190
199
|
|
|
191
200
|
def __getattr__(name: str) -> typing.Any:
|
|
192
201
|
if name in _LAZY_PYDANTIC:
|
|
@@ -201,4 +210,9 @@ def __getattr__(name: str) -> typing.Any:
|
|
|
201
210
|
# ImportError message is raised by the sqlalchemy modules themselves.
|
|
202
211
|
module = __import__(module_name, fromlist=[attr])
|
|
203
212
|
return getattr(module, attr)
|
|
213
|
+
if name in _LAZY_SQLMODEL:
|
|
214
|
+
module_name, attr = _LAZY_SQLMODEL[name]
|
|
215
|
+
# ImportError message is raised by the sqlmodel modules themselves.
|
|
216
|
+
module = __import__(module_name, fromlist=[attr])
|
|
217
|
+
return getattr(module, attr)
|
|
204
218
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -173,11 +173,13 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
|
173
173
|
*,
|
|
174
174
|
serializer: EventCodec | None = None,
|
|
175
175
|
event_map: map.OutboxedEventMap | None = None,
|
|
176
|
+
model: typing.Any = None,
|
|
176
177
|
):
|
|
177
178
|
self.session = session
|
|
178
179
|
self._compressor = compressor
|
|
179
180
|
self._serializer = serializer or JsonEventSerializer()
|
|
180
181
|
self._event_map = event_map if event_map is not None else map.OutboxedEventMap
|
|
182
|
+
self._model = OutboxModel if model is None else model
|
|
181
183
|
|
|
182
184
|
def _resolve_codec(self, event_name: str) -> EventCodec:
|
|
183
185
|
return self._event_map.get_serializer(event_name) or self._serializer
|
|
@@ -211,7 +213,7 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
|
211
213
|
bytes_payload = self._compressor.compress(bytes_payload)
|
|
212
214
|
|
|
213
215
|
self.session.add(
|
|
214
|
-
|
|
216
|
+
self._model(
|
|
215
217
|
event_id=event.event_id,
|
|
216
218
|
event_id_bin=event.event_id.bytes,
|
|
217
219
|
event_name=event.event_name,
|
|
@@ -221,7 +223,7 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
|
221
223
|
),
|
|
222
224
|
)
|
|
223
225
|
|
|
224
|
-
def _process_events(self, model:
|
|
226
|
+
def _process_events(self, model: typing.Any) -> repository.OutboxedEvent | None:
|
|
225
227
|
event_dict = model.row_to_dict()
|
|
226
228
|
event_name = event_dict["event_name"]
|
|
227
229
|
|
|
@@ -260,8 +262,8 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
|
260
262
|
batch_size: int = 100,
|
|
261
263
|
topic: typing.Text | None = None,
|
|
262
264
|
) -> typing.List[repository.OutboxedEvent]:
|
|
263
|
-
events: typing.Sequence[
|
|
264
|
-
(await self.session.execute(
|
|
265
|
+
events: typing.Sequence[typing.Any] = (
|
|
266
|
+
(await self.session.execute(self._model.get_batch_query(batch_size, topic))).scalars().all()
|
|
265
267
|
)
|
|
266
268
|
|
|
267
269
|
result = []
|
|
@@ -285,7 +287,7 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
|
285
287
|
new_status: repository.EventStatus,
|
|
286
288
|
) -> None:
|
|
287
289
|
await self.session.execute(
|
|
288
|
-
statement=
|
|
290
|
+
statement=self._model.update_status_query(outboxed_event_id, new_status),
|
|
289
291
|
)
|
|
290
292
|
|
|
291
293
|
async def commit(self):
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""SQLModel outbox: same table, queries, and repository behavior as SQLAlchemy."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import typing
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from cqrs import compressors
|
|
8
|
+
from cqrs.mapping import outbox as map
|
|
9
|
+
from cqrs.outbox import repository
|
|
10
|
+
from cqrs.serializers.protocol import EventCodec
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from cqrs.outbox.sqlalchemy import (
|
|
14
|
+
MAX_FLUSH_COUNTER_VALUE,
|
|
15
|
+
BinaryUUID,
|
|
16
|
+
OutboxModel as SqlAlchemyOutboxModel,
|
|
17
|
+
SqlAlchemyOutboxedEventRepository,
|
|
18
|
+
rebind_outbox_model,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
import sqlalchemy
|
|
22
|
+
from sqlalchemy import func
|
|
23
|
+
from sqlmodel import Field
|
|
24
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
25
|
+
|
|
26
|
+
from cqrs import sqlalchemy_types
|
|
27
|
+
from cqrs.sqlmodel_base import CqrsSQLModel, SqlAlchemyExecuteSession
|
|
28
|
+
except ImportError:
|
|
29
|
+
raise ImportError(
|
|
30
|
+
"You are trying to use the SQLModel outbox implementation, "
|
|
31
|
+
"but 'sqlmodel' is not installed. "
|
|
32
|
+
"Please install it using: pip install python-cqrs[sqlmodel]",
|
|
33
|
+
) from None
|
|
34
|
+
|
|
35
|
+
Base = CqrsSQLModel
|
|
36
|
+
|
|
37
|
+
DEFAULT_OUTBOX_TABLE_NAME = "outbox"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OutboxModel(Base, table=True):
|
|
41
|
+
__tablename__ = DEFAULT_OUTBOX_TABLE_NAME # type: ignore[assignment]
|
|
42
|
+
|
|
43
|
+
__table_args__ = (
|
|
44
|
+
sqlalchemy.UniqueConstraint(
|
|
45
|
+
"event_id_bin",
|
|
46
|
+
"event_name",
|
|
47
|
+
name="event_id_unique_index",
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
id: int | None = Field(
|
|
52
|
+
default=None,
|
|
53
|
+
sa_column=sqlalchemy.Column(
|
|
54
|
+
sqlalchemy.BigInteger,
|
|
55
|
+
sqlalchemy.Identity(),
|
|
56
|
+
primary_key=True,
|
|
57
|
+
nullable=False,
|
|
58
|
+
autoincrement=True,
|
|
59
|
+
comment="Identity",
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
event_id: uuid.UUID = Field(
|
|
63
|
+
sa_column=sqlalchemy.Column(
|
|
64
|
+
sqlalchemy_types.UUIDBinary,
|
|
65
|
+
nullable=False,
|
|
66
|
+
comment="Event idempotency id",
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
event_id_bin: bytes = Field(
|
|
70
|
+
sa_column=sqlalchemy.Column(
|
|
71
|
+
sqlalchemy_types.Binary16,
|
|
72
|
+
nullable=False,
|
|
73
|
+
comment="Event idempotency id in 16 bit presentation",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
event_status: repository.EventStatus = Field(
|
|
77
|
+
default=repository.EventStatus.NEW,
|
|
78
|
+
sa_column=sqlalchemy.Column(
|
|
79
|
+
sqlalchemy.Enum(repository.EventStatus),
|
|
80
|
+
nullable=False,
|
|
81
|
+
default=repository.EventStatus.NEW,
|
|
82
|
+
comment="Event producing status",
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
flush_counter: int = Field(
|
|
86
|
+
default=0,
|
|
87
|
+
sa_column=sqlalchemy.Column(
|
|
88
|
+
sqlalchemy.SmallInteger,
|
|
89
|
+
nullable=False,
|
|
90
|
+
default=0,
|
|
91
|
+
comment="Event producing flush counter",
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
event_name: str = Field(
|
|
95
|
+
sa_column=sqlalchemy.Column(
|
|
96
|
+
sqlalchemy.String(255),
|
|
97
|
+
nullable=False,
|
|
98
|
+
comment="Event name",
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
topic: str = Field(
|
|
102
|
+
default="",
|
|
103
|
+
sa_column=sqlalchemy.Column(
|
|
104
|
+
sqlalchemy.String(255),
|
|
105
|
+
nullable=False,
|
|
106
|
+
default="",
|
|
107
|
+
comment="Event topic",
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
created_at: datetime.datetime | None = Field(
|
|
111
|
+
default=None,
|
|
112
|
+
sa_column=sqlalchemy.Column(
|
|
113
|
+
sqlalchemy.DateTime,
|
|
114
|
+
nullable=False,
|
|
115
|
+
server_default=func.now(),
|
|
116
|
+
comment="Event creation timestamp",
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
payload: bytes = Field(
|
|
120
|
+
sa_column=sqlalchemy.Column(
|
|
121
|
+
sqlalchemy_types.PayloadBinary,
|
|
122
|
+
nullable=False,
|
|
123
|
+
comment="Event payload",
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def row_to_dict(self) -> typing.Dict[typing.Text, typing.Any]:
|
|
128
|
+
table = typing.cast(typing.Any, self).__table__
|
|
129
|
+
return {column.name: getattr(self, column.name) for column in table.columns}
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def get_batch_query(
|
|
133
|
+
cls,
|
|
134
|
+
size: int,
|
|
135
|
+
topic: typing.Text | None = None,
|
|
136
|
+
) -> sqlalchemy.Select:
|
|
137
|
+
query = SqlAlchemyOutboxModel.get_batch_query
|
|
138
|
+
return query.__func__(cls, size, topic) # type: ignore[attr-defined]
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def update_status_query(
|
|
142
|
+
cls,
|
|
143
|
+
outboxed_event_id: int,
|
|
144
|
+
status: repository.EventStatus,
|
|
145
|
+
) -> sqlalchemy.Update:
|
|
146
|
+
query = SqlAlchemyOutboxModel.update_status_query
|
|
147
|
+
return query.__func__(cls, outboxed_event_id, status) # type: ignore[attr-defined]
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def status_sorting_case(cls) -> sqlalchemy.Case:
|
|
151
|
+
query = SqlAlchemyOutboxModel.status_sorting_case
|
|
152
|
+
return query.__func__(cls) # type: ignore[attr-defined]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SqlModelOutboxedEventRepository(SqlAlchemyOutboxedEventRepository):
|
|
156
|
+
"""Outbox repository that stores rows as SQLModel ``OutboxModel`` instances.
|
|
157
|
+
|
|
158
|
+
Pass a ``sqlmodel.ext.asyncio.session.AsyncSession``. Behavior matches
|
|
159
|
+
``SqlAlchemyOutboxedEventRepository``: the same batch query, flush budget,
|
|
160
|
+
status ordering, codecs, and compressor hooks.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
session: AsyncSession,
|
|
166
|
+
compressor: compressors.Compressor | None = None,
|
|
167
|
+
*,
|
|
168
|
+
serializer: EventCodec | None = None,
|
|
169
|
+
event_map: map.OutboxedEventMap | None = None,
|
|
170
|
+
):
|
|
171
|
+
super().__init__(
|
|
172
|
+
typing.cast(typing.Any, SqlAlchemyExecuteSession(session)),
|
|
173
|
+
compressor,
|
|
174
|
+
serializer=serializer,
|
|
175
|
+
event_map=event_map,
|
|
176
|
+
model=OutboxModel,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = (
|
|
181
|
+
"AsyncSession",
|
|
182
|
+
"Base",
|
|
183
|
+
"BinaryUUID",
|
|
184
|
+
"DEFAULT_OUTBOX_TABLE_NAME",
|
|
185
|
+
"MAX_FLUSH_COUNTER_VALUE",
|
|
186
|
+
"OutboxModel",
|
|
187
|
+
"SqlModelOutboxedEventRepository",
|
|
188
|
+
"rebind_outbox_model",
|
|
189
|
+
)
|