python-cqrs 4.12.0__tar.gz → 4.14.0__tar.gz

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