python-cqrs 5.1.0__py3-none-any.whl → 5.2.0__py3-none-any.whl

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 (71) hide show
  1. cqrs/__init__.py +71 -0
  2. cqrs/adapters/__init__.py +10 -0
  3. cqrs/adapters/circuit_breaker.py +6 -0
  4. cqrs/adapters/kafka.py +10 -0
  5. cqrs/bootstrap/__init__.py +17 -1
  6. cqrs/bootstrap/events.py +13 -2
  7. cqrs/bootstrap/requests.py +41 -0
  8. cqrs/bootstrap/saga.py +14 -1
  9. cqrs/circuit_breaker.py +10 -0
  10. cqrs/compressors/protocol.py +21 -2
  11. cqrs/compressors/zlib.py +9 -0
  12. cqrs/container/__init__.py +12 -0
  13. cqrs/container/dependency_injector.py +6 -0
  14. cqrs/container/di.py +7 -0
  15. cqrs/container/dishka.py +14 -1
  16. cqrs/container/protocol.py +18 -0
  17. cqrs/container/scope.py +29 -1
  18. cqrs/dispatcher/__init__.py +9 -0
  19. cqrs/events/__init__.py +9 -1
  20. cqrs/events/event_emitter.py +14 -0
  21. cqrs/events/fallback.py +6 -0
  22. cqrs/generic_utils.py +76 -0
  23. cqrs/handlers/__init__.py +11 -0
  24. cqrs/handlers/cor.py +59 -1
  25. cqrs/handlers/event.py +17 -0
  26. cqrs/handlers/request.py +69 -1
  27. cqrs/handlers/saga.py +30 -0
  28. cqrs/mapping/events.py +13 -0
  29. cqrs/mapping/outbox.py +40 -0
  30. cqrs/mapping/requests.py +37 -0
  31. cqrs/mediators/event.py +12 -0
  32. cqrs/mediators/request.py +19 -10
  33. cqrs/mediators/saga.py +16 -1
  34. cqrs/mediators/streaming.py +23 -3
  35. cqrs/mermaid/cor.py +13 -0
  36. cqrs/mermaid/saga.py +11 -0
  37. cqrs/message_brokers/__init__.py +11 -0
  38. cqrs/message_brokers/amqp.py +9 -0
  39. cqrs/message_brokers/kafka.py +9 -0
  40. cqrs/message_brokers/producer.py +27 -1
  41. cqrs/message_brokers/protocol.py +17 -0
  42. cqrs/middlewares/__init__.py +11 -0
  43. cqrs/middlewares/base.py +29 -0
  44. cqrs/middlewares/logging.py +7 -0
  45. cqrs/models/event.py +54 -0
  46. cqrs/models/pydantic.py +34 -4
  47. cqrs/models/request.py +38 -4
  48. cqrs/models/response.py +20 -0
  49. cqrs/outbox/repository.py +60 -5
  50. cqrs/outbox/sqlalchemy.py +26 -0
  51. cqrs/outbox/sqlmodel.py +12 -1
  52. cqrs/py.typed +0 -0
  53. cqrs/requests/fallback.py +8 -0
  54. cqrs/saga/fallback.py +15 -1
  55. cqrs/saga/models.py +15 -0
  56. cqrs/saga/recovery.py +18 -0
  57. cqrs/saga/saga.py +37 -14
  58. cqrs/saga/storage/__init__.py +12 -0
  59. cqrs/saga/storage/memory.py +18 -1
  60. cqrs/saga/storage/protocol.py +8 -0
  61. cqrs/saga/storage/sqlalchemy.py +16 -0
  62. cqrs/saga/storage/sqlmodel.py +13 -1
  63. cqrs/serializers/json.py +8 -1
  64. cqrs/serializers/protobuf.py +9 -1
  65. cqrs/serializers/protocol.py +41 -7
  66. {python_cqrs-5.1.0.dist-info → python_cqrs-5.2.0.dist-info}/METADATA +81 -36
  67. python_cqrs-5.2.0.dist-info/RECORD +106 -0
  68. {python_cqrs-5.1.0.dist-info → python_cqrs-5.2.0.dist-info}/WHEEL +1 -1
  69. python_cqrs-5.1.0.dist-info/RECORD +0 -105
  70. {python_cqrs-5.1.0.dist-info → python_cqrs-5.2.0.dist-info}/licenses/LICENSE +0 -0
  71. {python_cqrs-5.1.0.dist-info → python_cqrs-5.2.0.dist-info}/top_level.txt +0 -0
