python-cq 0.16.1__tar.gz → 0.18.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 (35) hide show
  1. python_cq-0.18.0/PKG-INFO +110 -0
  2. {python_cq-0.16.1 → python_cq-0.18.0}/cq/__init__.py +11 -3
  3. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/cq.py +1 -1
  4. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/di.py +1 -1
  5. python_cq-0.16.1/cq/_core/dispatcher/base.py → python_cq-0.18.0/cq/_core/dispatchers/abc.py +10 -8
  6. {python_cq-0.16.1/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/bus.py +16 -16
  7. {python_cq-0.16.1/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/lazy.py +3 -3
  8. {python_cq-0.16.1/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/pipe.py +43 -30
  9. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/handler.py +32 -32
  10. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/message.py +1 -1
  11. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/pipetools.py +6 -3
  12. python_cq-0.18.0/cq/_core/pump.py +34 -0
  13. python_cq-0.18.0/cq/_core/queues/abc.py +29 -0
  14. python_cq-0.18.0/cq/_core/queues/memory.py +42 -0
  15. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/related_events.py +5 -6
  16. python_cq-0.18.0/cq/py.typed +0 -0
  17. python_cq-0.18.0/docs/index.md +79 -0
  18. {python_cq-0.16.1 → python_cq-0.18.0}/pyproject.toml +1 -1
  19. python_cq-0.16.1/PKG-INFO +0 -78
  20. python_cq-0.16.1/docs/index.md +0 -47
  21. {python_cq-0.16.1 → python_cq-0.18.0}/.gitignore +0 -0
  22. {python_cq-0.16.1 → python_cq-0.18.0}/LICENSE +0 -0
  23. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/__init__.py +0 -0
  24. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/common/__init__.py +0 -0
  25. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/common/typing.py +0 -0
  26. {python_cq-0.16.1/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/__init__.py +0 -0
  27. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/middleware.py +0 -0
  28. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/middlewares/__init__.py +0 -0
  29. {python_cq-0.16.1 → python_cq-0.18.0}/cq/_core/middlewares/scope.py +0 -0
  30. {python_cq-0.16.1/cq/ext → python_cq-0.18.0/cq/_core/queues}/__init__.py +0 -0
  31. {python_cq-0.16.1 → python_cq-0.18.0}/cq/exceptions.py +0 -0
  32. {python_cq-0.16.1/cq/middlewares → python_cq-0.18.0/cq/ext}/__init__.py +0 -0
  33. {python_cq-0.16.1 → python_cq-0.18.0}/cq/ext/injection.py +0 -0
  34. /python_cq-0.16.1/cq/py.typed → /python_cq-0.18.0/cq/middlewares/__init__.py +0 -0
  35. {python_cq-0.16.1 → python_cq-0.18.0}/cq/middlewares/retry.py +0 -0
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-cq
3
+ Version: 0.18.0
4
+ Summary: CQRS library for async Python projects.
5
+ Project-URL: Documentation, https://python-cq.remimd.dev
6
+ Project-URL: Repository, https://github.com/100nm/python-cq
7
+ Author: remimd
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: cqrs
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: <3.15,>=3.12
26
+ Requires-Dist: anyio
27
+ Requires-Dist: type-analyzer
28
+ Provides-Extra: injection
29
+ Requires-Dist: python-injection[async]; extra == 'injection'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # python-cq
33
+
34
+ [![PyPI - Version](https://img.shields.io/pypi/v/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypi.org/project/python-cq)
35
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypistats.org/packages/python-cq)
36
+
37
+ **python-cq** is an async-first Python library for organizing code around CQRS. It separates reads (queries), writes (commands), and notifications (events) into dedicated message buses, and lets you plug in any dependency injection framework behind a small protocol.
38
+
39
+ ## What is CQRS?
40
+
41
+ CQRS (Command Query Responsibility Segregation) splits read operations from write operations. Each operation has a single, well-defined responsibility, which:
42
+
43
+ * **clarifies intent**: a `CreateUserCommand` does one thing, and its name says so;
44
+ * **keeps handlers small**: one message, one handler, easy to test in isolation;
45
+ * **makes side effects explicit**: events fan out to subscribers without coupling the producer to them.
46
+
47
+ CQRS is often discussed alongside distributed systems and Event Sourcing, but the pattern is just as useful in a local or monolithic application. The boundaries it draws are valuable on their own.
48
+
49
+ ## Three message types
50
+
51
+ | Type | Intent | Handlers | Returns |
52
+ |-----------|---------------------------------------|-----------------------|--------------------------|
53
+ | `Command` | Change the state of the system | Exactly one | The handler's return value |
54
+ | `Query` | Read state without side effects | Exactly one | The handler's return value |
55
+ | `Event` | Notify that something has happened | Zero, one, or many | Nothing |
56
+
57
+ A `Command` is allowed to return a value (for convenience, typically an id or a result object), but that does not mean it should be used as a query. Keep intent clear.
58
+
59
+ ## Installation
60
+
61
+ Requires Python 3.12 or higher.
62
+
63
+ With the default DI backend ([python-injection](https://github.com/100nm/python-injection), recommended):
64
+
65
+ ```bash
66
+ pip install "python-cq[injection]"
67
+ ```
68
+
69
+ Without dependency injection (you will need to implement a `DIAdapter`):
70
+
71
+ ```bash
72
+ pip install python-cq
73
+ ```
74
+
75
+ ## Quickstart
76
+
77
+ ```python
78
+ import asyncio
79
+ from cq import CommandBus, command_handler
80
+ from dataclasses import dataclass
81
+ from injection import inject
82
+
83
+ @dataclass
84
+ class CreateUserCommand:
85
+ name: str
86
+ email: str
87
+
88
+ @command_handler
89
+ class CreateUserHandler:
90
+ async def handle(self, command: CreateUserCommand) -> int:
91
+ # ... persist the user, return its id
92
+ return 42
93
+
94
+ @inject
95
+ async def main(bus: CommandBus[int]) -> None:
96
+ command = CreateUserCommand(name="Ada", email="ada@example.com")
97
+ user_id = await bus.dispatch(command)
98
+ print(f"Created user {user_id}")
99
+
100
+ asyncio.run(main())
101
+ ```
102
+
103
+ The decorator registers the handler against the type of its first `handle` parameter. The bus is resolved by the DI container and dispatched to that handler.
104
+
105
+ ## Prerequisites
106
+
107
+ Familiarity with the following helps you get the most out of python-cq:
108
+
109
+ * **CQRS**, in particular the distinction between Commands, Queries, and Events.
110
+ * **Domain Driven Design (DDD)**, particularly aggregates and bounded contexts, which complement CQRS well.
@@ -1,9 +1,9 @@
1
1
  from ._core.cq import CQ
2
2
  from ._core.di import DIAdapter
3
3
  from ._core.di import NoDI as _NoDI
4
- from ._core.dispatcher.base import Dispatcher
5
- from ._core.dispatcher.bus import Bus
6
- from ._core.dispatcher.pipe import ContextPipeline, Pipe
4
+ from ._core.dispatchers.abc import Dispatcher
5
+ from ._core.dispatchers.bus import Bus
6
+ from ._core.dispatchers.pipe import ContextPipeline, Pipe
7
7
  from ._core.message import (
8
8
  AnyCommandBus,
9
9
  Command,
@@ -15,6 +15,9 @@ from ._core.message import (
15
15
  )
16
16
  from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source
17
17
  from ._core.pipetools import ContextCommandPipeline as _ContextCommandPipeline
18
+ from ._core.pump import Pump
19
+ from ._core.queues.abc import Consumer, Producer, Queue
20
+ from ._core.queues.memory import MemoryQueue
18
21
  from ._core.related_events import AnyIORelatedEvents, RelatedEvents
19
22
 
20
23
  __all__ = (
@@ -24,17 +27,22 @@ __all__ = (
24
27
  "CQ",
25
28
  "Command",
26
29
  "CommandBus",
30
+ "Consumer",
27
31
  "ContextCommandPipeline",
28
32
  "ContextPipeline",
29
33
  "DIAdapter",
30
34
  "Dispatcher",
31
35
  "Event",
32
36
  "EventBus",
37
+ "MemoryQueue",
33
38
  "Middleware",
34
39
  "MiddlewareResult",
35
40
  "Pipe",
41
+ "Producer",
42
+ "Pump",
36
43
  "Query",
37
44
  "QueryBus",
45
+ "Queue",
38
46
  "RelatedEvents",
39
47
  "command_handler",
40
48
  "event_handler",
@@ -1,7 +1,7 @@
1
1
  from typing import Any, Self
2
2
 
3
3
  from cq._core.di import DIAdapter
4
- from cq._core.dispatcher.bus import Bus, SimpleBus, TaskBus
4
+ from cq._core.dispatchers.bus import Bus, SimpleBus, TaskBus
5
5
  from cq._core.handler import (
6
6
  HandlerDecorator,
7
7
  HandlerRegistry,
@@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
5
5
  from contextlib import nullcontext
6
6
  from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol, runtime_checkable
7
7
 
8
- if TYPE_CHECKING:
8
+ if TYPE_CHECKING: # pragma: no cover
9
9
  from cq import CommandBus, EventBus, QueryBus
10
10
 
11
11
 
@@ -1,6 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Awaitable, Callable
3
- from contextlib import AsyncExitStack, suppress
4
3
  from typing import Protocol, Self, runtime_checkable
5
4
 
6
5
  from cq._core.middleware import Middleware, MiddlewareGroup
@@ -10,8 +9,11 @@ from cq._core.middleware import Middleware, MiddlewareGroup
10
9
  class Dispatcher[I, O](Protocol):
11
10
  __slots__ = ()
12
11
 
12
+ async def __call__(self, message: I, /) -> O:
13
+ return await self.dispatch(message)
14
+
13
15
  @abstractmethod
14
- async def dispatch(self, input_value: I, /) -> O:
16
+ async def dispatch(self, message: I, /) -> O:
15
17
  raise NotImplementedError
16
18
 
17
19
 
@@ -30,14 +32,14 @@ class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
30
32
  async def _invoke_with_middlewares(
31
33
  self,
32
34
  handler: Callable[[I], Awaitable[O]],
33
- input_value: I,
35
+ message: I,
34
36
  /,
35
37
  fail_silently: bool = False,
36
38
  ) -> O:
37
- async with AsyncExitStack() as stack:
39
+ try:
40
+ return await self.__middleware_group.invoke(handler, message)
41
+ except Exception:
38
42
  if fail_silently:
39
- stack.enter_context(suppress(Exception))
40
-
41
- return await self.__middleware_group.invoke(handler, input_value)
43
+ return NotImplemented
42
44
 
43
- return NotImplemented
45
+ raise
@@ -5,7 +5,7 @@ from typing import Any, Protocol, Self, runtime_checkable
5
5
  import anyio
6
6
  from anyio.abc import TaskGroup
7
7
 
8
- from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
8
+ from cq._core.dispatchers.abc import BaseDispatcher, Dispatcher
9
9
  from cq._core.handler import (
10
10
  HandleFunction,
11
11
  HandlerFactory,
@@ -33,7 +33,7 @@ class Bus[I, O](Dispatcher[I, O], Protocol):
33
33
  @abstractmethod
34
34
  def subscribe(
35
35
  self,
36
- input_type: type[I],
36
+ message_type: type[I],
37
37
  factory: HandlerFactory[[I], O],
38
38
  fail_silently: bool = ...,
39
39
  ) -> Self:
@@ -57,19 +57,19 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
57
57
 
58
58
  def subscribe(
59
59
  self,
60
- input_type: type[I],
60
+ message_type: type[I],
61
61
  factory: HandlerFactory[[I], O],
62
62
  fail_silently: bool = False,
63
63
  ) -> Self:
64
- self.__registry.subscribe(input_type, factory, fail_silently=fail_silently)
64
+ self.__registry.subscribe(message_type, factory, fail_silently=fail_silently)
65
65
  return self
66
66
 
67
- def _handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
68
- return self.__registry.handlers_from(input_type)
67
+ def _handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
68
+ return self.__registry.handlers_from(message_type)
69
69
 
70
- def _trigger_listeners(self, input_value: I, /, task_group: TaskGroup) -> None:
70
+ def _trigger_listeners(self, message: I, /, task_group: TaskGroup) -> None:
71
71
  for listener in self.__listeners:
72
- task_group.start_soon(listener, input_value)
72
+ task_group.start_soon(listener, message)
73
73
 
74
74
 
75
75
  class SimpleBus[I, O](BaseBus[I, O]):
@@ -78,14 +78,14 @@ class SimpleBus[I, O](BaseBus[I, O]):
78
78
  def __init__(self, registry: HandlerRegistry[I, O] | None = None, /) -> None:
79
79
  super().__init__(registry or SingleHandlerRegistry())
80
80
 
81
- async def dispatch(self, input_value: I, /) -> O:
81
+ async def dispatch(self, message: I, /) -> O:
82
82
  async with anyio.create_task_group() as task_group:
83
- self._trigger_listeners(input_value, task_group)
83
+ self._trigger_listeners(message, task_group)
84
84
 
85
- for handler in self._handlers_from(type(input_value)):
85
+ for handler in self._handlers_from(type(message)):
86
86
  return await self._invoke_with_middlewares(
87
87
  handler,
88
- input_value,
88
+ message,
89
89
  handler.fail_silently,
90
90
  )
91
91
 
@@ -98,14 +98,14 @@ class TaskBus[I](BaseBus[I, None]):
98
98
  def __init__(self, registry: HandlerRegistry[I, None] | None = None, /) -> None:
99
99
  super().__init__(registry or MultipleHandlerRegistry())
100
100
 
101
- async def dispatch(self, input_value: I, /) -> None:
101
+ async def dispatch(self, message: I, /) -> None:
102
102
  async with anyio.create_task_group() as task_group:
103
- self._trigger_listeners(input_value, task_group)
103
+ self._trigger_listeners(message, task_group)
104
104
 
105
- for handler in self._handlers_from(type(input_value)):
105
+ for handler in self._handlers_from(type(message)):
106
106
  task_group.start_soon(
107
107
  self._invoke_with_middlewares,
108
108
  handler,
109
- input_value,
109
+ message,
110
110
  handler.fail_silently,
111
111
  )
@@ -3,7 +3,7 @@ from types import GenericAlias
3
3
  from typing import TypeAliasType
4
4
 
5
5
  from cq._core.di import DIAdapter
6
- from cq._core.dispatcher.base import Dispatcher
6
+ from cq._core.dispatchers.abc import Dispatcher
7
7
 
8
8
 
9
9
  class LazyDispatcher[I, O](Dispatcher[I, O]):
@@ -19,6 +19,6 @@ class LazyDispatcher[I, O](Dispatcher[I, O]):
19
19
  ) -> None:
20
20
  self.__resolve = di.lazy(dispatcher_type) # type: ignore[arg-type]
21
21
 
22
- async def dispatch(self, input_value: I, /) -> O:
22
+ async def dispatch(self, message: I, /) -> O:
23
23
  dispatcher = await self.__resolve()
24
- return await dispatcher.dispatch(input_value)
24
+ return await dispatcher.dispatch(message)
@@ -14,7 +14,7 @@ from typing import (
14
14
  )
15
15
 
16
16
  from cq._core.common.typing import Decorator, Method
17
- from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
17
+ from cq._core.dispatchers.abc import BaseDispatcher, Dispatcher
18
18
  from cq._core.middleware import Middleware, MiddlewareGroup
19
19
 
20
20
  type ConvertAsync[**P, I, O] = Callable[Concatenate[O, P], Awaitable[I]]
@@ -31,7 +31,7 @@ class PipelineConverter[**P, I, O](Protocol):
31
31
  __slots__ = ()
32
32
 
33
33
  @abstractmethod
34
- async def convert(self, output_value: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
34
+ async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
35
35
  raise NotImplementedError
36
36
 
37
37
 
@@ -54,19 +54,24 @@ class PipelineSteps[**P, I, O]:
54
54
  self.__steps.append(PipelineStep(converter, dispatcher))
55
55
  return self
56
56
 
57
- async def execute(self, input_value: I, /, *args: P.args, **kwargs: P.kwargs) -> O:
57
+ def add_static[T](self, message: T, dispatcher: Dispatcher[T, Any] | None) -> Self:
58
+ converter = _StaticPipelineConverter(message)
59
+ self.add(converter, dispatcher) # type: ignore[arg-type]
60
+ return self
61
+
62
+ async def execute(self, message: I, /, *args: P.args, **kwargs: P.kwargs) -> O:
58
63
  dispatcher = self.default_dispatcher
59
64
 
60
65
  for step in self.__steps:
61
- output_value = await dispatcher.dispatch(input_value)
62
- input_value = await step.converter.convert(output_value, *args, **kwargs)
66
+ result = await dispatcher.dispatch(message)
67
+ message = await step.converter.convert(result, *args, **kwargs)
63
68
 
64
- if input_value is None:
69
+ if message is None:
65
70
  return NotImplemented
66
71
 
67
72
  dispatcher = step.dispatcher or self.default_dispatcher
68
73
 
69
- return await dispatcher.dispatch(input_value)
74
+ return await dispatcher.dispatch(message)
70
75
 
71
76
 
72
77
  class Pipe[I, O](BaseDispatcher[I, O]):
@@ -127,16 +132,15 @@ class Pipe[I, O](BaseDispatcher[I, O]):
127
132
 
128
133
  def add_static_step[T](
129
134
  self,
130
- input_value: T,
131
- *,
135
+ message: T,
136
+ /,
132
137
  dispatcher: Dispatcher[T, Any] | None = None,
133
138
  ) -> Self:
134
- converter = _StaticPipelineConverter(input_value)
135
- self.__steps.add(converter, dispatcher)
139
+ self.__steps.add_static(message, dispatcher)
136
140
  return self
137
141
 
138
- async def dispatch(self, input_value: I, /) -> O:
139
- return await self._invoke_with_middlewares(self.__steps.execute, input_value)
142
+ async def dispatch(self, message: I, /) -> O:
143
+ return await self._invoke_with_middlewares(self.__steps.execute, message)
140
144
 
141
145
 
142
146
  class ContextPipeline[I]:
@@ -189,6 +193,15 @@ class ContextPipeline[I]:
189
193
  self.__middleware_group.add(*middlewares)
190
194
  return self
191
195
 
196
+ def add_static_step[T](
197
+ self,
198
+ message: T,
199
+ /,
200
+ dispatcher: Dispatcher[T, Any] | None = None,
201
+ ) -> Self:
202
+ self.__steps.add_static(message, dispatcher)
203
+ return self
204
+
192
205
  if TYPE_CHECKING: # pragma: no cover
193
206
 
194
207
  @overload
@@ -238,49 +251,49 @@ class ContextPipeline[I]:
238
251
 
239
252
  async def __execute[Context](
240
253
  self,
241
- input_value: I,
254
+ message: I,
242
255
  /,
243
256
  *,
244
257
  context: Context,
245
258
  context_type: type[Context] | None,
246
259
  ) -> Context:
247
- async def handler(i: I, /) -> Context:
248
- await self.__steps.execute(i, context, context_type)
260
+ async def handler(first_message: I, /) -> Context:
261
+ await self.__steps.execute(first_message, context, context_type)
249
262
  return context
250
263
 
251
- return await self.__middleware_group.invoke(handler, input_value)
264
+ return await self.__middleware_group.invoke(handler, message)
252
265
 
253
266
 
254
267
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
255
268
  class BoundContextPipeline[I, O](Dispatcher[I, O]):
256
269
  dispatch_method: Callable[[I], Awaitable[O]]
257
270
 
258
- async def dispatch(self, input_value: I, /) -> O:
259
- return await self.dispatch_method(input_value)
271
+ async def dispatch(self, message: I, /) -> O:
272
+ return await self.dispatch_method(message)
260
273
 
261
274
 
262
275
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
263
276
  class _AsyncPipelineConverter[**P, I, O](PipelineConverter[P, I, O]):
264
277
  converter: ConvertAsync[P, I, O]
265
278
 
266
- async def convert(self, output_value: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
267
- return await self.converter(output_value, *args, **kwargs)
279
+ async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
280
+ return await self.converter(result, *args, **kwargs)
268
281
 
269
282
 
270
283
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
271
284
  class _SyncPipelineConverter[**P, I, O](PipelineConverter[P, I, O]):
272
285
  converter: ConvertSync[P, I, O]
273
286
 
274
- async def convert(self, output_value: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
275
- return self.converter(output_value, *args, **kwargs)
287
+ async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
288
+ return self.converter(result, *args, **kwargs)
276
289
 
277
290
 
278
291
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
279
292
  class _StaticPipelineConverter[I](PipelineConverter[..., I, Any]):
280
- input_value: I
293
+ message: I
281
294
 
282
- async def convert(self, output_value: Any, /, *args: Any, **kwargs: Any) -> I:
283
- return self.input_value
295
+ async def convert(self, result: Any, /, *args: Any, **kwargs: Any) -> I:
296
+ return self.message
284
297
 
285
298
 
286
299
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -291,13 +304,13 @@ class _AsyncContextPipelineConverter[I, O](
291
304
 
292
305
  async def convert(
293
306
  self,
294
- output_value: O,
307
+ result: O,
295
308
  /,
296
309
  context: object,
297
310
  context_type: type | None,
298
311
  ) -> I:
299
312
  method = self.converter.__get__(context, context_type)
300
- return await method(output_value)
313
+ return await method(result)
301
314
 
302
315
 
303
316
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -308,10 +321,10 @@ class _SyncContextPipelineConverter[I, O](
308
321
 
309
322
  async def convert(
310
323
  self,
311
- output_value: O,
324
+ result: O,
312
325
  /,
313
326
  context: object,
314
327
  context_type: type | None,
315
328
  ) -> I:
316
329
  method = self.converter.__get__(context, context_type)
317
- return method(output_value)
330
+ return method(result)
@@ -50,13 +50,13 @@ class HandlerRegistry[I, O](Protocol):
50
50
  __slots__ = ()
51
51
 
52
52
  @abstractmethod
53
- def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
53
+ def handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
54
54
  raise NotImplementedError
55
55
 
56
56
  @abstractmethod
57
57
  def subscribe(
58
58
  self,
59
- input_type: type[I],
59
+ message_type: type[I],
60
60
  handler_factory: HandlerFactory[[I], O],
61
61
  handler_type: HandlerType[[I], O] | None = ...,
62
62
  fail_silently: bool = ...,
@@ -71,20 +71,20 @@ class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
71
71
  init=False,
72
72
  )
73
73
 
74
- def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
75
- for key_type in _iter_key_types(input_type):
74
+ def handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
75
+ for key_type in _iter_key_types(message_type):
76
76
  yield from self.__values.get(key_type, ())
77
77
 
78
78
  def subscribe(
79
79
  self,
80
- input_type: type[I],
80
+ message_type: type[I],
81
81
  handler_factory: HandlerFactory[[I], O],
82
82
  handler_type: HandlerType[[I], O] | None = None,
83
83
  fail_silently: bool = False,
84
84
  ) -> Self:
85
85
  function = HandleFunction.create(handler_factory, handler_type, fail_silently)
86
86
 
87
- for key_type in _build_key_types(input_type):
87
+ for key_type in _build_key_types(message_type):
88
88
  self.__values[key_type].append(function)
89
89
 
90
90
  return self
@@ -97,26 +97,26 @@ class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
97
97
  init=False,
98
98
  )
99
99
 
100
- def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
101
- for key_type in _iter_key_types(input_type):
100
+ def handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
101
+ for key_type in _iter_key_types(message_type):
102
102
  function = self.__values.get(key_type, None)
103
103
  if function is not None:
104
104
  yield function
105
105
 
106
106
  def subscribe(
107
107
  self,
108
- input_type: type[I],
108
+ message_type: type[I],
109
109
  handler_factory: HandlerFactory[[I], O],
110
110
  handler_type: HandlerType[[I], O] | None = None,
111
111
  fail_silently: bool = False,
112
112
  ) -> Self:
113
113
  function = HandleFunction.create(handler_factory, handler_type, fail_silently)
114
- entries = {key_type: function for key_type in _build_key_types(input_type)}
114
+ entries = {key_type: function for key_type in _build_key_types(message_type)}
115
115
 
116
116
  for key_type in entries:
117
117
  if key_type in self.__values:
118
118
  raise RuntimeError(
119
- f"A handler is already registered for the input type: `{key_type}`."
119
+ f"A handler is already registered for the message type: `{key_type}`."
120
120
  )
121
121
 
122
122
  self.__values.update(entries)
@@ -133,7 +133,7 @@ class HandlerDecorator[I, O]:
133
133
  @overload
134
134
  def __call__(
135
135
  self,
136
- input_or_handler_type: type[I],
136
+ message_or_handler_type: type[I],
137
137
  /,
138
138
  *,
139
139
  fail_silently: bool = ...,
@@ -142,7 +142,7 @@ class HandlerDecorator[I, O]:
142
142
  @overload
143
143
  def __call__[T](
144
144
  self,
145
- input_or_handler_type: T,
145
+ message_or_handler_type: T,
146
146
  /,
147
147
  *,
148
148
  fail_silently: bool = ...,
@@ -151,7 +151,7 @@ class HandlerDecorator[I, O]:
151
151
  @overload
152
152
  def __call__(
153
153
  self,
154
- input_or_handler_type: None = ...,
154
+ message_or_handler_type: None = ...,
155
155
  /,
156
156
  *,
157
157
  fail_silently: bool = ...,
@@ -159,24 +159,24 @@ class HandlerDecorator[I, O]:
159
159
 
160
160
  def __call__[T](
161
161
  self,
162
- input_or_handler_type: type[I] | T | None = None,
162
+ message_or_handler_type: type[I] | T | None = None,
163
163
  /,
164
164
  *,
165
165
  fail_silently: bool = False,
166
166
  ) -> Any:
167
167
  if (
168
- input_or_handler_type is not None
169
- and isclass(input_or_handler_type)
170
- and issubclass(input_or_handler_type, Handler)
168
+ message_or_handler_type is not None
169
+ and isclass(message_or_handler_type)
170
+ and issubclass(message_or_handler_type, Handler)
171
171
  ):
172
172
  return self.__decorator(
173
- input_or_handler_type,
173
+ message_or_handler_type,
174
174
  fail_silently=fail_silently,
175
175
  )
176
176
 
177
177
  return partial(
178
178
  self.__decorator,
179
- input_type=input_or_handler_type, # type: ignore[arg-type]
179
+ message_type=message_or_handler_type, # type: ignore[arg-type]
180
180
  fail_silently=fail_silently,
181
181
  )
182
182
 
@@ -185,42 +185,42 @@ class HandlerDecorator[I, O]:
185
185
  wrapped: HandlerType[[I], O],
186
186
  /,
187
187
  *,
188
- input_type: type[I] | None = None,
188
+ message_type: type[I] | None = None,
189
189
  fail_silently: bool = False,
190
190
  ) -> HandlerType[[I], O]:
191
191
  factory = self.di.wire(wrapped)
192
- input_type = input_type or _resolve_input_type(wrapped)
193
- self.registry.subscribe(input_type, factory, wrapped, fail_silently)
192
+ message_type = message_type or _resolve_message_type(wrapped)
193
+ self.registry.subscribe(message_type, factory, wrapped, fail_silently)
194
194
  return wrapped
195
195
 
196
196
 
197
- def _build_key_types(input_type: Any) -> tuple[Any, ...]:
197
+ def _build_key_types(message_type: Any) -> tuple[Any, ...]:
198
198
  config = MatchingTypesConfig(ignore_none=True)
199
- return matching_types(input_type, config)
199
+ return matching_types(message_type, config)
200
200
 
201
201
 
202
- def _iter_key_types(input_type: Any) -> Iterator[Any]:
202
+ def _iter_key_types(message_type: Any) -> Iterator[Any]:
203
203
  config = MatchingTypesConfig(
204
204
  with_bases=True,
205
205
  with_origin=True,
206
206
  with_type_alias_value=True,
207
207
  )
208
- return iter_matching_types(input_type, config)
208
+ return iter_matching_types(message_type, config)
209
209
 
210
210
 
211
- def _resolve_input_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]:
211
+ def _resolve_message_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]:
212
212
  fake_method = handler_type.handle.__get__(NotImplemented, handler_type)
213
213
  signature = inspect_signature(fake_method, eval_str=True)
214
214
 
215
215
  for parameter in signature.parameters.values():
216
- input_type = parameter.annotation
216
+ message_type = parameter.annotation
217
217
 
218
- if input_type is Parameter.empty:
218
+ if message_type is Parameter.empty:
219
219
  break
220
220
 
221
- return input_type
221
+ return message_type
222
222
 
223
223
  raise TypeError(
224
- f"Unable to resolve input type for handler `{handler_type}`, "
224
+ f"Unable to resolve message type for handler `{handler_type}`, "
225
225
  "`handle` method must have a type annotation for its first parameter."
226
226
  )
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from cq._core.dispatcher.base import Dispatcher
3
+ from cq._core.dispatchers.abc import Dispatcher
4
4
 
5
5
  Command = object
6
6
  Event = object
@@ -1,10 +1,10 @@
1
- from typing import TYPE_CHECKING, Any, overload
1
+ from typing import TYPE_CHECKING, Any, Self, overload
2
2
 
3
3
  from cq import Dispatcher
4
4
  from cq._core.common.typing import Decorator
5
5
  from cq._core.di import DIAdapter
6
- from cq._core.dispatcher.lazy import LazyDispatcher
7
- from cq._core.dispatcher.pipe import (
6
+ from cq._core.dispatchers.lazy import LazyDispatcher
7
+ from cq._core.dispatchers.pipe import (
8
8
  ContextPipeline,
9
9
  ConvertMethod,
10
10
  ConvertMethodAsync,
@@ -25,6 +25,9 @@ class ContextCommandPipeline[C: Command](ContextPipeline[C]):
25
25
  command_middleware = CommandDispatchScopeMiddleware(di)
26
26
  self.add_middlewares(command_middleware)
27
27
 
28
+ def add_static_query_step[Q: Query](self, query: Q, /) -> Self:
29
+ return self.add_static_step(query, dispatcher=self.__query_dispatcher)
30
+
28
31
  if TYPE_CHECKING: # pragma: no cover
29
32
 
30
33
  @overload
@@ -0,0 +1,34 @@
1
+ from collections.abc import AsyncIterator, Awaitable, Callable
2
+ from contextlib import asynccontextmanager
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ import anyio
7
+
8
+ from cq._core.queues.abc import Consumer
9
+
10
+
11
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
12
+ class Pump[T]:
13
+ consumer: Consumer[T]
14
+ dispatcher: Callable[[T], Awaitable[Any]]
15
+ fail_silently: bool = field(default=False)
16
+
17
+ async def drain(self) -> None:
18
+ async for message in self.consumer:
19
+ try:
20
+ await self.dispatcher(message)
21
+ except Exception:
22
+ if not self.fail_silently:
23
+ raise
24
+
25
+ @asynccontextmanager
26
+ async def draining(self, /, *, graceful: bool = False) -> AsyncIterator[None]:
27
+ async with anyio.create_task_group() as task_group:
28
+ task_group.start_soon(self.drain)
29
+
30
+ try:
31
+ yield
32
+ finally:
33
+ if not graceful:
34
+ task_group.cancel_scope.cancel()
@@ -0,0 +1,29 @@
1
+ from abc import abstractmethod
2
+ from collections.abc import AsyncIterator
3
+ from typing import Protocol, runtime_checkable
4
+
5
+
6
+ @runtime_checkable
7
+ class Producer[T](Protocol):
8
+ __slots__ = ()
9
+
10
+ async def __call__(self, message: T, /) -> None:
11
+ return await self.send(message)
12
+
13
+ @abstractmethod
14
+ async def send(self, message: T, /) -> None:
15
+ raise NotImplementedError
16
+
17
+
18
+ @runtime_checkable
19
+ class Consumer[T](Protocol):
20
+ __slots__ = ()
21
+
22
+ @abstractmethod
23
+ def __aiter__(self) -> AsyncIterator[T]:
24
+ raise NotImplementedError
25
+
26
+
27
+ @runtime_checkable
28
+ class Queue[T](Producer[T], Consumer[T], Protocol):
29
+ __slots__ = ()
@@ -0,0 +1,42 @@
1
+ from collections.abc import AsyncIterator, Awaitable, Callable
2
+ from contextlib import asynccontextmanager
3
+ from typing import Any, Self
4
+
5
+ import anyio
6
+ from anyio.abc import ObjectReceiveStream, ObjectSendStream
7
+
8
+ from cq._core.pump import Pump
9
+ from cq._core.queues.abc import Queue
10
+
11
+
12
+ class MemoryQueue[T](Queue[T]):
13
+ __slots__ = ("__consumer", "__producer")
14
+
15
+ __consumer: ObjectReceiveStream[T]
16
+ __producer: ObjectSendStream[T]
17
+
18
+ def __init__(self, maxsize: int = 0) -> None:
19
+ self.__producer, self.__consumer = anyio.create_memory_object_stream(maxsize)
20
+
21
+ def __aiter__(self) -> AsyncIterator[T]:
22
+ return aiter(self.__consumer)
23
+
24
+ async def close(self) -> None:
25
+ await self.__producer.aclose()
26
+
27
+ @asynccontextmanager
28
+ async def draining(
29
+ self,
30
+ dispatcher: Callable[[T], Awaitable[Any]],
31
+ /,
32
+ *,
33
+ fail_silently: bool = False,
34
+ ) -> AsyncIterator[Self]:
35
+ async with Pump(self, dispatcher, fail_silently).draining(graceful=True):
36
+ try:
37
+ yield self
38
+ finally:
39
+ await self.close()
40
+
41
+ async def send(self, message: T, /) -> None:
42
+ await self.__producer.send(message)
@@ -1,12 +1,13 @@
1
1
  from abc import abstractmethod
2
+ from collections.abc import Awaitable, Callable
2
3
  from dataclasses import dataclass, field
3
4
  from types import TracebackType
4
- from typing import Protocol, Self, runtime_checkable
5
+ from typing import Any, Protocol, Self, runtime_checkable
5
6
 
6
7
  from anyio import create_task_group
7
8
  from anyio.abc import TaskGroup
8
9
 
9
- from cq._core.message import Event, EventBus
10
+ from cq._core.message import Event
10
11
 
11
12
 
12
13
  @runtime_checkable
@@ -20,7 +21,7 @@ class RelatedEvents(Protocol):
20
21
 
21
22
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
22
23
  class AnyIORelatedEvents(RelatedEvents):
23
- event_bus: EventBus
24
+ emit: Callable[[Event], Awaitable[Any]]
24
25
  task_group: TaskGroup = field(default_factory=create_task_group)
25
26
  history: list[Event] = field(default_factory=list, init=False)
26
27
 
@@ -41,7 +42,5 @@ class AnyIORelatedEvents(RelatedEvents):
41
42
 
42
43
  def add(self, *events: Event) -> None:
43
44
  self.history.extend(events)
44
- dispatch_method = self.event_bus.dispatch
45
-
46
45
  for event in events:
47
- self.task_group.start_soon(dispatch_method, event)
46
+ self.task_group.start_soon(self.emit, event)
File without changes
@@ -0,0 +1,79 @@
1
+ # python-cq
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypi.org/project/python-cq)
4
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypistats.org/packages/python-cq)
5
+
6
+ **python-cq** is an async-first Python library for organizing code around CQRS. It separates reads (queries), writes (commands), and notifications (events) into dedicated message buses, and lets you plug in any dependency injection framework behind a small protocol.
7
+
8
+ ## What is CQRS?
9
+
10
+ CQRS (Command Query Responsibility Segregation) splits read operations from write operations. Each operation has a single, well-defined responsibility, which:
11
+
12
+ * **clarifies intent**: a `CreateUserCommand` does one thing, and its name says so;
13
+ * **keeps handlers small**: one message, one handler, easy to test in isolation;
14
+ * **makes side effects explicit**: events fan out to subscribers without coupling the producer to them.
15
+
16
+ CQRS is often discussed alongside distributed systems and Event Sourcing, but the pattern is just as useful in a local or monolithic application. The boundaries it draws are valuable on their own.
17
+
18
+ ## Three message types
19
+
20
+ | Type | Intent | Handlers | Returns |
21
+ |-----------|---------------------------------------|-----------------------|--------------------------|
22
+ | `Command` | Change the state of the system | Exactly one | The handler's return value |
23
+ | `Query` | Read state without side effects | Exactly one | The handler's return value |
24
+ | `Event` | Notify that something has happened | Zero, one, or many | Nothing |
25
+
26
+ A `Command` is allowed to return a value (for convenience, typically an id or a result object), but that does not mean it should be used as a query. Keep intent clear.
27
+
28
+ ## Installation
29
+
30
+ Requires Python 3.12 or higher.
31
+
32
+ With the default DI backend ([python-injection](https://github.com/100nm/python-injection), recommended):
33
+
34
+ ```bash
35
+ pip install "python-cq[injection]"
36
+ ```
37
+
38
+ Without dependency injection (you will need to implement a `DIAdapter`):
39
+
40
+ ```bash
41
+ pip install python-cq
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ import asyncio
48
+ from cq import CommandBus, command_handler
49
+ from dataclasses import dataclass
50
+ from injection import inject
51
+
52
+ @dataclass
53
+ class CreateUserCommand:
54
+ name: str
55
+ email: str
56
+
57
+ @command_handler
58
+ class CreateUserHandler:
59
+ async def handle(self, command: CreateUserCommand) -> int:
60
+ # ... persist the user, return its id
61
+ return 42
62
+
63
+ @inject
64
+ async def main(bus: CommandBus[int]) -> None:
65
+ command = CreateUserCommand(name="Ada", email="ada@example.com")
66
+ user_id = await bus.dispatch(command)
67
+ print(f"Created user {user_id}")
68
+
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ The decorator registers the handler against the type of its first `handle` parameter. The bus is resolved by the DI container and dispatched to that handler.
73
+
74
+ ## Prerequisites
75
+
76
+ Familiarity with the following helps you get the most out of python-cq:
77
+
78
+ * **CQRS**, in particular the distinction between Commands, Queries, and Events.
79
+ * **Domain Driven Design (DDD)**, particularly aggregates and bounded contexts, which complement CQRS well.
@@ -20,7 +20,7 @@ test = [
20
20
 
21
21
  [project]
22
22
  name = "python-cq"
23
- version = "0.16.1"
23
+ version = "0.18.0"
24
24
  description = "CQRS library for async Python projects."
25
25
  license = "MIT"
26
26
  license-files = ["LICENSE"]
python_cq-0.16.1/PKG-INFO DELETED
@@ -1,78 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-cq
3
- Version: 0.16.1
4
- Summary: CQRS library for async Python projects.
5
- Project-URL: Documentation, https://python-cq.remimd.dev
6
- Project-URL: Repository, https://github.com/100nm/python-cq
7
- Author: remimd
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Keywords: cqrs
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Natural Language :: English
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Programming Language :: Python
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Programming Language :: Python :: 3.14
21
- Classifier: Topic :: Software Development :: Libraries
22
- Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
23
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Classifier: Typing :: Typed
25
- Requires-Python: <3.15,>=3.12
26
- Requires-Dist: anyio
27
- Requires-Dist: type-analyzer
28
- Provides-Extra: injection
29
- Requires-Dist: python-injection[async]; extra == 'injection'
30
- Description-Content-Type: text/markdown
31
-
32
- # python-cq
33
-
34
- [![PyPI - Version](https://img.shields.io/pypi/v/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypi.org/project/python-cq)
35
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypistats.org/packages/python-cq)
36
-
37
- **python-cq** is a Python package designed to organize your code following CQRS principles. It provides a `DIAdapter` protocol for dependency injection, with [python-injection](https://github.com/100nm/python-injection) as the default implementation available via the `[injection]` extra.
38
-
39
- ## What is CQRS?
40
-
41
- CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read operations from write operations. This separation helps to:
42
-
43
- - **Clarify intent**: each operation has a single, well-defined responsibility
44
- - **Improve maintainability**: smaller, focused handlers are easier to understand and modify
45
- - **Simplify testing**: isolated handlers are straightforward to unit test
46
-
47
- CQRS is often associated with distributed systems and Event Sourcing, but its benefits extend beyond that. Even in a local or monolithic application, adopting this pattern helps structure your code and makes the boundaries between reading and writing explicit.
48
-
49
- ## Prerequisites
50
-
51
- To get the most out of **python-cq**, familiarity with the following concepts is recommended:
52
-
53
- - **CQRS** and the distinction between Commands, Queries and Events
54
- - **Domain Driven Design (DDD)**, particularly aggregates and bounded contexts
55
-
56
- This knowledge will help you design coherent handlers and organize your code effectively.
57
-
58
- ## Message types
59
-
60
- **python-cq** provides three types of messages to model your application's operations:
61
-
62
- - **Command**: represents an intent to change the system's state. A command is handled by exactly one handler and may return a value for convenience.
63
- - **Query**: represents a request for information. A query is handled by exactly one handler and returns data without side effects.
64
- - **Event**: represents something that has happened in the system. An event can be handled by zero, one, or many handlers, enabling loose coupling between components.
65
-
66
- ## Installation
67
-
68
- Requires Python 3.12 or higher.
69
-
70
- Without dependency injection:
71
- ```bash
72
- pip install python-cq
73
- ```
74
-
75
- With [python-injection](https://github.com/100nm/python-injection) as the DI backend (recommended):
76
- ```bash
77
- pip install "python-cq[injection]"
78
- ```
@@ -1,47 +0,0 @@
1
- # python-cq
2
-
3
- [![PyPI - Version](https://img.shields.io/pypi/v/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypi.org/project/python-cq)
4
- [![PyPI - Downloads](https://img.shields.io/pypi/dm/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypistats.org/packages/python-cq)
5
-
6
- **python-cq** is a Python package designed to organize your code following CQRS principles. It provides a `DIAdapter` protocol for dependency injection, with [python-injection](https://github.com/100nm/python-injection) as the default implementation available via the `[injection]` extra.
7
-
8
- ## What is CQRS?
9
-
10
- CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read operations from write operations. This separation helps to:
11
-
12
- - **Clarify intent**: each operation has a single, well-defined responsibility
13
- - **Improve maintainability**: smaller, focused handlers are easier to understand and modify
14
- - **Simplify testing**: isolated handlers are straightforward to unit test
15
-
16
- CQRS is often associated with distributed systems and Event Sourcing, but its benefits extend beyond that. Even in a local or monolithic application, adopting this pattern helps structure your code and makes the boundaries between reading and writing explicit.
17
-
18
- ## Prerequisites
19
-
20
- To get the most out of **python-cq**, familiarity with the following concepts is recommended:
21
-
22
- - **CQRS** and the distinction between Commands, Queries and Events
23
- - **Domain Driven Design (DDD)**, particularly aggregates and bounded contexts
24
-
25
- This knowledge will help you design coherent handlers and organize your code effectively.
26
-
27
- ## Message types
28
-
29
- **python-cq** provides three types of messages to model your application's operations:
30
-
31
- - **Command**: represents an intent to change the system's state. A command is handled by exactly one handler and may return a value for convenience.
32
- - **Query**: represents a request for information. A query is handled by exactly one handler and returns data without side effects.
33
- - **Event**: represents something that has happened in the system. An event can be handled by zero, one, or many handlers, enabling loose coupling between components.
34
-
35
- ## Installation
36
-
37
- Requires Python 3.12 or higher.
38
-
39
- Without dependency injection:
40
- ```bash
41
- pip install python-cq
42
- ```
43
-
44
- With [python-injection](https://github.com/100nm/python-injection) as the DI backend (recommended):
45
- ```bash
46
- pip install "python-cq[injection]"
47
- ```
File without changes
File without changes
File without changes