python-cqrs 5.3.1__tar.gz → 5.4.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.4.0/PKG-INFO +399 -0
- python_cqrs-5.4.0/README.md +313 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/pyproject.toml +11 -9
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/__init__.py +5 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/event_emitter.py +25 -0
- python_cqrs-5.4.0/src/cqrs/outbox/memory.py +96 -0
- python_cqrs-5.4.0/src/cqrs/testing/__init__.py +23 -0
- python_cqrs-5.4.0/src/cqrs/testing/broker.py +44 -0
- python_cqrs-5.4.0/src/cqrs/testing/container.py +200 -0
- python_cqrs-5.4.0/src/cqrs/testing/harness.py +560 -0
- python_cqrs-5.4.0/src/cqrs/testing/recording.py +875 -0
- python_cqrs-5.4.0/src/python_cqrs.egg-info/PKG-INFO +399 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/SOURCES.txt +6 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/requires.txt +10 -7
- python_cqrs-5.3.1/PKG-INFO +0 -1314
- python_cqrs-5.3.1/README.md +0 -1230
- python_cqrs-5.3.1/src/python_cqrs.egg-info/PKG-INFO +0 -1314
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/LICENSE +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/setup.cfg +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/_dataclass_utils.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/events.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/requests.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/saga.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/circuit_breaker.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/dishka.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/scope.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/protobuf.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/fallback.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/generic_utils.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/cor.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/event.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/request.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/saga.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/map.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/memory.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/processor.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/repository.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/sqlalchemy.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/sqlmodel.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/events.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/outbox.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/requests.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/_common.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/event.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/request.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/saga.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/streaming.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mermaid/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mermaid/cor.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mermaid/saga.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/producer.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/event.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/pydantic.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/request.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/response.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/sqlalchemy.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/sqlmodel.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/py.typed +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/requests/fallback.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/sqlalchemy.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/sqlmodel.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/json.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/protobuf.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/protocol.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/sqlmodel_base.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/types.py +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-cqrs
|
|
3
|
+
Version: 5.4.0
|
|
4
|
+
Summary: Event-Driven Architecture Framework for Distributed Systems
|
|
5
|
+
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
|
|
6
|
+
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
7
|
+
Project-URL: Documentation, https://mkdocs.python-cqrs.dev/
|
|
8
|
+
Project-URL: Issues, https://github.com/vadikko2/python-cqrs/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/vadikko2/python-cqrs
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: dataclass-wizard==0.*
|
|
22
|
+
Requires-Dist: di[anyio]==0.*
|
|
23
|
+
Requires-Dist: dependency-injector>=4.0
|
|
24
|
+
Requires-Dist: orjson==3.*
|
|
25
|
+
Requires-Dist: python-dotenv==1.*
|
|
26
|
+
Requires-Dist: typing-extensions>=4.7
|
|
27
|
+
Provides-Extra: aiobreaker
|
|
28
|
+
Requires-Dist: aiobreaker>=0.3.0; extra == "aiobreaker"
|
|
29
|
+
Provides-Extra: pydantic
|
|
30
|
+
Requires-Dist: pydantic==2.*; extra == "pydantic"
|
|
31
|
+
Provides-Extra: sqlalchemy
|
|
32
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "sqlalchemy"
|
|
33
|
+
Provides-Extra: sqlmodel
|
|
34
|
+
Requires-Dist: sqlmodel>=0.0.22; extra == "sqlmodel"
|
|
35
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "sqlmodel"
|
|
36
|
+
Provides-Extra: testing
|
|
37
|
+
Requires-Dist: pytest~=7.4.2; extra == "testing"
|
|
38
|
+
Requires-Dist: pytest-asyncio~=0.21.1; extra == "testing"
|
|
39
|
+
Requires-Dist: pydantic==2.*; extra == "testing"
|
|
40
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "testing"
|
|
41
|
+
Requires-Dist: sqlmodel>=0.0.22; extra == "testing"
|
|
42
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == "testing"
|
|
43
|
+
Requires-Dist: protobuf>=4.25.8; extra == "testing"
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pycln==2.5.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pre-commit==3.8.0; extra == "dev"
|
|
47
|
+
Requires-Dist: pyright==1.1.408; extra == "dev"
|
|
48
|
+
Requires-Dist: ruff==0.6.2; extra == "dev"
|
|
49
|
+
Requires-Dist: vermin>=1.6.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
51
|
+
Requires-Dist: pytest-codspeed==4.2.0; extra == "dev"
|
|
52
|
+
Requires-Dist: python-cqrs[testing]; extra == "dev"
|
|
53
|
+
Requires-Dist: aio-pika==9.3.0; extra == "dev"
|
|
54
|
+
Requires-Dist: aiokafka==0.10.0; extra == "dev"
|
|
55
|
+
Requires-Dist: retry-async==0.1.*; extra == "dev"
|
|
56
|
+
Requires-Dist: requests==2.*; extra == "dev"
|
|
57
|
+
Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
58
|
+
Requires-Dist: alembic>=1.13.0; extra == "dev"
|
|
59
|
+
Requires-Dist: cryptography==50.0.0; extra == "dev"
|
|
60
|
+
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
61
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "dev"
|
|
62
|
+
Requires-Dist: redis>=5.0.0; extra == "dev"
|
|
63
|
+
Requires-Dist: aiobreaker>=0.3.0; extra == "dev"
|
|
64
|
+
Requires-Dist: dishka>=1.0; extra == "dev"
|
|
65
|
+
Provides-Extra: examples
|
|
66
|
+
Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
67
|
+
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
68
|
+
Requires-Dist: faker>=37.12.0; extra == "examples"
|
|
69
|
+
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
70
|
+
Requires-Dist: aiohttp==3.14.4; extra == "examples"
|
|
71
|
+
Requires-Dist: protobuf>=4.25.8; extra == "examples"
|
|
72
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
|
|
73
|
+
Requires-Dist: dishka>=1.0; extra == "examples"
|
|
74
|
+
Requires-Dist: pydantic==2.*; extra == "examples"
|
|
75
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "examples"
|
|
76
|
+
Provides-Extra: kafka
|
|
77
|
+
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
78
|
+
Requires-Dist: retry-async==0.1.*; extra == "kafka"
|
|
79
|
+
Provides-Extra: protobuf
|
|
80
|
+
Requires-Dist: protobuf>=4.25.8; extra == "protobuf"
|
|
81
|
+
Provides-Extra: rabbit
|
|
82
|
+
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
83
|
+
Provides-Extra: dishka
|
|
84
|
+
Requires-Dist: dishka>=1.0; extra == "dishka"
|
|
85
|
+
Dynamic: license-file
|
|
86
|
+
|
|
87
|
+
<div align="center">
|
|
88
|
+
<h1>Python CQRS</h1>
|
|
89
|
+
<p><strong>CQRS and event-driven architecture for Python services, from a single API to distributed systems.</strong></p>
|
|
90
|
+
<p>
|
|
91
|
+
<a href="https://pypi.org/project/python-cqrs/">
|
|
92
|
+
<img src="https://img.shields.io/pypi/pyversions/python-cqrs?logo=python&logoColor=white" alt="Python Versions">
|
|
93
|
+
</a>
|
|
94
|
+
<a href="https://pypi.org/project/python-cqrs/">
|
|
95
|
+
<img src="https://img.shields.io/pypi/v/python-cqrs?label=pypi&logo=pypi" alt="PyPI version">
|
|
96
|
+
</a>
|
|
97
|
+
<a href="https://pepy.tech/projects/python-cqrs">
|
|
98
|
+
<img src="https://pepy.tech/badge/python-cqrs" alt="Total downloads">
|
|
99
|
+
</a>
|
|
100
|
+
<a href="https://pepy.tech/projects/python-cqrs">
|
|
101
|
+
<img src="https://pepy.tech/badge/python-cqrs/month" alt="Downloads per month">
|
|
102
|
+
</a>
|
|
103
|
+
<a href="https://codecov.io/gh/vadikko2/python-cqrs">
|
|
104
|
+
<img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
|
|
105
|
+
</a>
|
|
106
|
+
<a href="https://github.com/mjhea0/awesome-fastapi#developer-tools">
|
|
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">
|
|
108
|
+
</a>
|
|
109
|
+
</p>
|
|
110
|
+
<p>
|
|
111
|
+
<a href="https://mkdocs.python-cqrs.dev/latest/"><strong>Documentation</strong></a> ·
|
|
112
|
+
<a href="https://github.com/pypatterns/python-cqrs/tree/master/examples">Examples</a> ·
|
|
113
|
+
<a href="https://github.com/pypatterns/python-cqrs/releases">Changelog</a>
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
As a service grows, business logic spreads across routes, consumers, and workers. python-cqrs puts each use case in one handler behind a mediator. Routes and consumers only send requests, and side effects become events with their own handlers.
|
|
118
|
+
|
|
119
|
+
Once services talk to each other, a database write and a broker message are no longer atomic. The outbox and inbox keep messages from being lost or handled twice, and a saga undoes completed steps when a later one fails.
|
|
120
|
+
|
|
121
|
+
Dataclasses or Pydantic · SQLAlchemy or SQLModel · Kafka or RabbitMQ · FastAPI and FastStream · Python 3.10+
|
|
122
|
+
|
|
123
|
+
## Installation
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install python-cqrs
|
|
127
|
+
pip install "python-cqrs[pydantic,sqlalchemy,kafka]" # add what you use
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Extras: `pydantic`, `sqlalchemy`, `sqlmodel`, `kafka`, `rabbit`, `protobuf`, `aiobreaker`, `dishka`, `testing`. Details: [installation](https://mkdocs.python-cqrs.dev/latest/installation/). From 4.x: [migration guide](https://mkdocs.python-cqrs.dev/latest/migration/4-to-5/).
|
|
131
|
+
|
|
132
|
+
## Quick start
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import di
|
|
136
|
+
import cqrs
|
|
137
|
+
from cqrs.bootstrap import requests as bootstrap
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class OrderCreated(cqrs.Response):
|
|
141
|
+
order_id: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CreateOrder(cqrs.Request[OrderCreated]):
|
|
145
|
+
sku: str
|
|
146
|
+
amount: float
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrder]):
|
|
150
|
+
async def handle(self, request: CreateOrder):
|
|
151
|
+
return OrderCreated(order_id=f"ord-{request.sku}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
155
|
+
mapper.bind(CreateOrder, CreateOrderHandler)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
mediator = bootstrap.bootstrap(di_container=di.Container(), commands_mapper=commands_mapper)
|
|
159
|
+
result = await mediator.send(CreateOrder(sku="book", amount=99.99)) # OrderCreated(order_id="ord-book")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`result` is typed as `OrderCreated`. Queries use the same shape. Handler dependencies come from `di`, dependency-injector, or dishka. [Request handlers](https://mkdocs.python-cqrs.dev/latest/request_handler/) · [Bootstrap](https://mkdocs.python-cqrs.dev/latest/bootstrap/) · [Dependency injection](https://mkdocs.python-cqrs.dev/latest/di/)
|
|
163
|
+
|
|
164
|
+
## Saga
|
|
165
|
+
|
|
166
|
+
Paying for an order touches the warehouse service and the payment service, and no transaction spans both. If the charge fails, the stock stays reserved. A saga runs the steps in order and, on failure, calls `compensate` on the completed ones in reverse.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import dataclasses
|
|
170
|
+
from cqrs.bootstrap import saga as saga_bootstrap
|
|
171
|
+
from cqrs.saga.models import SagaContext
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclasses.dataclass
|
|
175
|
+
class OrderContext(SagaContext):
|
|
176
|
+
order_id: str
|
|
177
|
+
amount: float
|
|
178
|
+
reservation_id: str | None = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ReserveStock(cqrs.SagaStepHandler[OrderContext, None]):
|
|
182
|
+
def __init__(self, inventory: InventoryClient) -> None:
|
|
183
|
+
self._inventory = inventory
|
|
184
|
+
|
|
185
|
+
async def act(self, context: OrderContext):
|
|
186
|
+
context.reservation_id = await self._inventory.reserve(context.order_id)
|
|
187
|
+
return self._generate_step_result(None)
|
|
188
|
+
|
|
189
|
+
async def compensate(self, context: OrderContext) -> None:
|
|
190
|
+
await self._inventory.release(context.reservation_id)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ChargeCard(cqrs.SagaStepHandler[OrderContext, None]):
|
|
194
|
+
def __init__(self, payments: PaymentClient) -> None:
|
|
195
|
+
self._payments = payments
|
|
196
|
+
|
|
197
|
+
async def act(self, context: OrderContext):
|
|
198
|
+
await self._payments.charge(context.order_id, context.amount)
|
|
199
|
+
return self._generate_step_result(None)
|
|
200
|
+
|
|
201
|
+
async def compensate(self, context: OrderContext) -> None:
|
|
202
|
+
await self._payments.refund(context.order_id)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class OrderSaga(cqrs.Saga[OrderContext]):
|
|
206
|
+
steps = [ReserveStock, ChargeCard]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def sagas_mapper(mapper: cqrs.SagaMap) -> None:
|
|
210
|
+
mapper.bind(OrderContext, OrderSaga)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
mediator = saga_bootstrap.bootstrap(di_container=di.Container(), sagas_mapper=sagas_mapper)
|
|
214
|
+
|
|
215
|
+
# if ChargeCard raises, ReserveStock.compensate releases the stock
|
|
216
|
+
async for step in mediator.execute(OrderContext(order_id="ord-1", amount=99.99)):
|
|
217
|
+
print(step.step_type.__name__)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Storage, recovery after a crash, and fallback steps: [Saga](https://mkdocs.python-cqrs.dev/latest/saga/).
|
|
221
|
+
|
|
222
|
+
## Outbox and inbox
|
|
223
|
+
|
|
224
|
+
### Outbox
|
|
225
|
+
|
|
226
|
+
Commit the order, then publish to Kafka: a crash between the two loses the event. The outbox writes the event in the same transaction as the order. A publisher sends it later, at least once.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
class OrderPlaced(cqrs.NotificationEvent):
|
|
230
|
+
event_name: str = "order_placed"
|
|
231
|
+
topic: str = "orders"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrder]):
|
|
235
|
+
def __init__(self, outbox: cqrs.OutboxedEventRepository) -> None:
|
|
236
|
+
self._outbox = outbox
|
|
237
|
+
|
|
238
|
+
async def handle(self, request: CreateOrder):
|
|
239
|
+
order_id = f"ord-{request.sku}"
|
|
240
|
+
# write the order row through the same session here
|
|
241
|
+
self._outbox.add(OrderPlaced(payload={"order_id": order_id}))
|
|
242
|
+
await self._outbox.commit()
|
|
243
|
+
return OrderCreated(order_id=order_id)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
[Outbox](https://mkdocs.python-cqrs.dev/latest/outbox/) · [Database support](https://mkdocs.python-cqrs.dev/latest/outbox/databases/) · [`examples/outbox/`](https://github.com/pypatterns/python-cqrs/tree/master/examples/outbox)
|
|
247
|
+
|
|
248
|
+
### Inbox
|
|
249
|
+
|
|
250
|
+
The broker redelivers, and a consumer can crash halfway through a handler. The FastStream subscriber only stores the message. A repeated `payment_id` is skipped.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
class ConfirmPayment(cqrs.Request[None]):
|
|
254
|
+
order_id: str
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@broker.subscriber("payments")
|
|
258
|
+
async def on_payment(
|
|
259
|
+
body: dict,
|
|
260
|
+
inbox: cqrs.InboxRepository = Depends(inbox_repository), # commits after return, then ack
|
|
261
|
+
) -> None:
|
|
262
|
+
await inbox.add([(body["payment_id"], ConfirmPayment(order_id=body["order_id"]))])
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A worker runs stored rows through the mediator. One commit saves the handler's changes and the `processed` status.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
processor = cqrs.InboxProcessor(inbox, message_map, request_mediator=mediator)
|
|
269
|
+
for row in await inbox.get_many():
|
|
270
|
+
await processor.process(row)
|
|
271
|
+
await session.commit()
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
[Inbox](https://mkdocs.python-cqrs.dev/latest/inbox/) · [`examples/inbox/`](https://github.com/pypatterns/python-cqrs/tree/master/examples/inbox)
|
|
275
|
+
|
|
276
|
+
## Domain events
|
|
277
|
+
|
|
278
|
+
A placed order also updates a read model and sends an email. In the command handler, that couples unrelated work. Instead, the handler emits an event, and each reaction is its own handler.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
class OrderPlaced(cqrs.DomainEvent):
|
|
282
|
+
order_id: str
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrder]):
|
|
286
|
+
def __init__(self) -> None:
|
|
287
|
+
self._events: list[cqrs.Event] = []
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def events(self) -> list[cqrs.Event]:
|
|
291
|
+
return self._events
|
|
292
|
+
|
|
293
|
+
async def handle(self, request: CreateOrder):
|
|
294
|
+
order_id = f"ord-{request.sku}"
|
|
295
|
+
self._events.append(OrderPlaced(order_id=order_id))
|
|
296
|
+
return OrderCreated(order_id=order_id)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class UpdateOrderList(cqrs.EventHandler[OrderPlaced]):
|
|
300
|
+
async def handle(self, event: OrderPlaced) -> None: ...
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class SendConfirmation(cqrs.EventHandler[OrderPlaced]):
|
|
304
|
+
async def handle(self, event: OrderPlaced) -> None: ...
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def domain_events_mapper(mapper: cqrs.EventMap) -> None:
|
|
308
|
+
mapper.bind(OrderPlaced, UpdateOrderList)
|
|
309
|
+
mapper.bind(OrderPlaced, SendConfirmation)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Pass `domain_events_mapper` to `bootstrap`. Event handlers run after the command, in parallel when enabled. [Events](https://mkdocs.python-cqrs.dev/latest/event_handler/) · [Parallel processing](https://mkdocs.python-cqrs.dev/latest/event_handler/parallel_processing/) · [Scoped dependencies](https://mkdocs.python-cqrs.dev/latest/scoped_dependencies/)
|
|
313
|
+
|
|
314
|
+
## FastAPI
|
|
315
|
+
|
|
316
|
+
The route parses HTTP and sends the request. The handler does not import FastAPI.
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
@app.post("/orders")
|
|
320
|
+
async def create_order(
|
|
321
|
+
command: CreateOrder,
|
|
322
|
+
mediator: cqrs.RequestMediator = Depends(mediator_factory),
|
|
323
|
+
) -> dict[str, str]:
|
|
324
|
+
result = await mediator.send(command)
|
|
325
|
+
return {"order_id": result.order_id}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
FastStream consumers and SSE streaming work the same way. [FastAPI](https://mkdocs.python-cqrs.dev/latest/fastapi/) · [FastStream](https://mkdocs.python-cqrs.dev/latest/faststream/) · [SSE streaming](https://mkdocs.python-cqrs.dev/latest/stream_handling/fastapi_integration/)
|
|
329
|
+
|
|
330
|
+
## Testing
|
|
331
|
+
|
|
332
|
+
Mocks show that a method was called, not what the service did. `test_harness` runs the real mediators and records what was handled, published, and stored.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from cqrs.testing import test_harness
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def test_create_order() -> None:
|
|
339
|
+
async with test_harness(di.Container(), commands_mapper=commands_mapper) as harness:
|
|
340
|
+
result = await harness.mediator.send(CreateOrder(sku="book", amount=99.99))
|
|
341
|
+
|
|
342
|
+
assert result.order_id == "ord-book"
|
|
343
|
+
assert harness.requests.contains(CreateOrder)
|
|
344
|
+
harness.invocations.assert_entered(CreateOrderHandler)
|
|
345
|
+
harness.published.assert_empty()
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The same journals cover domain events, the outbox, and saga compensation. [Testing](https://mkdocs.python-cqrs.dev/latest/testing/) · [`examples/testing/`](https://github.com/pypatterns/python-cqrs/tree/master/examples/testing)
|
|
349
|
+
|
|
350
|
+
## When NOT to use python-cqrs
|
|
351
|
+
|
|
352
|
+
CQRS, Outbox, and Saga are tools for specific problems — not a default for every service.
|
|
353
|
+
|
|
354
|
+
- **Prefer a local DB transaction** for single-service CRUD (one database, no broker, no compensation).
|
|
355
|
+
- **Use [Transactional Outbox](#outbox-and-inbox)** 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.
|
|
356
|
+
- **Use [Saga](#saga)** when a multi-step flow crosses service/database boundaries and a later step must be compensated if it fails.
|
|
357
|
+
|
|
358
|
+
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).
|
|
359
|
+
|
|
360
|
+
## Documentation
|
|
361
|
+
|
|
362
|
+
Full guides: **[mkdocs.python-cqrs.dev](https://mkdocs.python-cqrs.dev/latest/)**. Also there:
|
|
363
|
+
|
|
364
|
+
- [Request and response types](https://mkdocs.python-cqrs.dev/latest/request_response_types/): dataclasses, Pydantic, msgspec, attrs, and others
|
|
365
|
+
- [Streaming](https://mkdocs.python-cqrs.dev/latest/stream_handling/): handlers that yield results as they finish
|
|
366
|
+
- [Chain of responsibility](https://mkdocs.python-cqrs.dev/latest/chain_of_responsibility/): several handlers for one request, the first match wins
|
|
367
|
+
- [Saga storage and recovery](https://mkdocs.python-cqrs.dev/latest/saga/recovery/): resume a saga after a crash
|
|
368
|
+
- [Fallback and circuit breaker](https://mkdocs.python-cqrs.dev/latest/saga/fallback/): a spare step or handler when the primary fails
|
|
369
|
+
- [Middlewares](https://mkdocs.python-cqrs.dev/latest/bootstrap/middlewares/) and [message brokers](https://mkdocs.python-cqrs.dev/latest/bootstrap/message_brokers/)
|
|
370
|
+
- [Protobuf](https://mkdocs.python-cqrs.dev/latest/protobuf/) and [Mermaid diagrams](https://mkdocs.python-cqrs.dev/latest/mermaid/)
|
|
371
|
+
|
|
372
|
+
## Agent skills
|
|
373
|
+
|
|
374
|
+
Skills tell a coding agent which feature fits the task. Install from your service's root:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
npx skills add pypatterns/python-cqrs --skill cqrs-setup -y
|
|
378
|
+
npx skills add pypatterns/python-cqrs --skill cqrs-eda -y
|
|
379
|
+
npx skills add pypatterns/python-cqrs --skill cqrs-saga -y
|
|
380
|
+
npx skills add pypatterns/python-cqrs --skill cqrs-streaming -y
|
|
381
|
+
npx skills add pypatterns/python-cqrs --skill cqrs-cor -y
|
|
382
|
+
npx skills add pypatterns/python-cqrs --skill cqrs-testing -y
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Also copy [`templates/AGENTS.md`](templates/AGENTS.md) into the application. [Agent skills](https://mkdocs.python-cqrs.dev/latest/skills/) · [Rules for agents](https://mkdocs.python-cqrs.dev/latest/agents/)
|
|
386
|
+
|
|
387
|
+
## Contributing
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
pip install -e ".[dev]"
|
|
391
|
+
pytest -c ./tests/pytest-config.ini ./tests/unit
|
|
392
|
+
pre-commit install
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Details: [CONTRIBUTING.md](CONTRIBUTING.md). Vulnerabilities: [SECURITY.md](SECURITY.md).
|
|
396
|
+
|
|
397
|
+
## License
|
|
398
|
+
|
|
399
|
+
[MIT](LICENSE)
|