cqrs/__init__.py CHANGED
@@ -1,3 +1,74 @@
1
+ """
2
+ python-cqrs.
3
+
4
+ [AI Instruction]
5
+ Import the library as ``cqrs``. Names listed in ``__all__`` below are the
6
+ stable surface. Commands and queries are both ``cqrs.Request``. Wire a
7
+ request with ``RequestMap.bind``, an event with ``EventMap.bind``, and a
8
+ saga with ``SagaMap.bind``. There is no ``Command``, ``Query``, or
9
+ ``CommandBus`` type, and no ``register_handler`` decorator.
10
+
11
+ Startup is not on this package. Import the submodule:
12
+
13
+ - ``cqrs.bootstrap.requests.bootstrap`` builds ``RequestMediator``.
14
+ - ``cqrs.bootstrap.requests.bootstrap_streaming`` builds
15
+ ``StreamingRequestMediator``.
16
+ - ``cqrs.bootstrap.events.bootstrap`` builds ``EventMediator``.
17
+ - ``cqrs.bootstrap.saga.bootstrap`` builds ``SagaMediator``.
18
+
19
+ Requests and handlers on ``cqrs``: ``Request``, ``DCRequest``, ``Response``,
20
+ ``RequestHandler``, ``StreamingRequestHandler``, ``RequestMediator``,
21
+ ``StreamingRequestMediator``, ``RequestMap``, ``RequestHandlerFallback``.
22
+ ``PydanticRequest`` and ``PydanticResponse`` are lazy and need the
23
+ ``pydantic`` extra. A chain of handlers is ``cqrs.handlers.CORRequestHandler``.
24
+ ``build_chain`` is ``cqrs.handlers.cor.build_chain``.
25
+
26
+ Events on ``cqrs``: ``DomainEvent`` for this process, ``NotificationEvent``
27
+ for another service, plus ``EventHandler``, ``EventMap``, ``EventMediator``,
28
+ ``EventEmitter``, ``EventHandlerFallback``. Pydantic event classes are lazy
29
+ and need the ``pydantic`` extra.
30
+
31
+ Sagas on ``cqrs``: ``Saga``, ``SagaStepHandler``, ``SagaStepResult``,
32
+ ``SagaMediator``, ``SagaMap``. Import the rest from their modules.
33
+ ``SagaContext`` is ``cqrs.saga.models.SagaContext``. A fallback step is
34
+ ``cqrs.saga.fallback.Fallback``. Resume a run with
35
+ ``cqrs.saga.recovery.recover_saga``. The storage protocol is
36
+ ``cqrs.saga.storage.ISagaStorage``. Implementations are
37
+ ``cqrs.saga.storage.memory.MemorySagaStorage``,
38
+ ``cqrs.saga.storage.sqlalchemy.SqlAlchemySagaStorage`` (extra
39
+ ``sqlalchemy``), and ``cqrs.saga.storage.sqlmodel.SqlModelSagaStorage``
40
+ (extra ``sqlmodel``). Those storage classes are not attributes of ``cqrs``.
41
+
42
+ Outbox on ``cqrs``: ``OutboxedEventRepository``, ``OutboxedEventMap``,
43
+ ``EventProducer``, ``EventStatus``. ``SqlAlchemyOutboxedEventRepository``,
44
+ ``rebind_outbox_model``, and the dialect types are lazy and need the
45
+ ``sqlalchemy`` extra. ``SqlModelOutboxedEventRepository`` is lazy and needs
46
+ the ``sqlmodel`` extra. Codecs on ``cqrs``: ``JsonEventSerializer``,
47
+ ``ProtobufEventSerializer`` (extra ``protobuf``).
48
+
49
+ Brokers are not on ``cqrs``. Kafka is
50
+ ``cqrs.adapters.kafka.kafka_producer_factory`` plus
51
+ ``cqrs.message_brokers.kafka.KafkaMessageBroker`` (extra ``kafka``). AMQP is
52
+ ``cqrs.message_brokers.amqp.AMQPMessageBroker`` (extra ``rabbit``).
53
+
54
+ Containers: pass one into bootstrap. ``cqrs.DIContainer`` wraps the ``di``
55
+ package, and bootstrap wraps a raw ``di.Container`` itself.
56
+ ``cqrs.container.dependency_injector.DependencyInjectorCQRSContainer`` wraps
57
+ dependency-injector. ``cqrs.container.dishka.DishkaCQRSContainer`` needs the
58
+ ``dishka`` extra. Scope helpers on ``cqrs`` are ``ScopeStrategy``,
59
+ ``bind_scope``, ``enter_scope``, and ``current_container``.
60
+
61
+ Middleware is ``cqrs.middlewares.Middleware``. Logging is
62
+ ``cqrs.middlewares.logging.LoggingMiddleware``; bootstrap adds it when the
63
+ list does not already contain it. A circuit breaker adapter is
64
+ ``cqrs.adapters.circuit_breaker.AioBreakerAdapter`` (extra ``aiobreaker``).
65
+ Diagrams on ``cqrs`` are ``CoRMermaid`` and ``SagaMermaid``.
66
+
67
+ Do not import ``cqrs.dispatcher`` from application code. Mediators call it.
68
+ Optional integrations are extras in ``pyproject.toml``. Reading a lazy name
69
+ without its extra raises ``ImportError``.
70
+ """
71
+
1
72
  import typing
