python-cqrs 4.11.0__tar.gz → 4.13.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 (89) hide show
  1. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/PKG-INFO +32 -6
  2. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/README.md +27 -3
  3. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/pyproject.toml +15 -13
  4. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/__init__.py +14 -0
  5. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/event_emitter.py +44 -14
  6. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/outbox/sqlalchemy.py +16 -41
  7. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/storage/sqlalchemy.py +3 -1
  8. python_cqrs-4.13.0/src/cqrs/sqlalchemy_types.py +237 -0
  9. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/python_cqrs.egg-info/PKG-INFO +32 -6
  10. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/python_cqrs.egg-info/SOURCES.txt +1 -0
  11. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/python_cqrs.egg-info/requires.txt +4 -2
  12. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/LICENSE +0 -0
  13. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/setup.cfg +0 -0
  14. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/adapters/__init__.py +0 -0
  15. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/adapters/amqp.py +0 -0
  16. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
  17. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/adapters/kafka.py +0 -0
  18. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/adapters/protocol.py +0 -0
  19. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/circuit_breaker.py +0 -0
  20. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/compressors/__init__.py +0 -0
  21. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/compressors/protocol.py +0 -0
  22. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/compressors/zlib.py +0 -0
  23. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/container/__init__.py +0 -0
  24. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/container/dependency_injector.py +0 -0
  25. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/container/di.py +0 -0
  26. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/container/protocol.py +0 -0
  27. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/deserializers/__init__.py +0 -0
  28. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/deserializers/exceptions.py +0 -0
  29. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/deserializers/json.py +0 -0
  30. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/__init__.py +0 -0
  31. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/event.py +0 -0
  32. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/exceptions.py +0 -0
  33. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/models.py +0 -0
  34. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/request.py +0 -0
  35. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/saga.py +0 -0
  36. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/dispatcher/streaming.py +0 -0
  37. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/__init__.py +0 -0
  38. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/bootstrap.py +0 -0
  39. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/event.py +0 -0
  40. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/event_handler.py +0 -0
  41. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/event_processor.py +0 -0
  42. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/fallback.py +0 -0
  43. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/events/map.py +0 -0
  44. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/generic_utils.py +0 -0
  45. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/mediator.py +0 -0
  46. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/message_brokers/__init__.py +0 -0
  47. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/message_brokers/amqp.py +0 -0
  48. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/message_brokers/devnull.py +0 -0
  49. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/message_brokers/kafka.py +0 -0
  50. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/message_brokers/protocol.py +0 -0
  51. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/middlewares/__init__.py +0 -0
  52. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/middlewares/base.py +0 -0
  53. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/middlewares/logging.py +0 -0
  54. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/outbox/__init__.py +0 -0
  55. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/outbox/map.py +0 -0
  56. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/outbox/mock.py +0 -0
  57. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/outbox/repository.py +0 -0
  58. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/producer.py +0 -0
  59. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/__init__.py +0 -0
  60. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/bootstrap.py +0 -0
  61. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/cor_request_handler.py +0 -0
  62. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/fallback.py +0 -0
  63. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/map.py +0 -0
  64. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/mermaid.py +0 -0
  65. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/request.py +0 -0
  66. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/requests/request_handler.py +0 -0
  67. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/response.py +0 -0
  68. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/__init__.py +0 -0
  69. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/bootstrap.py +0 -0
  70. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/circuit_breaker.py +0 -0
  71. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/compensation.py +0 -0
  72. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/execution.py +0 -0
  73. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/fallback.py +0 -0
  74. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/mermaid.py +0 -0
  75. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/models.py +0 -0
  76. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/recovery.py +0 -0
  77. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/saga.py +0 -0
  78. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/step.py +0 -0
  79. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/storage/__init__.py +0 -0
  80. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/storage/enums.py +0 -0
  81. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/storage/memory.py +0 -0
  82. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/storage/models.py +0 -0
  83. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/storage/protocol.py +0 -0
  84. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/saga/validation.py +0 -0
  85. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/serializers/__init__.py +0 -0
  86. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/serializers/default.py +0 -0
  87. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/cqrs/types.py +0 -0
  88. {python_cqrs-4.11.0 → python_cqrs-4.13.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  89. {python_cqrs-4.11.0 → python_cqrs-4.13.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.11.0
3
+ Version: 4.13.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,7 +42,8 @@ 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: cryptography==48.0.1; extra == "dev"
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"
@@ -52,8 +53,9 @@ Requires-Dist: fastapi==0.109.*; extra == "examples"
52
53
  Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
53
54
  Requires-Dist: faker>=37.12.0; extra == "examples"
54
55
  Requires-Dist: uvicorn==0.32.0; extra == "examples"
55
- Requires-Dist: aiohttp==3.14.1; extra == "examples"
56
+ Requires-Dist: aiohttp==3.14.3; extra == "examples"
56
57
  Requires-Dist: protobuf>=4.25.8; extra == "examples"
58
+ Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
57
59
  Provides-Extra: kafka
58
60
  Requires-Dist: aiokafka==0.10.0; extra == "kafka"
59
61
  Requires-Dist: retry-async==0.1.*; extra == "kafka"
@@ -108,6 +110,7 @@ Dynamic: license-file
108
110
  ## Table of Contents
109
111
 
110
112
  - [Overview](#overview)
113
+ - [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
111
114
  - [Installation](#installation)
112
115
  - [Quick Start](#quick-start)
113
116
  - [Request and Response Types](#request-and-response-types)
@@ -150,10 +153,21 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
150
153
 
151
154
  - **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
152
155
  - **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
156
+ - **Storage:** SQLAlchemy-based Outbox and Saga storage on MySQL/MariaDB and PostgreSQL.
153
157
  - **Integration:** Ready for integration with FastAPI and FastStream.
154
158
  - **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
155
159
  - **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
156
160
 
161
+ ## When NOT to use python-cqrs
162
+
163
+ CQRS, Outbox, and Saga are tools for specific problems — not a default for every service.
164
+
165
+ - **Prefer a local DB transaction** for single-service CRUD (one database, no broker, no compensation).
166
+ - **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.
167
+ - **Use [Saga](#saga-pattern)** when a multi-step flow crosses service/database boundaries and a later step must be compensated if it fails.
168
+
169
+ 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).
170
+
157
171
  ## Installation
158
172
 
159
173
  **Python 3.10+** is required.
@@ -860,10 +874,19 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
860
874
  A complete example can be found in
861
875
  the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
862
876
 
863
- > [!TIP]
864
- > You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
865
- > By default, it is set to `outbox`.
877
+ A runnable FastAPI flow (route → command → transactional outbox → publisher stub) is in
878
+ [fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_outbox.py)
866
879
 
880
+ > [!TIP]
881
+ > The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
882
+ > base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
883
+ > `create_all()`, before generating migrations and before the first query.
884
+ > [!TIP]
885
+ > `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
886
+ > rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
887
+ > `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
888
+ > [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
889
+ > how to register a native type for your own database.
867
890
  > [!TIP]
868
891
  > If you use the protobuf events you should specify `OutboxedEventRepository`
869
892
  > by [protobuf serialize](https://github.com/vadikko2/python-cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
@@ -1019,6 +1042,9 @@ async def join_metting(
1019
1042
  A complete example can be found in
1020
1043
  the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
1021
1044
 
1045
+ End-to-end command + outbox sample (business row and outbox event in one transaction, plus a publisher stub):
1046
+ [fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_outbox.py)
1047
+
1022
1048
  ### Kafka events consuming
1023
1049
 
1024
1050
  If you build interaction by events over broker like `Kafka`, you can to implement an event consumer on your
@@ -43,6 +43,7 @@
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)
@@ -85,10 +86,21 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
85
86
 
86
87
  - **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
87
88
  - **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
89
+ - **Storage:** SQLAlchemy-based Outbox and Saga storage on MySQL/MariaDB and PostgreSQL.
88
90
  - **Integration:** Ready for integration with FastAPI and FastStream.
89
91
  - **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
90
92
  - **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
91
93
 
94
+ ## When NOT to use python-cqrs
95
+
96
+ CQRS, Outbox, and Saga are tools for specific problems — not a default for every service.
97
+
98
+ - **Prefer a local DB transaction** for single-service CRUD (one database, no broker, no compensation).
99
+ - **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.
100
+ - **Use [Saga](#saga-pattern)** when a multi-step flow crosses service/database boundaries and a later step must be compensated if it fails.
101
+
102
+ 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).
103
+
92
104
  ## Installation
93
105
 
94
106
  **Python 3.10+** is required.
@@ -795,10 +807,19 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
795
807
  A complete example can be found in
796
808
  the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
797
809
 
798
- > [!TIP]
799
- > You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
800
- > By default, it is set to `outbox`.
810
+ A runnable FastAPI flow (route → command → transactional outbox → publisher stub) is in
811
+ [fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_outbox.py)
801
812
 
813
+ > [!TIP]
814
+ > The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
815
+ > base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
816
+ > `create_all()`, before generating migrations and before the first query.
817
+ > [!TIP]
818
+ > `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
819
+ > rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
820
+ > `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
821
+ > [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
822
+ > how to register a native type for your own database.
802
823
  > [!TIP]
803
824
  > If you use the protobuf events you should specify `OutboxedEventRepository`
804
825
  > by [protobuf serialize](https://github.com/vadikko2/python-cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
@@ -954,6 +975,9 @@ async def join_metting(
954
975
  A complete example can be found in
955
976
  the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
956
977
 
978
+ End-to-end command + outbox sample (business row and outbox event in one transaction, plus a publisher stub):
979
+ [fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_outbox.py)
980
+
957
981
  ### Kafka events consuming
958
982
 
959
983
  If you build interaction by events over broker like `Kafka`, you can to implement an event consumer on your
@@ -4,8 +4,8 @@ requires = ["setuptools>=42", "wheel"]
4
4
 
5
5
  [project]
6
6
  authors = [
7
- {name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"},
8
- {name = "Dmitry Kutlubaev", email = "kutlubaev00@mail.ru"}
7
+ { name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru" },
8
+ { name = "Dmitry Kutlubaev", email = "kutlubaev00@mail.ru" },
9
9
  ]
10
10
  classifiers = [
11
11
  "Development Status :: 4 - Beta",
@@ -14,7 +14,7 @@ classifiers = [
14
14
  "Programming Language :: Python :: 3.10",
15
15
  "Programming Language :: Python :: 3.11",
16
16
  "Programming Language :: Python :: 3.12",
17
- "Programming Language :: Python :: 3.13"
17
+ "Programming Language :: Python :: 3.13",
18
18
  ]
19
19
  dependencies = [
20
20
  "dataclass-wizard==0.*",
@@ -24,14 +24,14 @@ dependencies = [
24
24
  "pydantic==2.*",
25
25
  "sqlalchemy[asyncio]==2.0.*",
26
26
  "python-dotenv==1.*",
27
- "typing-extensions>=4.0"
27
+ "typing-extensions>=4.0",
28
28
  ]
29
29
  description = "Event-Driven Architecture Framework for Distributed Systems"
30
- maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
30
+ 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.11.0"
34
+ version = "4.13.0"
35
35
 
36
36
  [project.optional-dependencies]
37
37
  aiobreaker = ["aiobreaker>=0.3.0"]
@@ -45,27 +45,29 @@ dev = [
45
45
  "pytest-cov>=4.0.0",
46
46
  "pytest-codspeed==4.2.0",
47
47
  # Tests
48
- "aio-pika==9.3.0", # from rabbit
49
- "aiokafka==0.10.0", # from kafka
50
- "retry-async==0.1.*", # from kafka
51
- "requests==2.*", # from aiokafka
48
+ "aio-pika==9.3.0", # from rabbit
49
+ "aiokafka==0.10.0", # from kafka
50
+ "retry-async==0.1.*", # from kafka
51
+ "requests==2.*", # from aiokafka
52
52
  "pytest~=7.4.2",
53
53
  "pytest-asyncio~=0.21.1",
54
54
  "pytest-env==0.6.2",
55
- "cryptography==48.0.1",
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
- "aiobreaker>=0.3.0" # from aiobreaker
61
+ "aiobreaker>=0.3.0", # from aiobreaker
61
62
  ]
62
63
  examples = [
63
64
  "fastapi==0.109.*",
64
65
  "faststream[kafka]==0.5.28",
65
66
  "faker>=37.12.0",
66
67
  "uvicorn==0.32.0",
67
- "aiohttp==3.14.1",
68
+ "aiohttp==3.14.3",
68
69
  "protobuf>=4.25.8",
70
+ "aiosqlite>=0.20.0",
69
71
  ]
70
72
  kafka = ["aiokafka==0.10.0", "retry-async==0.1.*"]
71
73
  rabbit = ["aio-pika==9.3.0"]
@@ -54,6 +54,14 @@ from cqrs.saga.step import (
54
54
  SagaStepHandler,
55
55
  SagaStepResult,
56
56
  )
57
+ from cqrs.sqlalchemy_types import (
58
+ Binary16,
59
+ DialectAwareType,
60
+ DialectTypeHandler,
61
+ JSONType,
62
+ PayloadBinary,
63
+ UUIDBinary,
64
+ )
57
65
 
58
66
  __all__ = (
59
67
  "ICircuitBreaker",
@@ -108,4 +116,10 @@ __all__ = (
108
116
  "ContextT",
109
117
  "SagaMermaid",
110
118
  "CoRMermaid",
119
+ "DialectAwareType",
120
+ "DialectTypeHandler",
121
+ "UUIDBinary",
122
+ "Binary16",
123
+ "JSONType",
124
+ "PayloadBinary",
111
125
  )
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import functools
2
3
  import logging
3
4
  import typing
@@ -169,22 +170,32 @@ class EventEmitter:
169
170
  type(event).__name__,
170
171
  )
171
172
  return ()
173
+
174
+ results = await asyncio.gather(
175
+ *(self._process_single_handler(event, item) for item in handlers_types),
176
+ return_exceptions=True,
177
+ )
178
+
172
179
  follow_ups: list[IEvent] = []
173
- for handler_item in handlers_types:
174
- if isinstance(handler_item, EventHandlerFallback):
175
- follow_ups.extend(
176
- await self._handle_with_fallback(event, handler_item),
180
+ for handler_item, res in zip(handlers_types, results):
181
+ if isinstance(res, Exception):
182
+ handler_name = (
183
+ handler_item.primary.__name__
184
+ if isinstance(handler_item, EventHandlerFallback)
185
+ else handler_item.__name__
177
186
  )
178
- continue
179
- handler_type = handler_item
180
- handler: _H = await self._container.resolve(handler_type)
181
- logger.debug(
182
- "Handling Event(%s) via event handler(%s)",
183
- type(event).__name__,
184
- handler_type.__name__,
185
- )
186
- await handler.handle(event)
187
- follow_ups.extend(list(handler.events))
187
+ logger.exception(
188
+ "Error occurred while processing domain event %s in handler %s: %s",
189
+ type(event).__name__,
190
+ handler_name,
191
+ res,
192
+ exc_info=res,
193
+ )
194
+ elif isinstance(res, BaseException):
195
+ raise res
196
+ else:
197
+ follow_ups.extend(res)
198
+
188
199
  return follow_ups
189
200
 
190
201
  @emit.register(INotificationEvent)
@@ -192,3 +203,22 @@ class EventEmitter:
192
203
  """Emit notification event: send to message broker; no follow-ups."""
193
204
  await self._send_to_broker(event)
194
205
  return ()
206
+
207
+ async def _process_single_handler(
208
+ self,
209
+ event: IDomainEvent,
210
+ handler_item: typing.Union[typing.Type[_H], EventHandlerFallback],
211
+ ) -> typing.Sequence[IEvent]:
212
+ """Process a single event handler and return its follow-up events."""
213
+ if isinstance(handler_item, EventHandlerFallback):
214
+ return await self._handle_with_fallback(event, handler_item)
215
+
216
+ handler_type = handler_item
217
+ handler: _H = await self._container.resolve(handler_type)
218
+ logger.debug(
219
+ "Handling Event(%s) via event handler(%s)",
220
+ type(event).__name__,
221
+ handler_type.__name__,
222
+ )
223
+ await handler.handle(event)
224
+ return list(handler.events)
@@ -15,7 +15,8 @@ try:
15
15
  from sqlalchemy import func
16
16
  from sqlalchemy.orm import Mapped, mapped_column, DeclarativeMeta, registry
17
17
  from sqlalchemy.ext.asyncio import session as sql_session
18
- from sqlalchemy.dialects import postgresql
18
+
19
+ from cqrs import sqlalchemy_types
19
20
  except ImportError:
20
21
  raise ImportError(
21
22
  "You are trying to use SQLAlchemy outbox implementation, "
@@ -35,37 +36,10 @@ DEFAULT_OUTBOX_TABLE_NAME = "outbox"
35
36
  MAX_FLUSH_COUNTER_VALUE = 5
36
37
 
37
38
 
38
- class BinaryUUID(sqlalchemy.TypeDecorator):
39
- """Stores the UUID as a native UUID in Postgres and as BINARY(16) in other databases (MySQL)."""
40
-
41
- impl = sqlalchemy.BINARY(16)
42
- cache_ok = True
43
-
44
- def load_dialect_impl(self, dialect):
45
- if dialect.name == "postgresql":
46
- return dialect.type_descriptor(postgresql.UUID())
47
- else:
48
- return dialect.type_descriptor(sqlalchemy.BINARY(16))
49
-
50
- def process_bind_param(self, value, dialect):
51
- if value is None:
52
- return value
53
- if isinstance(value, str):
54
- value = uuid.UUID(value)
55
- if dialect.name == "postgresql":
56
- return value # asyncpg works with uuid.UUID
57
- if isinstance(value, uuid.UUID):
58
- return value.bytes # For MySQL return 16 bytes
59
- return value
60
-
61
- def process_result_value(self, value, dialect):
62
- if value is None:
63
- return value
64
- if dialect.name == "postgresql":
65
- return value # asyncpg return uuid.UUID
66
- if isinstance(value, bytes):
67
- return uuid.UUID(bytes=value) # From MySQL got bytes, make UUID
68
- return value
39
+ # Deprecated name of cqrs.sqlalchemy_types.UUIDBinary — kept importable for
40
+ # backward compatibility (e.g. already generated Alembic migrations).
41
+ # Prefer cqrs.sqlalchemy_types.UUIDBinary in new code.
42
+ BinaryUUID = sqlalchemy_types.UUIDBinary
69
43
 
70
44
 
71
45
  class OutboxModel(Base):
@@ -87,12 +61,12 @@ class OutboxModel(Base):
87
61
  comment="Identity",
88
62
  )
89
63
  event_id: Mapped[uuid.UUID] = mapped_column(
90
- BinaryUUID,
64
+ sqlalchemy_types.UUIDBinary,
91
65
  nullable=False,
92
66
  comment="Event idempotency id",
93
67
  )
94
68
  event_id_bin: Mapped[bytes] = mapped_column(
95
- sqlalchemy.BINARY(16),
69
+ sqlalchemy_types.Binary16,
96
70
  nullable=False,
97
71
  comment="Event idempotency id in 16 bit presentation",
98
72
  )
@@ -126,7 +100,7 @@ class OutboxModel(Base):
126
100
  comment="Event creation timestamp",
127
101
  )
128
102
  payload: Mapped[bytes] = mapped_column(
129
- sqlalchemy.LargeBinary,
103
+ sqlalchemy_types.PayloadBinary,
130
104
  nullable=False,
131
105
  comment="Event payload",
132
106
  )
@@ -177,13 +151,14 @@ class OutboxModel(Base):
177
151
 
178
152
  @classmethod
179
153
  def status_sorting_case(cls) -> sqlalchemy.Case:
154
+ # Conditions instead of a `value=` mapping: only a comparison with the
155
+ # column binds the statuses through the Enum type. As plain mapping keys
156
+ # they are sent as enum values ("new"), which PostgreSQL rejects because
157
+ # its `eventstatus` type holds member names ("NEW").
180
158
  return sqlalchemy.case(
181
- {
182
- repository.EventStatus.NEW: 1,
183
- repository.EventStatus.NOT_PRODUCED: 2,
184
- repository.EventStatus.PRODUCED: 3,
185
- },
186
- value=cls.event_status,
159
+ (cls.event_status == repository.EventStatus.NEW, 1),
160
+ (cls.event_status == repository.EventStatus.NOT_PRODUCED, 2),
161
+ (cls.event_status == repository.EventStatus.PRODUCED, 3),
187
162
  else_=4,
188
163
  )
189
164
 
@@ -17,6 +17,8 @@ try:
17
17
  from sqlalchemy.exc import SQLAlchemyError
18
18
  from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
19
19
  from sqlalchemy.orm import registry
20
+
21
+ from cqrs.sqlalchemy_types import JSONType
20
22
  except ImportError:
21
23
  raise ImportError(
22
24
  "You are trying to use SQLAlchemy saga storage implementation, "
@@ -63,7 +65,7 @@ class SagaExecutionModel(Base):
63
65
  comment="Current status of the saga",
64
66
  )
65
67
  context = sqlalchemy.Column(
66
- sqlalchemy.JSON,
68
+ JSONType,
67
69
  nullable=False,
68
70
  comment="Serialized context",
69
71
  )
@@ -0,0 +1,237 @@
1
+ """Dialect-aware SQLAlchemy types shared by the library models.
2
+
3
+ The module generalizes the trick used by the outbox model: a single Python type
4
+ is rendered as the most suitable native type of the current dialect. Mapping
5
+ between a dialect and its DDL type/value conversion is kept in a per-class
6
+ registry, so support for a new DBMS is one `register_dialect` call away.
7
+ """
8
+
9
+ import dataclasses
10
+ import typing
11
+ import uuid
12
+
13
+ try:
14
+ import sqlalchemy
15
+
16
+ from sqlalchemy.dialects import mysql, postgresql
17
+ except ImportError:
18
+ raise ImportError(
19
+ "You are trying to use SQLAlchemy types, "
20
+ "but 'sqlalchemy' is not installed. "
21
+ "Please install it using: pip install python-cqrs[sqlalchemy]",
22
+ ) from None
23
+
24
+ __all__ = (
25
+ "Binary16",
26
+ "DialectAwareType",
27
+ "DialectTypeHandler",
28
+ "JSONType",
29
+ "PayloadBinary",
30
+ "UUIDBinary",
31
+ )
32
+
33
+
34
+ @dataclasses.dataclass(frozen=True)
35
+ class DialectTypeHandler:
36
+ """Describes how a type lives in a particular dialect.
37
+
38
+ Attributes:
39
+ type_factory: builds the dialect native type used in DDL and binding.
40
+ bind: converts a Python value into the dialect representation.
41
+ result: converts a value fetched from the database into a Python one.
42
+ """
43
+
44
+ type_factory: typing.Callable[[typing.Any], typing.Any]
45
+ bind: typing.Callable[[typing.Any], typing.Any] | None = None
46
+ result: typing.Callable[[typing.Any], typing.Any] | None = None
47
+
48
+
49
+ class DialectAwareType(sqlalchemy.TypeDecorator):
50
+ """Base class for types resolved through a per-dialect registry.
51
+
52
+ Subclasses declare a `default_handler` used as a portable fallback and
53
+ register dialect specific handlers via `register_dialect`. Every subclass
54
+ owns its registry, so registrations never leak between types.
55
+ """
56
+
57
+ impl = sqlalchemy.LargeBinary
58
+ cache_ok = True
59
+
60
+ default_handler: typing.ClassVar[DialectTypeHandler]
61
+ _handlers: typing.ClassVar[typing.Dict[typing.Text, DialectTypeHandler]] = {}
62
+
63
+ def __init_subclass__(cls, **kwargs) -> None:
64
+ super().__init_subclass__(**kwargs)
65
+ # Each subclass owns its registry, otherwise a registration made for one
66
+ # type would leak into all the others.
67
+ cls._handlers = dict(cls._handlers)
68
+
69
+ @classmethod
70
+ def register_dialect(
71
+ cls,
72
+ dialect_name: typing.Text,
73
+ handler: DialectTypeHandler,
74
+ ) -> None:
75
+ """Registers (or overrides) the handler used for `dialect_name`."""
76
+ cls._handlers[dialect_name] = handler
77
+
78
+ @classmethod
79
+ def get_handler(cls, dialect_name: typing.Text) -> DialectTypeHandler:
80
+ """Returns the handler for `dialect_name` or the portable fallback."""
81
+ return cls._handlers.get(dialect_name, cls.default_handler)
82
+
83
+ def load_dialect_impl(self, dialect):
84
+ handler = self.get_handler(dialect.name)
85
+ return dialect.type_descriptor(handler.type_factory(dialect))
86
+
87
+ def process_bind_param(self, value, dialect):
88
+ if value is None:
89
+ return value
90
+ handler = self.get_handler(dialect.name)
91
+ if handler.bind is None:
92
+ return value
93
+ return handler.bind(value)
94
+
95
+ def process_result_value(self, value, dialect):
96
+ if value is None:
97
+ return value
98
+ handler = self.get_handler(dialect.name)
99
+ if handler.result is None:
100
+ return value
101
+ return handler.result(value)
102
+
103
+
104
+ def _as_uuid(value: typing.Any) -> typing.Any:
105
+ if isinstance(value, str):
106
+ return uuid.UUID(value)
107
+ return value
108
+
109
+
110
+ def _as_bytes(value: typing.Any) -> typing.Any:
111
+ value = _as_uuid(value)
112
+ if isinstance(value, uuid.UUID):
113
+ return value.bytes
114
+ return value
115
+
116
+
117
+ def _as_uuid_from_bytes(value: typing.Any) -> typing.Any:
118
+ if isinstance(value, bytes):
119
+ return uuid.UUID(bytes=value)
120
+ return value
121
+
122
+
123
+ class UUIDBinary(DialectAwareType):
124
+ """`uuid.UUID` stored as a native UUID in PostgreSQL and as BINARY(16) elsewhere."""
125
+
126
+ default_handler = DialectTypeHandler(
127
+ type_factory=lambda dialect: sqlalchemy.LargeBinary(16),
128
+ bind=_as_bytes,
129
+ result=_as_uuid_from_bytes,
130
+ )
131
+
132
+
133
+ UUIDBinary.register_dialect(
134
+ "postgresql",
135
+ DialectTypeHandler(
136
+ # asyncpg works with uuid.UUID, no conversion is needed.
137
+ type_factory=lambda dialect: postgresql.UUID(as_uuid=True),
138
+ bind=_as_uuid,
139
+ ),
140
+ )
141
+ UUIDBinary.register_dialect(
142
+ "mysql",
143
+ DialectTypeHandler(
144
+ type_factory=lambda dialect: mysql.BINARY(16),
145
+ bind=_as_bytes,
146
+ result=_as_uuid_from_bytes,
147
+ ),
148
+ )
149
+ UUIDBinary.register_dialect("mariadb", UUIDBinary.get_handler("mysql"))
150
+
151
+
152
+ class Binary16(DialectAwareType):
153
+ """Raw 16 bytes: BYTEA in PostgreSQL, BINARY(16) in MySQL, LargeBinary(16) elsewhere."""
154
+
155
+ default_handler = DialectTypeHandler(
156
+ type_factory=lambda dialect: sqlalchemy.LargeBinary(16),
157
+ bind=_as_bytes,
158
+ )
159
+
160
+
161
+ Binary16.register_dialect(
162
+ "postgresql",
163
+ DialectTypeHandler(
164
+ type_factory=lambda dialect: postgresql.BYTEA(),
165
+ bind=_as_bytes,
166
+ ),
167
+ )
168
+ Binary16.register_dialect(
169
+ "mysql",
170
+ DialectTypeHandler(
171
+ type_factory=lambda dialect: mysql.BINARY(16),
172
+ bind=_as_bytes,
173
+ ),
174
+ )
175
+ Binary16.register_dialect("mariadb", Binary16.get_handler("mysql"))
176
+
177
+
178
+ class JSONType(DialectAwareType):
179
+ """JSON document rendered as plain `JSON` on every dialect.
180
+
181
+ The default handler intentionally repeats `sqlalchemy.JSON`, so switching a
182
+ column to this type does not change the generated DDL anywhere. It only adds
183
+ an extension point: a project that wants a native PostgreSQL `JSONB` enables
184
+ it with a single registration, without touching the library models.
185
+
186
+ ```python
187
+ from sqlalchemy.dialects import postgresql
188
+ from cqrs.sqlalchemy_types import DialectTypeHandler, JSONType
189
+
190
+ JSONType.register_dialect(
191
+ "postgresql",
192
+ DialectTypeHandler(type_factory=lambda dialect: postgresql.JSONB()),
193
+ )
194
+ ```
195
+
196
+ No value conversion is performed: serialization stays the responsibility of
197
+ the dialect level JSON type, exactly as with `sqlalchemy.JSON`.
198
+ """
199
+
200
+ impl = sqlalchemy.JSON
201
+ cache_ok = True
202
+
203
+ default_handler = DialectTypeHandler(
204
+ type_factory=lambda dialect: sqlalchemy.JSON(),
205
+ )
206
+
207
+
208
+ class PayloadBinary(DialectAwareType):
209
+ """Binary blob of an arbitrary length rendered as plain `LargeBinary` everywhere.
210
+
211
+ The default handler intentionally repeats `sqlalchemy.LargeBinary` without a
212
+ length, so switching a column to this type keeps the DDL identical on every
213
+ dialect (`BYTEA` in PostgreSQL, `BLOB` in MySQL and SQLite). It only adds an
214
+ extension point for a DBMS the library knows nothing about: Microsoft SQL
215
+ Server, for instance, maps `LargeBinary` to the deprecated `IMAGE`, which a
216
+ project fixes with a single registration.
217
+
218
+ ```python
219
+ from sqlalchemy.dialects import mssql
220
+ from cqrs.sqlalchemy_types import DialectTypeHandler, PayloadBinary
221
+
222
+ PayloadBinary.register_dialect(
223
+ "mssql",
224
+ DialectTypeHandler(type_factory=lambda dialect: mssql.VARBINARY("max")),
225
+ )
226
+ ```
227
+
228
+ No value conversion is performed: the column holds `bytes` and they are
229
+ passed to the driver as is.
230
+ """
231
+
232
+ impl = sqlalchemy.LargeBinary
233
+ cache_ok = True
234
+
235
+ default_handler = DialectTypeHandler(
236
+ type_factory=lambda dialect: sqlalchemy.LargeBinary(),
237
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cqrs
3
- Version: 4.11.0
3
+ Version: 4.13.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,7 +42,8 @@ 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: cryptography==48.0.1; extra == "dev"
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"
@@ -52,8 +53,9 @@ Requires-Dist: fastapi==0.109.*; extra == "examples"
52
53
  Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
53
54
  Requires-Dist: faker>=37.12.0; extra == "examples"
54
55
  Requires-Dist: uvicorn==0.32.0; extra == "examples"
55
- Requires-Dist: aiohttp==3.14.1; extra == "examples"
56
+ Requires-Dist: aiohttp==3.14.3; extra == "examples"
56
57
  Requires-Dist: protobuf>=4.25.8; extra == "examples"
58
+ Requires-Dist: aiosqlite>=0.20.0; extra == "examples"
57
59
  Provides-Extra: kafka
58
60
  Requires-Dist: aiokafka==0.10.0; extra == "kafka"
59
61
  Requires-Dist: retry-async==0.1.*; extra == "kafka"
@@ -108,6 +110,7 @@ Dynamic: license-file
108
110
  ## Table of Contents
109
111
 
110
112
  - [Overview](#overview)
113
+ - [When NOT to use python-cqrs](#when-not-to-use-python-cqrs)
111
114
  - [Installation](#installation)
112
115
  - [Quick Start](#quick-start)
113
116
  - [Request and Response Types](#request-and-response-types)
@@ -150,10 +153,21 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
150
153
 
151
154
  - **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
152
155
  - **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
156
+ - **Storage:** SQLAlchemy-based Outbox and Saga storage on MySQL/MariaDB and PostgreSQL.
153
157
  - **Integration:** Ready for integration with FastAPI and FastStream.
154
158
  - **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
155
159
  - **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
156
160
 
161
+ ## When NOT to use python-cqrs
162
+
163
+ CQRS, Outbox, and Saga are tools for specific problems — not a default for every service.
164
+
165
+ - **Prefer a local DB transaction** for single-service CRUD (one database, no broker, no compensation).
166
+ - **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.
167
+ - **Use [Saga](#saga-pattern)** when a multi-step flow crosses service/database boundaries and a later step must be compensated if it fails.
168
+
169
+ 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).
170
+
157
171
  ## Installation
158
172
 
159
173
  **Python 3.10+** is required.
@@ -860,10 +874,19 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
860
874
  A complete example can be found in
861
875
  the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
862
876
 
863
- > [!TIP]
864
- > You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
865
- > By default, it is set to `outbox`.
877
+ A runnable FastAPI flow (route → command → transactional outbox → publisher stub) is in
878
+ [fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_outbox.py)
866
879
 
880
+ > [!TIP]
881
+ > The Outbox table is named `outbox` by default. To rename it, or to attach the model to your own declarative
882
+ > base, use `cqrs.rebind_outbox_model(OutboxModel, Base, table_name="my_outbox_table")` at startup — before
883
+ > `create_all()`, before generating migrations and before the first query.
884
+ > [!TIP]
885
+ > `SqlAlchemyOutboxedEventRepository` works on **MySQL/MariaDB** and **PostgreSQL**: the outbox columns are
886
+ > rendered with dialect-aware types (`BINARY(16)` on MySQL, `UUID`/`BYTEA` on PostgreSQL), with a portable
887
+ > `BLOB` fallback for other dialects. Switching databases is a DSN change only — see the
888
+ > [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
889
+ > how to register a native type for your own database.
867
890
  > [!TIP]
868
891
  > If you use the protobuf events you should specify `OutboxedEventRepository`
869
892
  > by [protobuf serialize](https://github.com/vadikko2/python-cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
@@ -1019,6 +1042,9 @@ async def join_metting(
1019
1042
  A complete example can be found in
1020
1043
  the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
1021
1044
 
1045
+ End-to-end command + outbox sample (business row and outbox event in one transaction, plus a publisher stub):
1046
+ [fastapi_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_outbox.py)
1047
+
1022
1048
  ### Kafka events consuming
1023
1049
 
1024
1050
  If you build interaction by events over broker like `Kafka`, you can to implement an event consumer on your
@@ -7,6 +7,7 @@ src/cqrs/generic_utils.py
7
7
  src/cqrs/mediator.py
8
8
  src/cqrs/producer.py
9
9
  src/cqrs/response.py
10
+ src/cqrs/sqlalchemy_types.py
10
11
  src/cqrs/types.py
11
12
  src/cqrs/adapters/__init__.py
12
13
  src/cqrs/adapters/amqp.py
@@ -25,7 +25,8 @@ requests==2.*
25
25
  pytest~=7.4.2
26
26
  pytest-asyncio~=0.21.1
27
27
  pytest-env==0.6.2
28
- cryptography==48.0.1
28
+ alembic>=1.13.0
29
+ cryptography==50.0.0
29
30
  asyncmy==0.2.9
30
31
  asyncpg>=0.29.0
31
32
  redis>=5.0.0
@@ -36,8 +37,9 @@ fastapi==0.109.*
36
37
  faststream[kafka]==0.5.28
37
38
  faker>=37.12.0
38
39
  uvicorn==0.32.0
39
- aiohttp==3.14.1
40
+ aiohttp==3.14.3
40
41
  protobuf>=4.25.8
42
+ aiosqlite>=0.20.0
41
43
 
42
44
  [kafka]
43
45
  aiokafka==0.10.0
File without changes
File without changes