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.
Files changed (127) hide show
  1. python_cqrs-5.4.0/PKG-INFO +399 -0
  2. python_cqrs-5.4.0/README.md +313 -0
  3. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/pyproject.toml +11 -9
  4. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/__init__.py +5 -0
  5. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/event_emitter.py +25 -0
  6. python_cqrs-5.4.0/src/cqrs/outbox/memory.py +96 -0
  7. python_cqrs-5.4.0/src/cqrs/testing/__init__.py +23 -0
  8. python_cqrs-5.4.0/src/cqrs/testing/broker.py +44 -0
  9. python_cqrs-5.4.0/src/cqrs/testing/container.py +200 -0
  10. python_cqrs-5.4.0/src/cqrs/testing/harness.py +560 -0
  11. python_cqrs-5.4.0/src/cqrs/testing/recording.py +875 -0
  12. python_cqrs-5.4.0/src/python_cqrs.egg-info/PKG-INFO +399 -0
  13. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/SOURCES.txt +6 -0
  14. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/requires.txt +10 -7
  15. python_cqrs-5.3.1/PKG-INFO +0 -1314
  16. python_cqrs-5.3.1/README.md +0 -1230
  17. python_cqrs-5.3.1/src/python_cqrs.egg-info/PKG-INFO +0 -1314
  18. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/LICENSE +0 -0
  19. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/setup.cfg +0 -0
  20. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/_dataclass_utils.py +0 -0
  21. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/__init__.py +0 -0
  22. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/amqp.py +0 -0
  23. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
  24. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/kafka.py +0 -0
  25. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/adapters/protocol.py +0 -0
  26. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/__init__.py +0 -0
  27. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/events.py +0 -0
  28. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/requests.py +0 -0
  29. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/bootstrap/saga.py +0 -0
  30. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/circuit_breaker.py +0 -0
  31. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/compressors/__init__.py +0 -0
  32. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/compressors/protocol.py +0 -0
  33. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/compressors/zlib.py +0 -0
  34. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/__init__.py +0 -0
  35. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/dependency_injector.py +0 -0
  36. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/di.py +0 -0
  37. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/dishka.py +0 -0
  38. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/protocol.py +0 -0
  39. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/container/scope.py +0 -0
  40. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/__init__.py +0 -0
  41. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/exceptions.py +0 -0
  42. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/json.py +0 -0
  43. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/deserializers/protobuf.py +0 -0
  44. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/__init__.py +0 -0
  45. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/event.py +0 -0
  46. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/exceptions.py +0 -0
  47. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/models.py +0 -0
  48. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/request.py +0 -0
  49. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/saga.py +0 -0
  50. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/dispatcher/streaming.py +0 -0
  51. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/__init__.py +0 -0
  52. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/event_processor.py +0 -0
  53. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/events/fallback.py +0 -0
  54. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/generic_utils.py +0 -0
  55. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/__init__.py +0 -0
  56. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/cor.py +0 -0
  57. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/event.py +0 -0
  58. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/request.py +0 -0
  59. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/handlers/saga.py +0 -0
  60. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/__init__.py +0 -0
  61. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/map.py +0 -0
  62. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/memory.py +0 -0
  63. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/processor.py +0 -0
  64. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/repository.py +0 -0
  65. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/sqlalchemy.py +0 -0
  66. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/inbox/sqlmodel.py +0 -0
  67. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/__init__.py +0 -0
  68. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/events.py +0 -0
  69. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/outbox.py +0 -0
  70. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mapping/requests.py +0 -0
  71. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/__init__.py +0 -0
  72. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/_common.py +0 -0
  73. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/event.py +0 -0
  74. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/request.py +0 -0
  75. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/saga.py +0 -0
  76. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mediators/streaming.py +0 -0
  77. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mermaid/__init__.py +0 -0
  78. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mermaid/cor.py +0 -0
  79. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/mermaid/saga.py +0 -0
  80. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/__init__.py +0 -0
  81. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/amqp.py +0 -0
  82. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/devnull.py +0 -0
  83. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/kafka.py +0 -0
  84. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/producer.py +0 -0
  85. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/message_brokers/protocol.py +0 -0
  86. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/middlewares/__init__.py +0 -0
  87. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/middlewares/base.py +0 -0
  88. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/middlewares/logging.py +0 -0
  89. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/__init__.py +0 -0
  90. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/event.py +0 -0
  91. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/pydantic.py +0 -0
  92. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/request.py +0 -0
  93. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/models/response.py +0 -0
  94. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/__init__.py +0 -0
  95. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/mock.py +0 -0
  96. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/repository.py +0 -0
  97. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/sqlalchemy.py +0 -0
  98. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/outbox/sqlmodel.py +0 -0
  99. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/py.typed +0 -0
  100. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/requests/__init__.py +0 -0
  101. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/requests/fallback.py +0 -0
  102. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/__init__.py +0 -0
  103. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/circuit_breaker.py +0 -0
  104. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/compensation.py +0 -0
  105. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/execution.py +0 -0
  106. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/fallback.py +0 -0
  107. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/models.py +0 -0
  108. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/recovery.py +0 -0
  109. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/saga.py +0 -0
  110. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/__init__.py +0 -0
  111. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/enums.py +0 -0
  112. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/memory.py +0 -0
  113. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/models.py +0 -0
  114. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/protocol.py +0 -0
  115. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/sqlalchemy.py +0 -0
  116. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/storage/sqlmodel.py +0 -0
  117. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/saga/validation.py +0 -0
  118. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/__init__.py +0 -0
  119. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/default.py +0 -0
  120. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/json.py +0 -0
  121. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/protobuf.py +0 -0
  122. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/serializers/protocol.py +0 -0
  123. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/sqlalchemy_types.py +0 -0
  124. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/sqlmodel_base.py +0 -0
  125. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/cqrs/types.py +0 -0
  126. {python_cqrs-5.3.1 → python_cqrs-5.4.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  127. {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)