2
73
 
3
74
  if typing.TYPE_CHECKING:
cqrs/adapters/__init__.py CHANGED
@@ -0,0 +1,10 @@
1
+ """
2
+ Broker and producer adapters.
3
+
4
+ [AI Instruction]
5
+ This package has no re-exports. Kafka is ``cqrs.adapters.kafka`` (extra
6
+ ``kafka``). AMQP is ``cqrs.adapters.amqp`` (extra ``rabbit``). The circuit
7
+ breaker adapter is ``cqrs.adapters.circuit_breaker.AioBreakerAdapter``
8
+ (extra ``aiobreaker``). Broker classes themselves are
9
+ ``cqrs.message_brokers.kafka`` and ``cqrs.message_brokers.amqp``.
10
+ """
@@ -136,6 +136,12 @@ class AioBreakerAdapter(ICircuitBreaker, ISagaStepCircuitBreaker):
136
136
  saga_cb = AioBreakerAdapter(fail_max=3, timeout_duration=60)
137
137
  request_cb = AioBreakerAdapter(fail_max=5, timeout_duration=30)
138
138
  event_cb = AioBreakerAdapter(fail_max=5, timeout_duration=30)
139
+
140
+ [AI Instruction]
141
+ Construct one adapter per area (requests, events, saga) and pass it into
142
+ that area's fallback. ``fail_max`` is how many failures open the circuit.
143
+ ``timeout_duration`` is how long it stays open. Requires ``aiobreaker``.
144
+ A business exception listed in ``exclude`` must not open the circuit.
139
145
  """
140
146
 
141
147
  def __init__(
cqrs/adapters/kafka.py CHANGED
@@ -1,3 +1,13 @@
1
+ """
2
+ Kafka producer adapter.
3
+
4
+ [AI Instruction]
5
+ Import ``kafka_producer_factory`` from this module and pass the result to
6
+ ``cqrs.message_brokers.kafka.KafkaMessageBroker``. Both need the ``kafka``
7
+ extra. Application code does not call the producer directly when an outbox
8
+ or an event emitter is already publishing.
9
+ """
10
+
1
11
  import asyncio
2
12
  import functools
3
13
  import logging
@@ -1,4 +1,20 @@
1
- """Bootstrap helpers for mediators (requests, events, saga)."""
1
+ """
2
+ Bootstrap helpers for mediators (requests, events, saga).
3
+
4
+ [AI Instruction]
5
+ These submodules are the application entry points. ``cqrs`` does not export
6
+ them.
7
+
8
+ - ``cqrs.bootstrap.requests.bootstrap`` returns ``RequestMediator``.
9
+ ``bootstrap_streaming`` in the same module returns
10
+ ``StreamingRequestMediator``.
11
+ - ``cqrs.bootstrap.events.bootstrap`` returns ``EventMediator``.
12
+ - ``cqrs.bootstrap.saga.bootstrap`` returns ``SagaMediator``.
13
+
14
+ Import the submodule (``from cqrs.bootstrap import requests``) and call
15
+ ``bootstrap`` on it. ``setup_*`` functions in those modules are for callers
16
+ that already hold an emitter or a container.
17
+ """
2
18
 
3
19
  from cqrs.bootstrap import events, requests, saga
4
20
 
cqrs/bootstrap/events.py CHANGED
@@ -58,7 +58,12 @@ def setup_mediator(
58
58
  middlewares=[logging_middleware.LoggingMiddleware()],
59
59
  events_mapper=bind_events,
60
60
  )
61
- await mediator.emit(OrderCreatedEvent(order_id="1"))
61
+ await mediator.send(OrderCreatedEvent(order_id="1"))
62
+
63
+ [AI Instruction]
64
+ Assemble an ``EventMediator`` from a container the caller already holds.
65
+ Application startup should call ``bootstrap`` instead, unless it needs
66
+ this lower-level wiring.
62
67
  """
