python-cqrs 4.12.0__tar.gz → 4.14.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-4.12.0 → python_cqrs-4.14.0}/PKG-INFO +97 -29
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/README.md +88 -26
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/pyproject.toml +8 -3
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/__init__.py +28 -1
- python_cqrs-4.14.0/src/cqrs/container/__init__.py +18 -0
- python_cqrs-4.14.0/src/cqrs/container/di.py +75 -0
- python_cqrs-4.14.0/src/cqrs/container/dishka.py +48 -0
- python_cqrs-4.14.0/src/cqrs/container/protocol.py +43 -0
- python_cqrs-4.14.0/src/cqrs/container/scope.py +321 -0
- python_cqrs-4.14.0/src/cqrs/dispatcher/event.py +111 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/dispatcher/request.py +67 -46
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/dispatcher/saga.py +8 -4
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/dispatcher/streaming.py +70 -47
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/bootstrap.py +9 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/event_emitter.py +90 -51
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/mediator.py +114 -10
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/outbox/sqlalchemy.py +16 -41
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/bootstrap.py +29 -50
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/bootstrap.py +20 -32
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/compensation.py +77 -15
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/execution.py +141 -125
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/recovery.py +29 -10
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/saga.py +49 -19
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/storage/sqlalchemy.py +3 -1
- python_cqrs-4.14.0/src/cqrs/sqlalchemy_types.py +237 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/python_cqrs.egg-info/PKG-INFO +97 -29
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/python_cqrs.egg-info/SOURCES.txt +3 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/python_cqrs.egg-info/requires.txt +9 -2
- python_cqrs-4.12.0/src/cqrs/container/__init__.py +0 -3
- python_cqrs-4.12.0/src/cqrs/container/di.py +0 -26
- python_cqrs-4.12.0/src/cqrs/container/protocol.py +0 -20
- python_cqrs-4.12.0/src/cqrs/dispatcher/event.py +0 -96
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/LICENSE +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/setup.cfg +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/circuit_breaker.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/event.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/fallback.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/events/map.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/generic_utils.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/outbox/map.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/producer.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/cor_request_handler.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/fallback.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/mermaid.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/requests/request_handler.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/response.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/mermaid.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/step.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/cqrs/types.py +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.12.0 → python_cqrs-4.14.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: 4.
|
|
3
|
+
Version: 4.14.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>
|
|
@@ -42,18 +42,22 @@ Requires-Dist: requests==2.*; extra == "dev"
|
|
|
42
42
|
Requires-Dist: pytest~=7.4.2; extra == "dev"
|
|
43
43
|
Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
|
|
44
44
|
Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
45
|
-
Requires-Dist:
|
|
45
|
+
Requires-Dist: alembic>=1.13.0; extra == "dev"
|
|
46
|
+
Requires-Dist: cryptography==50.0.0; extra == "dev"
|
|
46
47
|
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
47
48
|
Requires-Dist: asyncpg>=0.29.0; extra == "dev"
|
|
48
49
|
Requires-Dist: redis>=5.0.0; extra == "dev"
|
|
49
50
|
Requires-Dist: aiobreaker>=0.3.0; extra == "dev"
|
|
51
|
+
Requires-Dist: dishka>=1.0; extra == "dev"
|
|
50
52
|
Provides-Extra: examples
|
|
51
53
|
Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
52
54
|
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
53
55
|
Requires-Dist: faker>=37.12.0; extra == "examples"
|
|
54
56
|
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
55
|
-
Requires-Dist: aiohttp==3.14.
|
|
57
|
+
Requires-Dist: aiohttp==3.14.3; extra == "examples"
|
|
56
58
|
Requires-Dist: protobuf>=4.25.8; extra == "examples"
|
|
59
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
|
|
60
|
+
Requires-Dist: dishka>=1.0; extra == "examples"
|
|
57
61
|
Provides-Extra: kafka
|
|
58
62
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
59
63
|
Requires-Dist: retry-async==0.1.*; extra == "kafka"
|
|
@@ -61,6 +65,8 @@ Provides-Extra: rabbit
|
|
|
61
65
|
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
62
66
|
Provides-Extra: sqlalchemy
|
|
63
67
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "sqlalchemy"
|
|
68
|
+
Provides-Extra: dishka
|
|
69
|
+
Requires-Dist: dishka>=1.0; extra == "dishka"
|
|
64
70
|
Dynamic: license-file
|
|
65
71
|
|
|
66
72
|
<div align="center">
|
|
@@ -108,12 +114,14 @@ Dynamic: license-file
|
|
|
108
114
|
## Table of Contents
|
|
109
115
|
|
|
110
116
|
- [Overview](#overview)
|
|
117
|
+
- [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
|
|
111
118
|
- [Installation](#installation)
|
|
112
119
|
- [Quick Start](#quick-start)
|
|
113
120
|
- [Request and Response Types](#request-and-response-types)
|
|
114
121
|
- [Request Handlers](#request-handlers)
|
|
115
122
|
- [Mapping](#mapping)
|
|
116
123
|
- [DI container](#di-container)
|
|
124
|
+
- [Scoped dependencies](#scoped-dependencies)
|
|
117
125
|
- [Bootstrap](#bootstrap)
|
|
118
126
|
- [Saga Pattern](#saga-pattern)
|
|
119
127
|
- [Producing Notification Events](#producing-notification-events)
|
|
@@ -150,10 +158,21 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
150
158
|
|
|
151
159
|
- **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
|
|
152
160
|
- **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
|
|
161
|
+
- **Storage:** SQLAlchemy-based Outbox and Saga storage on MySQL/MariaDB and PostgreSQL.
|
|
153
162
|
- **Integration:** Ready for integration with FastAPI and FastStream.
|
|
154
163
|
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
155
164
|
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
156
165
|
|
|
166
|
+
## When NOT to use python-cqrs
|
|
167
|
+
|
|
168
|
+
CQRS, Outbox, and Saga are tools for specific problems — not a default for every service.
|
|
169
|
+
|
|
170
|
+
- **Prefer a local DB transaction** for single-service CRUD (one database, no broker, no compensation).
|
|
171
|
+
- **Use [Transactional Outbox](#transactional-outbox)** when you dual-write to a database **and** a message broker. Commit the business row and the outbox row together; a publisher drains the outbox later.
|
|
172
|
+
- **Use [Saga](#saga-pattern)** when a multi-step flow crosses service/database boundaries and a later step must be compensated if it fails.
|
|
173
|
+
|
|
174
|
+
Do not add a Saga for a single `UPDATE` in one service, and do not use a Saga as a substitute for Outbox. Full write-up: [When NOT to use python-cqrs](docs/when_not_to_use.md).
|
|
175
|
+
|
|
157
176
|
## Installation
|
|
158
177
|
|
|
159
178
|
**Python 3.10+** is required.
|
|
@@ -250,7 +269,7 @@ class CustomResponse(cqrs.IResponse):
|
|
|
250
269
|
return cls(result=kwargs["result"], status=kwargs["status"])
|
|
251
270
|
```
|
|
252
271
|
|
|
253
|
-
A complete example can be found in [request_response_types.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_response_types.py)
|
|
272
|
+
A complete example can be found in [request_response_types.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_response_types.py)
|
|
254
273
|
|
|
255
274
|
## Request Handlers
|
|
256
275
|
|
|
@@ -283,7 +302,7 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
283
302
|
```
|
|
284
303
|
|
|
285
304
|
A complete example can be found in
|
|
286
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
305
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
|
|
287
306
|
|
|
288
307
|
### Query handler
|
|
289
308
|
|
|
@@ -313,7 +332,7 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
313
332
|
```
|
|
314
333
|
|
|
315
334
|
A complete example can be found in
|
|
316
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
335
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
|
|
317
336
|
|
|
318
337
|
### Streaming Request Handler
|
|
319
338
|
|
|
@@ -349,7 +368,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
349
368
|
```
|
|
350
369
|
|
|
351
370
|
A complete example can be found in
|
|
352
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
371
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py)
|
|
353
372
|
|
|
354
373
|
### Chain of Responsibility Request Handler
|
|
355
374
|
|
|
@@ -412,7 +431,7 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
|
412
431
|
```
|
|
413
432
|
|
|
414
433
|
A complete example can be found in
|
|
415
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_request_handler.py)
|
|
434
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py)
|
|
416
435
|
|
|
417
436
|
#### Mermaid Diagram Generation
|
|
418
437
|
|
|
@@ -432,7 +451,7 @@ sequence_diagram = generator.sequence()
|
|
|
432
451
|
class_diagram = generator.class_diagram()
|
|
433
452
|
```
|
|
434
453
|
|
|
435
|
-
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_mermaid.py)
|
|
454
|
+
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_mermaid.py)
|
|
436
455
|
|
|
437
456
|
## Mapping
|
|
438
457
|
|
|
@@ -493,7 +512,11 @@ def saga_mapper(mapper: cqrs.SagaMap) -> None:
|
|
|
493
512
|
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
494
513
|
dependency management simpler.
|
|
495
514
|
|
|
496
|
-
The package supports
|
|
515
|
+
The package supports three DI container libraries:
|
|
516
|
+
|
|
517
|
+
- [`di`](#di-library) (default)
|
|
518
|
+
- [`dependency-injector`](#dependency-injector-library)
|
|
519
|
+
- [dishka](https://github.com/reagento/dishka) via `DishkaCQRSContainer` (`pip install python-cqrs[dishka]`)
|
|
497
520
|
|
|
498
521
|
### di library
|
|
499
522
|
|
|
@@ -522,7 +545,7 @@ def setup_di() -> di.Container:
|
|
|
522
545
|
```
|
|
523
546
|
|
|
524
547
|
A complete example can be found in
|
|
525
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/
|
|
548
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/di/di_basic.py)
|
|
526
549
|
|
|
527
550
|
### dependency-injector library
|
|
528
551
|
|
|
@@ -538,7 +561,8 @@ class ApplicationContainer(containers.DeclarativeContainer):
|
|
|
538
561
|
service = providers.Factory(ServiceImplementation)
|
|
539
562
|
|
|
540
563
|
# Create CQRS container adapter
|
|
541
|
-
cqrs_container = DependencyInjectorCQRSContainer(
|
|
564
|
+
cqrs_container = DependencyInjectorCQRSContainer()
|
|
565
|
+
cqrs_container.attach_external_container(ApplicationContainer())
|
|
542
566
|
|
|
543
567
|
# Use with bootstrap
|
|
544
568
|
mediator = bootstrap.bootstrap(
|
|
@@ -549,8 +573,32 @@ mediator = bootstrap.bootstrap(
|
|
|
549
573
|
```
|
|
550
574
|
|
|
551
575
|
Complete examples can be found in:
|
|
552
|
-
- [Simple example](https://github.com/vadikko2/python-cqrs/blob/master/examples/
|
|
553
|
-
- [Practical example with FastAPI](https://github.com/vadikko2/python-cqrs/blob/master/examples/
|
|
576
|
+
- [Simple example](https://github.com/vadikko2/python-cqrs/blob/master/examples/di/dependency_injector_simple.py)
|
|
577
|
+
- [Practical example with FastAPI](https://github.com/vadikko2/python-cqrs/blob/master/examples/di/dependency_injector_practical.py)
|
|
578
|
+
|
|
579
|
+
### Scoped dependencies
|
|
580
|
+
|
|
581
|
+
By default, the container opens and closes a scope **inside** `resolve()`. A generator such as `async def session() -> AsyncIterator[AsyncSession]` therefore finishes **before** `handle`, which is why people inject factories.
|
|
582
|
+
|
|
583
|
+
Pass `scope_strategy=ScopeStrategy.SEND` and that session lives until `mediator.send()` returns — including domain-event handlers that run afterwards.
|
|
584
|
+
|
|
585
|
+
If you do not use generator providers, leave the default `ScopeStrategy.NONE` and change nothing.
|
|
586
|
+
|
|
587
|
+
```python
|
|
588
|
+
mediator = bootstrap.bootstrap(
|
|
589
|
+
di_container=container,
|
|
590
|
+
commands_mapper=commands_mapper,
|
|
591
|
+
scope_strategy=cqrs.ScopeStrategy.SEND,
|
|
592
|
+
)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Copy-paste SQLAlchemy session + outbox under one SEND scope: the [Tutorial](https://mkdocs.python-cqrs.dev/scoped_dependencies/tutorial/) (`examples/di/scoped_dependencies_sqlalchemy.py`). Minimal `di` UoW: `examples/di/scoped_dependencies_di.py`.
|
|
596
|
+
|
|
597
|
+
- **SEND** — one UoW per `send()` (command, fallback, and domain events).
|
|
598
|
+
- **HANDLER** — a fresh UoW per handler, including fallback (the primary rolls back first).
|
|
599
|
+
- **NONE** — legacy one-shot resolve; the default.
|
|
600
|
+
|
|
601
|
+
`di`'s `scope="request"` is provider lifetime, not a CQRS scope — that is only `scope_strategy=`. Docs: [Scoped Dependencies](https://mkdocs.python-cqrs.dev/scoped_dependencies/).
|
|
554
602
|
|
|
555
603
|
## Bootstrap
|
|
556
604
|
|
|
@@ -715,6 +763,7 @@ await recover_saga(
|
|
|
715
763
|
context_builder=OrderContext,
|
|
716
764
|
container=di_container, # Same container used in bootstrap
|
|
717
765
|
storage=storage,
|
|
766
|
+
scope_strategy=cqrs.ScopeStrategy.SEND, # Match the original run
|
|
718
767
|
)
|
|
719
768
|
|
|
720
769
|
# Access execution history (SagaLog) for monitoring and debugging
|
|
@@ -747,7 +796,7 @@ sequence_diagram = generator.sequence()
|
|
|
747
796
|
class_diagram = generator.class_diagram()
|
|
748
797
|
```
|
|
749
798
|
|
|
750
|
-
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/saga_mermaid.py)
|
|
799
|
+
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/saga/saga_mermaid.py)
|
|
751
800
|
|
|
752
801
|
## Producing Notification Events
|
|
753
802
|
|
|
@@ -787,7 +836,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
787
836
|
```
|
|
788
837
|
|
|
789
838
|
A complete example can be found in
|
|
790
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/event_producing.py)
|
|
839
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/event_producing.py)
|
|
791
840
|
|
|
792
841
|
After processing the command/request, if there are any Notification/ECST events,
|
|
793
842
|
the EventEmitter is invoked to produce the events via the message broker.
|
|
@@ -858,16 +907,29 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
858
907
|
```
|
|
859
908
|
|
|
860
909
|
A complete example can be found in
|
|
861
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
910
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/save_events_into_outbox.py)
|
|
862
911
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
> By default, it is set to `outbox`.
|
|
912
|
+
A runnable FastAPI flow (route → command → transactional outbox → publisher stub) is in
|
|
913
|
+
[fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/fastapi_outbox.py)
|
|
866
914
|
|
|
867
915
|
> [!TIP]
|
|
868
|
-
>
|
|
869
|
-
>
|
|
870
|
-
the
|
|
916
|
+
> The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
|
|
917
|
+
> base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
|
|
918
|
+
> `create_all()`, before generating migrations and before the first query.
|
|
919
|
+
> [!TIP]
|
|
920
|
+
> `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
|
|
921
|
+
> rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
|
|
922
|
+
> `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
|
|
923
|
+
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
924
|
+
> how to register a native type for your own database.
|
|
925
|
+
> [!TIP]
|
|
926
|
+
> For Protobuf events, implement `proto()` / `from_proto()` on the notification
|
|
927
|
+
> event (see [Protobuf messaging](#protobuf-messaging) and the schema assets in
|
|
928
|
+
> [examples/proto/](https://github.com/vadikko2/python-cqrs/tree/master/examples/proto)).
|
|
929
|
+
> Outbox persistence is unchanged — use
|
|
930
|
+
> [save_events_into_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/save_events_into_outbox.py);
|
|
931
|
+
> Protobuf applies when producing to the broker. Full guide:
|
|
932
|
+
> [Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
871
933
|
|
|
872
934
|
## Producing Events from Outbox to Kafka
|
|
873
935
|
|
|
@@ -906,7 +968,7 @@ async with session_factory() as session:
|
|
|
906
968
|
```
|
|
907
969
|
|
|
908
970
|
A complete example can be found in
|
|
909
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
971
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/kafka_outboxed_event_producing.py)
|
|
910
972
|
|
|
911
973
|
## Transaction log tailing
|
|
912
974
|
|
|
@@ -949,7 +1011,7 @@ class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
|
949
1011
|
```
|
|
950
1012
|
|
|
951
1013
|
A complete example can be found in
|
|
952
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/domain_event_handler.py)
|
|
1014
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/domain_event_handler.py)
|
|
953
1015
|
|
|
954
1016
|
### Parallel Event Processing
|
|
955
1017
|
|
|
@@ -980,6 +1042,7 @@ mediator = bootstrap.bootstrap_streaming(
|
|
|
980
1042
|
> - Set `max_concurrent_event_handlers` to limit the number of simultaneously running event handlers
|
|
981
1043
|
> - Set `concurrent_event_handle_enable=False` to disable parallel processing and process events sequentially
|
|
982
1044
|
> - The default value for `max_concurrent_event_handlers` is `10` for `StreamingRequestMediator` and `1` for `RequestMediator`
|
|
1045
|
+
> - `ScopeStrategy.SEND` always uses sequential events (`concurrent=None` → `False`). Passing `concurrent_event_handle_enable=True` with `SEND` raises `ValueError`; use `HANDLER` or `NONE` for parallel events.
|
|
983
1046
|
|
|
984
1047
|
## Integration with presentation layers
|
|
985
1048
|
|
|
@@ -1017,7 +1080,10 @@ async def join_metting(
|
|
|
1017
1080
|
```
|
|
1018
1081
|
|
|
1019
1082
|
A complete example can be found in
|
|
1020
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
|
|
1083
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/fastapi_integration.py)
|
|
1084
|
+
|
|
1085
|
+
End-to-end command + outbox sample (business row and outbox event in one transaction, plus a publisher stub):
|
|
1086
|
+
[fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/fastapi_outbox.py)
|
|
1021
1087
|
|
|
1022
1088
|
### Kafka events consuming
|
|
1023
1089
|
|
|
@@ -1065,7 +1131,7 @@ async def hello_world_event_handler(
|
|
|
1065
1131
|
```
|
|
1066
1132
|
|
|
1067
1133
|
A complete example can be found in
|
|
1068
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
|
|
1134
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/kafka_event_consuming.py)
|
|
1069
1135
|
|
|
1070
1136
|
### FastAPI SSE Streaming
|
|
1071
1137
|
|
|
@@ -1086,7 +1152,7 @@ def streaming_mediator_factory() -> cqrs.StreamingRequestMediator:
|
|
|
1086
1152
|
domain_events_mapper=domain_events_mapper,
|
|
1087
1153
|
message_broker=broker,
|
|
1088
1154
|
max_concurrent_event_handlers=3,
|
|
1089
|
-
concurrent_event_handle_enable=True,
|
|
1155
|
+
concurrent_event_handle_enable=True, # HANDLER/NONE only; SEND forbids this
|
|
1090
1156
|
)
|
|
1091
1157
|
|
|
1092
1158
|
@app.post("/process-files")
|
|
@@ -1113,7 +1179,9 @@ async def process_files_stream(
|
|
|
1113
1179
|
```
|
|
1114
1180
|
|
|
1115
1181
|
A complete example can be found in
|
|
1116
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_sse_streaming.py)
|
|
1182
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/fastapi_sse_streaming.py)
|
|
1183
|
+
|
|
1184
|
+
If the client disconnects, close the iterator (`aclose()` / `async with aclosing(...)`) so a `SEND` UoW is not held until GC.
|
|
1117
1185
|
|
|
1118
1186
|
## Protobuf messaging
|
|
1119
1187
|
|
|
@@ -43,12 +43,14 @@
|
|
|
43
43
|
## Table of Contents
|
|
44
44
|
|
|
45
45
|
- [Overview](#overview)
|
|
46
|
+
- [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
|
|
46
47
|
- [Installation](#installation)
|
|
47
48
|
- [Quick Start](#quick-start)
|
|
48
49
|
- [Request and Response Types](#request-and-response-types)
|
|
49
50
|
- [Request Handlers](#request-handlers)
|
|
50
51
|
- [Mapping](#mapping)
|
|
51
52
|
- [DI container](#di-container)
|
|
53
|
+
- [Scoped dependencies](#scoped-dependencies)
|
|
52
54
|
- [Bootstrap](#bootstrap)
|
|
53
55
|
- [Saga Pattern](#saga-pattern)
|
|
54
56
|
- [Producing Notification Events](#producing-notification-events)
|
|
@@ -85,10 +87,21 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
85
87
|
|
|
86
88
|
- **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
|
|
87
89
|
- **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
|
|
90
|
+
- **Storage:** SQLAlchemy-based Outbox and Saga storage on MySQL/MariaDB and PostgreSQL.
|
|
88
91
|
- **Integration:** Ready for integration with FastAPI and FastStream.
|
|
89
92
|
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
90
93
|
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
91
94
|
|
|
95
|
+
## When NOT to use python-cqrs
|
|
96
|
+
|
|
97
|
+
CQRS, Outbox, and Saga are tools for specific problems — not a default for every service.
|
|
98
|
+
|
|
99
|
+
- **Prefer a local DB transaction** for single-service CRUD (one database, no broker, no compensation).
|
|
100
|
+
- **Use [Transactional Outbox](#transactional-outbox)** when you dual-write to a database **and** a message broker. Commit the business row and the outbox row together; a publisher drains the outbox later.
|
|
101
|
+
- **Use [Saga](#saga-pattern)** when a multi-step flow crosses service/database boundaries and a later step must be compensated if it fails.
|
|
102
|
+
|
|
103
|
+
Do not add a Saga for a single `UPDATE` in one service, and do not use a Saga as a substitute for Outbox. Full write-up: [When NOT to use python-cqrs](docs/when_not_to_use.md).
|
|
104
|
+
|
|
92
105
|
## Installation
|
|
93
106
|
|
|
94
107
|
**Python 3.10+** is required.
|
|
@@ -185,7 +198,7 @@ class CustomResponse(cqrs.IResponse):
|
|
|
185
198
|
return cls(result=kwargs["result"], status=kwargs["status"])
|
|
186
199
|
```
|
|
187
200
|
|
|
188
|
-
A complete example can be found in [request_response_types.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_response_types.py)
|
|
201
|
+
A complete example can be found in [request_response_types.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_response_types.py)
|
|
189
202
|
|
|
190
203
|
## Request Handlers
|
|
191
204
|
|
|
@@ -218,7 +231,7 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
218
231
|
```
|
|
219
232
|
|
|
220
233
|
A complete example can be found in
|
|
221
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
234
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
|
|
222
235
|
|
|
223
236
|
### Query handler
|
|
224
237
|
|
|
@@ -248,7 +261,7 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
248
261
|
```
|
|
249
262
|
|
|
250
263
|
A complete example can be found in
|
|
251
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
264
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/request_handler.py)
|
|
252
265
|
|
|
253
266
|
### Streaming Request Handler
|
|
254
267
|
|
|
@@ -284,7 +297,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
284
297
|
```
|
|
285
298
|
|
|
286
299
|
A complete example can be found in
|
|
287
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
300
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/streaming_handler_parallel_events.py)
|
|
288
301
|
|
|
289
302
|
### Chain of Responsibility Request Handler
|
|
290
303
|
|
|
@@ -347,7 +360,7 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
|
347
360
|
```
|
|
348
361
|
|
|
349
362
|
A complete example can be found in
|
|
350
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_request_handler.py)
|
|
363
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_request_handler.py)
|
|
351
364
|
|
|
352
365
|
#### Mermaid Diagram Generation
|
|
353
366
|
|
|
@@ -367,7 +380,7 @@ sequence_diagram = generator.sequence()
|
|
|
367
380
|
class_diagram = generator.class_diagram()
|
|
368
381
|
```
|
|
369
382
|
|
|
370
|
-
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_mermaid.py)
|
|
383
|
+
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor/cor_mermaid.py)
|
|
371
384
|
|
|
372
385
|
## Mapping
|
|
373
386
|
|
|
@@ -428,7 +441,11 @@ def saga_mapper(mapper: cqrs.SagaMap) -> None:
|
|
|
428
441
|
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
429
442
|
dependency management simpler.
|
|
430
443
|
|
|
431
|
-
The package supports
|
|
444
|
+
The package supports three DI container libraries:
|
|
445
|
+
|
|
446
|
+
- [`di`](#di-library) (default)
|
|
447
|
+
- [`dependency-injector`](#dependency-injector-library)
|
|
448
|
+
- [dishka](https://github.com/reagento/dishka) via `DishkaCQRSContainer` (`pip install python-cqrs[dishka]`)
|
|
432
449
|
|
|
433
450
|
### di library
|
|
434
451
|
|
|
@@ -457,7 +474,7 @@ def setup_di() -> di.Container:
|
|
|
457
474
|
```
|
|
458
475
|
|
|
459
476
|
A complete example can be found in
|
|
460
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/
|
|
477
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/di/di_basic.py)
|
|
461
478
|
|
|
462
479
|
### dependency-injector library
|
|
463
480
|
|
|
@@ -473,7 +490,8 @@ class ApplicationContainer(containers.DeclarativeContainer):
|
|
|
473
490
|
service = providers.Factory(ServiceImplementation)
|
|
474
491
|
|
|
475
492
|
# Create CQRS container adapter
|
|
476
|
-
cqrs_container = DependencyInjectorCQRSContainer(
|
|
493
|
+
cqrs_container = DependencyInjectorCQRSContainer()
|
|
494
|
+
cqrs_container.attach_external_container(ApplicationContainer())
|
|
477
495
|
|
|
478
496
|
# Use with bootstrap
|
|
479
497
|
mediator = bootstrap.bootstrap(
|
|
@@ -484,8 +502,32 @@ mediator = bootstrap.bootstrap(
|
|
|
484
502
|
```
|
|
485
503
|
|
|
486
504
|
Complete examples can be found in:
|
|
487
|
-
- [Simple example](https://github.com/vadikko2/python-cqrs/blob/master/examples/
|
|
488
|
-
- [Practical example with FastAPI](https://github.com/vadikko2/python-cqrs/blob/master/examples/
|
|
505
|
+
- [Simple example](https://github.com/vadikko2/python-cqrs/blob/master/examples/di/dependency_injector_simple.py)
|
|
506
|
+
- [Practical example with FastAPI](https://github.com/vadikko2/python-cqrs/blob/master/examples/di/dependency_injector_practical.py)
|
|
507
|
+
|
|
508
|
+
### Scoped dependencies
|
|
509
|
+
|
|
510
|
+
By default, the container opens and closes a scope **inside** `resolve()`. A generator such as `async def session() -> AsyncIterator[AsyncSession]` therefore finishes **before** `handle`, which is why people inject factories.
|
|
511
|
+
|
|
512
|
+
Pass `scope_strategy=ScopeStrategy.SEND` and that session lives until `mediator.send()` returns — including domain-event handlers that run afterwards.
|
|
513
|
+
|
|
514
|
+
If you do not use generator providers, leave the default `ScopeStrategy.NONE` and change nothing.
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
mediator = bootstrap.bootstrap(
|
|
518
|
+
di_container=container,
|
|
519
|
+
commands_mapper=commands_mapper,
|
|
520
|
+
scope_strategy=cqrs.ScopeStrategy.SEND,
|
|
521
|
+
)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Copy-paste SQLAlchemy session + outbox under one SEND scope: the [Tutorial](https://mkdocs.python-cqrs.dev/scoped_dependencies/tutorial/) (`examples/di/scoped_dependencies_sqlalchemy.py`). Minimal `di` UoW: `examples/di/scoped_dependencies_di.py`.
|
|
525
|
+
|
|
526
|
+
- **SEND** — one UoW per `send()` (command, fallback, and domain events).
|
|
527
|
+
- **HANDLER** — a fresh UoW per handler, including fallback (the primary rolls back first).
|
|
528
|
+
- **NONE** — legacy one-shot resolve; the default.
|
|
529
|
+
|
|
530
|
+
`di`'s `scope="request"` is provider lifetime, not a CQRS scope — that is only `scope_strategy=`. Docs: [Scoped Dependencies](https://mkdocs.python-cqrs.dev/scoped_dependencies/).
|
|
489
531
|
|
|
490
532
|
## Bootstrap
|
|
491
533
|
|
|
@@ -650,6 +692,7 @@ await recover_saga(
|
|
|
650
692
|
context_builder=OrderContext,
|
|
651
693
|
container=di_container, # Same container used in bootstrap
|
|
652
694
|
storage=storage,
|
|
695
|
+
scope_strategy=cqrs.ScopeStrategy.SEND, # Match the original run
|
|
653
696
|
)
|
|
654
697
|
|
|
655
698
|
# Access execution history (SagaLog) for monitoring and debugging
|
|
@@ -682,7 +725,7 @@ sequence_diagram = generator.sequence()
|
|
|
682
725
|
class_diagram = generator.class_diagram()
|
|
683
726
|
```
|
|
684
727
|
|
|
685
|
-
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/saga_mermaid.py)
|
|
728
|
+
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/saga/saga_mermaid.py)
|
|
686
729
|
|
|
687
730
|
## Producing Notification Events
|
|
688
731
|
|
|
@@ -722,7 +765,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
722
765
|
```
|
|
723
766
|
|
|
724
767
|
A complete example can be found in
|
|
725
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/event_producing.py)
|
|
768
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/event_producing.py)
|
|
726
769
|
|
|
727
770
|
After processing the command/request, if there are any Notification/ECST events,
|
|
728
771
|
the EventEmitter is invoked to produce the events via the message broker.
|
|
@@ -793,16 +836,29 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
793
836
|
```
|
|
794
837
|
|
|
795
838
|
A complete example can be found in
|
|
796
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
839
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/save_events_into_outbox.py)
|
|
797
840
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
> By default, it is set to `outbox`.
|
|
841
|
+
A runnable FastAPI flow (route → command → transactional outbox → publisher stub) is in
|
|
842
|
+
[fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/fastapi_outbox.py)
|
|
801
843
|
|
|
802
844
|
> [!TIP]
|
|
803
|
-
>
|
|
804
|
-
>
|
|
805
|
-
the
|
|
845
|
+
> The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
|
|
846
|
+
> base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
|
|
847
|
+
> `create_all()`, before generating migrations and before the first query.
|
|
848
|
+
> [!TIP]
|
|
849
|
+
> `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
|
|
850
|
+
> rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
|
|
851
|
+
> `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
|
|
852
|
+
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
853
|
+
> how to register a native type for your own database.
|
|
854
|
+
> [!TIP]
|
|
855
|
+
> For Protobuf events, implement `proto()` / `from_proto()` on the notification
|
|
856
|
+
> event (see [Protobuf messaging](#protobuf-messaging) and the schema assets in
|
|
857
|
+
> [examples/proto/](https://github.com/vadikko2/python-cqrs/tree/master/examples/proto)).
|
|
858
|
+
> Outbox persistence is unchanged — use
|
|
859
|
+
> [save_events_into_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/save_events_into_outbox.py);
|
|
860
|
+
> Protobuf applies when producing to the broker. Full guide:
|
|
861
|
+
> [Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
806
862
|
|
|
807
863
|
## Producing Events from Outbox to Kafka
|
|
808
864
|
|
|
@@ -841,7 +897,7 @@ async with session_factory() as session:
|
|
|
841
897
|
```
|
|
842
898
|
|
|
843
899
|
A complete example can be found in
|
|
844
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
900
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/kafka_outboxed_event_producing.py)
|
|
845
901
|
|
|
846
902
|
## Transaction log tailing
|
|
847
903
|
|
|
@@ -884,7 +940,7 @@ class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
|
884
940
|
```
|
|
885
941
|
|
|
886
942
|
A complete example can be found in
|
|
887
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/domain_event_handler.py)
|
|
943
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/domain_event_handler.py)
|
|
888
944
|
|
|
889
945
|
### Parallel Event Processing
|
|
890
946
|
|
|
@@ -915,6 +971,7 @@ mediator = bootstrap.bootstrap_streaming(
|
|
|
915
971
|
> - Set `max_concurrent_event_handlers` to limit the number of simultaneously running event handlers
|
|
916
972
|
> - Set `concurrent_event_handle_enable=False` to disable parallel processing and process events sequentially
|
|
917
973
|
> - The default value for `max_concurrent_event_handlers` is `10` for `StreamingRequestMediator` and `1` for `RequestMediator`
|
|
974
|
+
> - `ScopeStrategy.SEND` always uses sequential events (`concurrent=None` → `False`). Passing `concurrent_event_handle_enable=True` with `SEND` raises `ValueError`; use `HANDLER` or `NONE` for parallel events.
|
|
918
975
|
|
|
919
976
|
## Integration with presentation layers
|
|
920
977
|
|
|
@@ -952,7 +1009,10 @@ async def join_metting(
|
|
|
952
1009
|
```
|
|
953
1010
|
|
|
954
1011
|
A complete example can be found in
|
|
955
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
|
|
1012
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/fastapi_integration.py)
|
|
1013
|
+
|
|
1014
|
+
End-to-end command + outbox sample (business row and outbox event in one transaction, plus a publisher stub):
|
|
1015
|
+
[fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/fastapi_outbox.py)
|
|
956
1016
|
|
|
957
1017
|
### Kafka events consuming
|
|
958
1018
|
|
|
@@ -1000,7 +1060,7 @@ async def hello_world_event_handler(
|
|
|
1000
1060
|
```
|
|
1001
1061
|
|
|
1002
1062
|
A complete example can be found in
|
|
1003
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
|
|
1063
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/events/kafka_event_consuming.py)
|
|
1004
1064
|
|
|
1005
1065
|
### FastAPI SSE Streaming
|
|
1006
1066
|
|
|
@@ -1021,7 +1081,7 @@ def streaming_mediator_factory() -> cqrs.StreamingRequestMediator:
|
|
|
1021
1081
|
domain_events_mapper=domain_events_mapper,
|
|
1022
1082
|
message_broker=broker,
|
|
1023
1083
|
max_concurrent_event_handlers=3,
|
|
1024
|
-
concurrent_event_handle_enable=True,
|
|
1084
|
+
concurrent_event_handle_enable=True, # HANDLER/NONE only; SEND forbids this
|
|
1025
1085
|
)
|
|
1026
1086
|
|
|
1027
1087
|
@app.post("/process-files")
|
|
@@ -1048,7 +1108,9 @@ async def process_files_stream(
|
|
|
1048
1108
|
```
|
|
1049
1109
|
|
|
1050
1110
|
A complete example can be found in
|
|
1051
|
-
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_sse_streaming.py)
|
|
1111
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming/fastapi_sse_streaming.py)
|
|
1112
|
+
|
|
1113
|
+
If the client disconnects, close the iterator (`aclose()` / `async with aclosing(...)`) so a `SEND` UoW is not held until GC.
|
|
1052
1114
|
|
|
1053
1115
|
## Protobuf messaging
|
|
1054
1116
|
|
|
@@ -31,7 +31,7 @@ maintainers = [{ name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru" }]
|
|
|
31
31
|
name = "python-cqrs"
|
|
32
32
|
readme = "README.md"
|
|
33
33
|
requires-python = ">=3.10"
|
|
34
|
-
version = "4.
|
|
34
|
+
version = "4.14.0"
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
37
|
aiobreaker = ["aiobreaker>=0.3.0"]
|
|
@@ -52,24 +52,29 @@ dev = [
|
|
|
52
52
|
"pytest~=7.4.2",
|
|
53
53
|
"pytest-asyncio~=0.21.1",
|
|
54
54
|
"pytest-env==0.6.2",
|
|
55
|
-
"
|
|
55
|
+
"alembic>=1.13.0", # from outbox migrations tests
|
|
56
|
+
"cryptography==50.0.0",
|
|
56
57
|
"asyncmy==0.2.9",
|
|
57
58
|
"asyncpg>=0.29.0",
|
|
58
59
|
"redis>=5.0.0",
|
|
59
60
|
# Circuit breaker for tests
|
|
60
61
|
"aiobreaker>=0.3.0", # from aiobreaker
|
|
62
|
+
"dishka>=1.0", # scoped-deps dishka adapter tests / examples
|
|
61
63
|
]
|
|
62
64
|
examples = [
|
|
63
65
|
"fastapi==0.109.*",
|
|
64
66
|
"faststream[kafka]==0.5.28",
|
|
65
67
|
"faker>=37.12.0",
|
|
66
68
|
"uvicorn==0.32.0",
|
|
67
|
-
"aiohttp==3.14.
|
|
69
|
+
"aiohttp==3.14.3",
|
|
68
70
|
"protobuf>=4.25.8",
|
|
71
|
+
"aiosqlite>=0.20.0",
|
|
72
|
+
"dishka>=1.0",
|
|
69
73
|
]
|
|
70
74
|
kafka = ["aiokafka==0.10.0", "retry-async==0.1.*"]
|
|
71
75
|
rabbit = ["aio-pika==9.3.0"]
|
|
72
76
|
sqlalchemy = ["sqlalchemy[asyncio]==2.0.*"]
|
|
77
|
+
dishka = ["dishka>=1.0"]
|
|
73
78
|
|
|
74
79
|
[project.urls]
|
|
75
80
|
Documentation = "https://mkdocs.python-cqrs.dev/"
|