63
68
  _events_mapper = events.EventMap()
64
69
  if events_mapper is not None:
@@ -130,7 +135,13 @@ def bootstrap(
130
135
  di_container=di.Container(),
131
136
  events_mapper=bind_events,
132
137
  )
133
- await mediator.emit(OrderCreatedEvent(order_id="1"))
138
+ await mediator.send(OrderCreatedEvent(order_id="1"))
139
+
140
+ [AI Instruction]
141
+ Build an ``EventMediator`` for events that arrive from outside this
142
+ process. ``events_mapper`` calls ``EventMap.bind``. The application then
143
+ ``await mediator.send(event)``. Commands and queries use
144
+ ``cqrs.bootstrap.requests.bootstrap``.
134
145
  """
135
146
  if on_startup is None:
136
147
  on_startup = []
@@ -46,6 +46,13 @@ def setup_event_emitter(
46
46
  *,
47
47
  serializer: EventSerializer | None = None,
48
48
  ) -> events.EventEmitter:
49
+ """
50
+ [AI Instruction]
51
+ Build the emitter ``bootstrap`` uses to publish handler events. Call
52
+ ``bootstrap`` from application startup. Use this only when the
53
+ application already owns the container and must pass a custom emitter
54
+ into ``setup_mediator``.
55
+ """
49
56
  if message_broker is None:
50
57
  message_broker = DEFAULT_MESSAGE_BROKER
51
58
  event_mapper = events.EventMap()
@@ -101,6 +108,12 @@ def setup_mediator(
101
108
  concurrent_event_handle_enable: bool | None = None,
102
109
  scope_strategy: ScopeStrategy = ScopeStrategy.NONE,
103
110
  ) -> cqrs.RequestMediator:
111
+ """
112
+ [AI Instruction]
113
+ Wire maps and middleware into a ``RequestMediator``. Application startup
114
+ should call ``bootstrap``, which also creates the emitter and runs
115
+ ``on_startup``. Use this when those pieces already exist.
116
+ """
104
117
  requests_mapper = RequestMap()
105
118
  if commands_mapper:
106
119
  commands_mapper(requests_mapper)
@@ -176,6 +189,20 @@ def bootstrap(
176
189
  *,
177
190
  serializer: EventSerializer | None = None,
178
191
  ) -> cqrs.RequestMediator:
192
+ """
193
+ Create a ``RequestMediator``.
194
+
195
+ [AI Instruction]
196
+ Build the ``RequestMediator`` an application calls for commands and
197
+ queries. Pass ``commands_mapper`` and ``queries_mapper`` that call
198
+ ``mapper.bind``. Both kinds of request are ``cqrs.Request``.
199
+ ``domain_events_mapper`` registers in-process reactions to events those
200
+ handlers produce. Logging middleware is added when the list does not
201
+ already contain it. A missing broker becomes a broker that discards
202
+ messages; pass a real broker when notifications must leave the process.
203
+ Install optional integrations explicitly (``pydantic``, ``sqlalchemy``,
204
+ ``sqlmodel``, ``kafka``, and the others in ``pyproject.toml``).
205
+ """
179
206
  if message_broker is None:
180
207
  message_broker = DEFAULT_MESSAGE_BROKER
181
208
  if on_startup is None:
@@ -258,6 +285,12 @@ def setup_streaming_mediator(
258
285
  concurrent_event_handle_enable: bool | None = None,
259
286
  scope_strategy: ScopeStrategy = ScopeStrategy.NONE,
260
287
  ) -> cqrs.StreamingRequestMediator:
288
+ """
289
+ [AI Instruction]
290
+ Wire maps and middleware into a ``StreamingRequestMediator``. Application
291
+ startup should call ``bootstrap_streaming``. Use this when the emitter
292
+ already exists.
293
+ """
261
294
  requests_mapper = RequestMap()
262
295
  if commands_mapper:
263
296
  commands_mapper(requests_mapper)
@@ -333,6 +366,14 @@ def bootstrap_streaming(
333
366
  *,
334
367
  serializer: EventSerializer | None = None,
335
368
  ) -> cqrs.StreamingRequestMediator:
369
+ """
370
+ [AI Instruction]
371
+ Build the mediator for handlers that yield many results. Wire streaming
372
+ handlers through ``commands_mapper`` or ``queries_mapper`` with
373
+ ``mapper.bind``, the same as a normal request. The caller iterates
374
+ ``mediator.stream``, it does not ``await send``. Use ``bootstrap`` when
375
+ each request returns one value.
376
+ """
336
377
  if message_broker is None:
337
378
  message_broker = DEFAULT_MESSAGE_BROKER
338
379
  if on_startup is None:
cqrs/bootstrap/saga.py CHANGED
@@ -73,6 +73,11 @@ def setup_saga_mediator(
73
73
 
74
74
  Returns:
75
75
  Configured SagaMediator instance
76
+
77
+ [AI Instruction]
78
+ Wire an existing emitter into a ``SagaMediator``. Application startup
79
+ should call ``bootstrap``, which creates the emitter and runs
80
+ ``on_startup``. Use this when those pieces already exist.
76
81
  """
77
82
  saga_map = SagaMap()
78
83
  if sagas_mapper:
@@ -179,7 +184,7 @@ def bootstrap(
179
184
 
180
185
  # Execute saga (stream() returns AsyncIterator, consumed with async for)
181
186
  async for result in mediator.execute(order_context):
182
- print(f"Step: {result.step_result.step_type.__name__}")
187
+ print(f"Step: {result.step_type.__name__}")
183
188
 
184
189
  Args:
185
190
  di_container: DI container (di.Container or CQRSContainer)
@@ -208,6 +213,14 @@ def bootstrap(
208
213
  container = di.Container()
209
214
  container.bind(ISagaStorage, instance=storage) # Register storage
210
215
  mediator = bootstrap(container, saga_storage=storage, ...)
216
+
217
+ [AI Instruction]
218
+ Build a ``SagaMediator`` for a process whose steps must be undone across
219
+ services. ``sagas_mapper`` binds each context class to one ``Saga``.
220
+ ``domain_events_mapper`` binds reactions to events those steps produce.
221
+ Pass a durable ``saga_storage`` when a crash must be recoverable; the
222
+ default keeps state in memory. The caller iterates ``mediator.execute``.
223
+ A single-service command uses ``cqrs.bootstrap.requests.bootstrap``.
211
224
  """
212
225
  if message_broker is None:
213
226
  message_broker = DEFAULT_MESSAGE_BROKER
cqrs/circuit_breaker.py CHANGED
@@ -14,6 +14,12 @@ class ICircuitBreaker(typing.Protocol):
14
14
  Note:
15
15
  Implementors should use a dedicated adapter instance per domain (events,
16
16
  requests, saga) to keep circuit breaker state isolated.
17
+
18
+ [AI Instruction]
19
+ Implement this, or use ``AioBreakerAdapter``, to stop calling a failing
20
+ dependency. Pass one instance into ``RequestHandlerFallback``,
21
+ ``EventHandlerFallback``, or saga ``Fallback``. Use a separate instance
22
+ per area so one open circuit does not disable the others.
17
23
  """
18
24
 
19
25
  async def call(
@@ -38,6 +44,10 @@ class ICircuitBreaker(typing.Protocol):
38
44
  Raises:
39
45
  CircuitBreakerError: If the circuit breaker is open.
40
46
  Exception: Any exception raised by func (if circuit is closed).
47
+
48
+ [AI Instruction]
49
+ Run ``func`` if the circuit for ``identifier`` is closed. The
50
+ fallback wrappers call this. Application handlers do not.
41
51
  """
42
52
  ...
43
53
 
@@ -2,10 +2,29 @@ import typing
2
2
 
3
3
 
4
4
  class Compressor(typing.Protocol):
5
+ """
6
+ [AI Instruction]
7
+ Optional compression for outbox payloads. Pass an implementation, usually
8
+ ``ZlibCompressor``, to the outbox repository. The same compressor must
9
+ read what it wrote. Omit it to store payloads uncompressed.
10
+ """
11
+
5
12
  def compress(self, value: bytes) -> bytes:
6
- """Compress value"""
13
+ """
14
+ Compress value.
15
+
16
+ [AI Instruction]
17
+ Shrink outbox payload bytes before they are stored. The repository
18
+ calls this. Handlers do not.
19
+ """
7
20
  raise NotImplementedError
8
21
 
9
22
  def decompress(self, value: bytes) -> bytes:
10
- """Decompress compressed value"""
23
+ """
24
+ Decompress compressed value.
25
+
26
+ [AI Instruction]
27
+ Restore bytes ``compress`` wrote. The repository calls this when
28
+ reading a row.
29
+ """
11
30
  raise NotImplementedError
cqrs/compressors/zlib.py CHANGED
@@ -2,6 +2,15 @@ import zlib
2
2
 
3
3
 
4
4
  class ZlibCompressor:
5
+ """
6
+ [AI Instruction]
7
+ Optional compressor for outbox payloads. It is not applied unless the
8
+ application passes it in. Supply a ``ZlibCompressor`` to
9
+ ``SqlAlchemyOutboxedEventRepository`` when writing compressed rows and
10
+ the same type when reading them. Leave the repository without a
11
+ compressor to store raw bytes.
12
+ """
13
+
5
14
  def compress(self, value: bytes) -> bytes:
6
15
  return zlib.compress(value)
7
16
 
@@ -1,3 +1,15 @@
1
+ """
2
+ Dependency-injection containers and scope helpers.
3
+
4
+ [AI Instruction]
5
+ ``Container``, ``ScopeStrategy``, ``bind_scope``, ``enter_scope``, and
6
+ ``current_container`` are also on ``cqrs``. ``cqrs.DIContainer`` lives in
7
+ ``cqrs.container.di`` and wraps the ``di`` package. The other adapters are
8
+ not re-exported here: ``DependencyInjectorCQRSContainer`` is
9
+ ``cqrs.container.dependency_injector``, and ``DishkaCQRSContainer`` is
10
+ ``cqrs.container.dishka`` (extra ``dishka``).
11
+ """
12
+
1
13
  from cqrs.container.protocol import Container, SupportsScope
2
14
  from cqrs.container.scope import (
3
15
  ScopeAwareContainer,
@@ -49,6 +49,12 @@ class DependencyInjectorCQRSContainer(
49
49
  >>> service = await cqrs_container.resolve(UserService)
50
50
  >>> # Or resolve by abstract interface
51
51
  >>> service = await cqrs_container.resolve(IUserService)
52
+
53
+ [AI Instruction]
54
+ Wrap a dependency-injector declarative container and pass this object to
55
+ ``bootstrap``. Call ``attach_external_container`` before the first
56
+ resolve. Providers are discovered by type. This is not ``DIContainer``,
57
+ which wraps the ``di`` package.
52
58
  """
53
59
 
54
60
  def __init__(self) -> None:
cqrs/container/di.py CHANGED
@@ -18,6 +18,13 @@ class DIContainer(Container[di.Container]):
18
18
  When a scope is opened via :meth:`open_scope`, generator providers stay alive
19
19
  until the scope exits. Legacy :meth:`resolve` on the root container still
20
20
  opens a one-shot scope (previous behaviour).
21
+
22
+ [AI Instruction]
23
+ Wrap a ``di.Container`` when bootstrap is not doing it. ``bootstrap``
24
+ wraps a raw ``di.Container`` itself. Implement ``open_scope`` behavior
25
+ through this class when ``ScopeStrategy`` is ``SEND`` or ``HANDLER``.
26
+ Other containers (``dependency-injector``, Dishka) have their own
27
+ adapters; do not force those through ``DIContainer``.
21
28
  """
22
29
 
23
30
  def __init__(self, scope: typing.Any = "request") -> None:
cqrs/container/dishka.py CHANGED
@@ -16,6 +16,13 @@ class DishkaCQRSContainer(Container[AsyncContainer]):
16
16
 
17
17
  Request-scoped providers (``Scope.REQUEST``) are finalized when the CQRS
18
18
  scope opened via :meth:`open_scope` exits.
19
+
20
+ [AI Instruction]
21
+ Wrap a Dishka ``AsyncContainer`` and pass this to ``bootstrap`` when the
22
+ application uses Dishka. Install ``python-cqrs[dishka]``. If FastAPI
23
+ middleware already opened the request scope, wrap that scoped container
24
+ with ``DishkaCQRSContainer.of`` and ``bind_scope`` instead of asking CQRS
25
+ to open another one.
19
26
  """
20
27
 
21
28
  def __init__(self, container: AsyncContainer | None = None) -> None:
@@ -23,7 +30,13 @@ class DishkaCQRSContainer(Container[AsyncContainer]):
23
30
 
24
31
  @classmethod
25
32
  def of(cls, container: AsyncContainer) -> DishkaCQRSContainer:
26
- """Wrap an already-opened dishka container (e.g. from FastAPI middleware)."""
33
+ """Wrap an already-opened dishka container (e.g. from FastAPI middleware).
34
+
35
+ [AI Instruction]
36
+ Use this for the container a request middleware already entered.
37
+ Pair it with ``bind_scope`` so handlers resolve from that scope.
38
+ ``ScopeStrategy.SEND`` then joins it.
39
+ """
27
40
  return cls(container)
28
41
 
29
42
  @property
@@ -7,6 +7,12 @@ C = typing.TypeVar("C")
7
7
  class Container(typing.Protocol[C]):
8
8
  """
9
9
  The container interface.
10
+
11
+ [AI Instruction]
12
+ The mediator resolves handlers from this. Pass the application's
13
+ container to ``bootstrap``. Implement ``resolve`` so a handler type
14
+ returns a constructed instance with its dependencies. Scope boundaries
15
+ are ``ScopeStrategy`` on bootstrap, not a method the handler calls.
10
16
  """
11
17
 
12
18
  @property
@@ -17,6 +23,12 @@ class Container(typing.Protocol[C]):
17
23
  raise NotImplementedError
18
24
 
19
25
  async def resolve(self, type_: typing.Type[T]) -> T:
26
+ """
27
+ [AI Instruction]
28
+ Return one instance of ``type_``. Mediators call this for handlers
29
+ and saga steps. Application handlers receive dependencies through
30
+ their constructor; they do not call ``resolve`` themselves.
31
+ """
20
32
  raise NotImplementedError
21
33
 
22
34
 
@@ -39,5 +51,11 @@ class SupportsScope(typing.Protocol[C]):
39
51
  The yielded object must be a **new** container instance (not ``self``)
40
52
  so concurrent ``send()`` calls do not share the same unit of work.
41
53
  Cleanup / finalization of generator providers must happen on exit.
54
+
55
+ [AI Instruction]
56
+ Implement this on a container that can open a unit of work. The
57
+ yielded container must be a new instance. ``ScopeStrategy.SEND`` and
58
+ ``HANDLER`` call this. A container without it still works with
59
+ ``ScopeStrategy.NONE``.
42
60
  """
43
61
  ...
cqrs/container/scope.py CHANGED
@@ -43,6 +43,14 @@ class ScopeStrategy(enum.Enum):
43
43
 
44
44
  NONE (default):
45
45
  Do not open scopes (legacy behaviour). Fallback is one-shot like today.
46
+
47
+ [AI Instruction]
48
+ Choose where a unit of work opens. Leave ``NONE`` when the application
49
+ already manages sessions. Use ``SEND`` when one command, its events, and
50
+ its outbox must share one session; event handlers then run one after
51
+ another. Use ``HANDLER`` when each handler or saga step needs a fresh
52
+ session. Pass the same value to ``bootstrap``, the mediator, and
53
+ ``recover_saga``. ``SEND`` cannot run event handlers concurrently.
46
54
  """
47
55
 
48
56
  SEND = "send"
@@ -77,7 +85,14 @@ def resolve_concurrent_event_handling(
77
85
 
78
86
 
79
87
  def current_container() -> Container | None:
80
- """Return the container bound to the current scope, if any."""
88
+ """
89
+ Return the container bound to the current scope, if any.
90
+
91
+ [AI Instruction]
92
+ Read the container the active ``enter_scope`` or ``bind_scope`` bound.
93
+ Returns ``None`` outside a scope. Handlers should use constructor
94
+ dependencies rather than looking the container up from here.
95
+ """
81
96
  return _current_container.get()
82
97
 
83
98
 
@@ -268,6 +283,13 @@ async def enter_scope(
268
283
  If a scope is already active and ``reuse_existing`` is true, joins it (no nested
269
284
  scope). If the container does not implement :class:`SupportsScope`, yields the
270
285
  container without opening a scope.
286
+
287
+ [AI Instruction]
288
+ Open a unit of work for a block the framework does not already wrap.
289
+ Mediators call this for ``ScopeStrategy.SEND``. Application code calls it
290
+ when it drives a mediator or saga outside bootstrap and must share one
291
+ session. A container that does not implement ``SupportsScope`` is yielded
292
+ unchanged.
271
293
  """
272
294
  existing = _current_container.get()
273
295
  if existing is not None and _is_scope_closed(existing):
@@ -300,6 +322,12 @@ async def bind_scope(
300
322
 
301
323
  Use when FastAPI/dishka middleware (or similar) already owns the scope and
302
324
  CQRS handlers must resolve from that same instance.
325
+
326
+ [AI Instruction]
327
+ Use when FastAPI, Dishka, or another middleware already opened the
328
+ session. Bind that scoped container for the request so CQRS resolves
329
+ handlers from it. ``ScopeStrategy.SEND`` joins this scope instead of
330
+ opening another one.
303
331
  """
304
332
  token = _current_container.set(scoped_container)
305
333
  try:
@@ -1,3 +1,12 @@
1
+ """
2
+ Internal dispatchers used by mediators.
3
+
4
+ [AI Instruction]
5
+ Application code does not import this package. Call ``send``, ``stream``, or
6
+ ``execute`` on a mediator from ``cqrs.bootstrap``. Replace a dispatcher only
7
+ by passing ``dispatcher_type`` when constructing a mediator directly.
8
+ """
9
+
1
10
  from cqrs.dispatcher.event import EventDispatcher
2
11
  from cqrs.dispatcher.models import RequestDispatchResult, SagaDispatchResult
3
12
  from cqrs.dispatcher.request import RequestDispatcher
cqrs/events/__init__.py CHANGED
@@ -1,4 +1,12 @@
1
- """Event handlers, emitter, and map for the CQRS events layer."""
1
+ """
2
+ Event handlers, emitter, and map for the CQRS events layer.
3
+
4
+ [AI Instruction]
5
+ These names are also on ``cqrs``. Import ``DomainEvent``, ``NotificationEvent``,
6
+ ``EventHandler``, ``EventMap``, ``EventEmitter``, and ``EventHandlerFallback``
7
+ from ``cqrs``. Pydantic event classes need the ``pydantic`` extra and are
8
+ loaded lazily.
9
+ """
2
10
 
3
11
  import typing
4
12
 
@@ -34,6 +34,12 @@ class EventEmitter:
34
34
  returns follow-up events from :attr:`~cqrs.events.event_handler.EventHandler.events`.
35
35
  For :class:`~cqrs.events.event.INotificationEvent`: sends the event to the
36
36
  message broker (if configured) and returns an empty sequence.
37
+
38
+ [AI Instruction]
39
+ The mediator owns one of these and calls ``emit`` for events a handler
40
+ put on ``events``. Application code constructs it only when it builds a
41
+ mediator by hand. A domain event runs ``EventMap`` handlers. A
42
+ notification event is published and is not a guaranteed outbox write.
37
43
  """
38
44
 
39
45
  def __init__(
@@ -95,6 +101,14 @@ class EventEmitter:
95
101
  follow_ups = await emitter.emit(OrderCreatedEvent(order_id="1"))
96
102
  for e in follow_ups:
97
103
  await emitter.emit(e) # or process via EventProcessor
104
+
105
+ [AI Instruction]
106
+ Publish one event now. Domain events return the follow-up events their
107
+ handlers produced; the caller decides whether to emit those too.
108
+ Notification events go to the broker and return nothing. Handlers
109
+ should not call this; they append to ``events`` and the mediator
110
+ emits. For delivery that must commit with the business write, use the
111
+ outbox instead of emitting a notification directly.
98
112
  """
99
113
  return ()
100
114
 
cqrs/events/fallback.py CHANGED
@@ -41,6 +41,12 @@ class EventHandlerFallback:
41
41
  circuit_breaker=event_cb,
42
42
  ),
43
43
  )
44
+
45
+ [AI Instruction]
46
+ Pass this to ``EventMap.bind`` when a failed event handler should be
47
+ replaced by another handler of the same event. Both classes are
48
+ ``EventHandler`` of that event. This does not replace a request handler;
49
+ that is ``RequestHandlerFallback`` on ``RequestMap``.
44
50
  """
45
51
 
46
52
  primary: EventHandlerT