python-cq 0.17.0__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.
- python_cq-0.18.0/PKG-INFO +110 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/__init__.py +11 -3
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/cq.py +1 -1
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/di.py +1 -1
- python_cq-0.17.0/cq/_core/dispatcher/base.py → python_cq-0.18.0/cq/_core/dispatchers/abc.py +5 -5
- {python_cq-0.17.0/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/bus.py +16 -16
- {python_cq-0.17.0/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/lazy.py +3 -3
- {python_cq-0.17.0/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/pipe.py +32 -36
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/handler.py +32 -32
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/message.py +1 -1
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/pipetools.py +2 -2
- python_cq-0.18.0/cq/_core/pump.py +34 -0
- python_cq-0.18.0/cq/_core/queues/abc.py +29 -0
- python_cq-0.18.0/cq/_core/queues/memory.py +42 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/related_events.py +5 -6
- python_cq-0.18.0/cq/py.typed +0 -0
- python_cq-0.18.0/docs/index.md +79 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/pyproject.toml +1 -1
- python_cq-0.17.0/PKG-INFO +0 -78
- python_cq-0.17.0/docs/index.md +0 -47
- {python_cq-0.17.0 → python_cq-0.18.0}/.gitignore +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/LICENSE +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/__init__.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/common/__init__.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/common/typing.py +0 -0
- {python_cq-0.17.0/cq/_core/dispatcher → python_cq-0.18.0/cq/_core/dispatchers}/__init__.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/middleware.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/middlewares/__init__.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/_core/middlewares/scope.py +0 -0
- {python_cq-0.17.0/cq/ext → python_cq-0.18.0/cq/_core/queues}/__init__.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/exceptions.py +0 -0
- {python_cq-0.17.0/cq/middlewares → python_cq-0.18.0/cq/ext}/__init__.py +0 -0
- {python_cq-0.17.0 → python_cq-0.18.0}/cq/ext/injection.py +0 -0
- /python_cq-0.17.0/cq/py.typed → /python_cq-0.18.0/cq/middlewares/__init__.py +0 -0
- {python_cq-0.17.0 → 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
|
+
[](https://pypi.org/project/python-cq)
|
|
35
|
+
[](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.
|
|
5
|
-
from ._core.
|
|
6
|
-
from ._core.
|
|
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",
|
|
@@ -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
|
|
|
@@ -9,11 +9,11 @@ from cq._core.middleware import Middleware, MiddlewareGroup
|
|
|
9
9
|
class Dispatcher[I, O](Protocol):
|
|
10
10
|
__slots__ = ()
|
|
11
11
|
|
|
12
|
-
async def __call__(self,
|
|
13
|
-
return await self.dispatch(
|
|
12
|
+
async def __call__(self, message: I, /) -> O:
|
|
13
|
+
return await self.dispatch(message)
|
|
14
14
|
|
|
15
15
|
@abstractmethod
|
|
16
|
-
async def dispatch(self,
|
|
16
|
+
async def dispatch(self, message: I, /) -> O:
|
|
17
17
|
raise NotImplementedError
|
|
18
18
|
|
|
19
19
|
|
|
@@ -32,12 +32,12 @@ class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
|
|
|
32
32
|
async def _invoke_with_middlewares(
|
|
33
33
|
self,
|
|
34
34
|
handler: Callable[[I], Awaitable[O]],
|
|
35
|
-
|
|
35
|
+
message: I,
|
|
36
36
|
/,
|
|
37
37
|
fail_silently: bool = False,
|
|
38
38
|
) -> O:
|
|
39
39
|
try:
|
|
40
|
-
return await self.__middleware_group.invoke(handler,
|
|
40
|
+
return await self.__middleware_group.invoke(handler, message)
|
|
41
41
|
except Exception:
|
|
42
42
|
if fail_silently:
|
|
43
43
|
return NotImplemented
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
message_type: type[I],
|
|
61
61
|
factory: HandlerFactory[[I], O],
|
|
62
62
|
fail_silently: bool = False,
|
|
63
63
|
) -> Self:
|
|
64
|
-
self.__registry.subscribe(
|
|
64
|
+
self.__registry.subscribe(message_type, factory, fail_silently=fail_silently)
|
|
65
65
|
return self
|
|
66
66
|
|
|
67
|
-
def _handlers_from(self,
|
|
68
|
-
return self.__registry.handlers_from(
|
|
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,
|
|
70
|
+
def _trigger_listeners(self, message: I, /, task_group: TaskGroup) -> None:
|
|
71
71
|
for listener in self.__listeners:
|
|
72
|
-
task_group.start_soon(listener,
|
|
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,
|
|
81
|
+
async def dispatch(self, message: I, /) -> O:
|
|
82
82
|
async with anyio.create_task_group() as task_group:
|
|
83
|
-
self._trigger_listeners(
|
|
83
|
+
self._trigger_listeners(message, task_group)
|
|
84
84
|
|
|
85
|
-
for handler in self._handlers_from(type(
|
|
85
|
+
for handler in self._handlers_from(type(message)):
|
|
86
86
|
return await self._invoke_with_middlewares(
|
|
87
87
|
handler,
|
|
88
|
-
|
|
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,
|
|
101
|
+
async def dispatch(self, message: I, /) -> None:
|
|
102
102
|
async with anyio.create_task_group() as task_group:
|
|
103
|
-
self._trigger_listeners(
|
|
103
|
+
self._trigger_listeners(message, task_group)
|
|
104
104
|
|
|
105
|
-
for handler in self._handlers_from(type(
|
|
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
|
-
|
|
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.
|
|
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,
|
|
22
|
+
async def dispatch(self, message: I, /) -> O:
|
|
23
23
|
dispatcher = await self.__resolve()
|
|
24
|
-
return await dispatcher.dispatch(
|
|
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.
|
|
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,
|
|
34
|
+
async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
|
|
35
35
|
raise NotImplementedError
|
|
36
36
|
|
|
37
37
|
|
|
@@ -54,28 +54,24 @@ class PipelineSteps[**P, I, O]:
|
|
|
54
54
|
self.__steps.append(PipelineStep(converter, dispatcher))
|
|
55
55
|
return self
|
|
56
56
|
|
|
57
|
-
def add_static[T](
|
|
58
|
-
|
|
59
|
-
input_value: T,
|
|
60
|
-
dispatcher: Dispatcher[T, Any] | None,
|
|
61
|
-
) -> Self:
|
|
62
|
-
converter = _StaticPipelineConverter(input_value)
|
|
57
|
+
def add_static[T](self, message: T, dispatcher: Dispatcher[T, Any] | None) -> Self:
|
|
58
|
+
converter = _StaticPipelineConverter(message)
|
|
63
59
|
self.add(converter, dispatcher) # type: ignore[arg-type]
|
|
64
60
|
return self
|
|
65
61
|
|
|
66
|
-
async def execute(self,
|
|
62
|
+
async def execute(self, message: I, /, *args: P.args, **kwargs: P.kwargs) -> O:
|
|
67
63
|
dispatcher = self.default_dispatcher
|
|
68
64
|
|
|
69
65
|
for step in self.__steps:
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
result = await dispatcher.dispatch(message)
|
|
67
|
+
message = await step.converter.convert(result, *args, **kwargs)
|
|
72
68
|
|
|
73
|
-
if
|
|
69
|
+
if message is None:
|
|
74
70
|
return NotImplemented
|
|
75
71
|
|
|
76
72
|
dispatcher = step.dispatcher or self.default_dispatcher
|
|
77
73
|
|
|
78
|
-
return await dispatcher.dispatch(
|
|
74
|
+
return await dispatcher.dispatch(message)
|
|
79
75
|
|
|
80
76
|
|
|
81
77
|
class Pipe[I, O](BaseDispatcher[I, O]):
|
|
@@ -136,15 +132,15 @@ class Pipe[I, O](BaseDispatcher[I, O]):
|
|
|
136
132
|
|
|
137
133
|
def add_static_step[T](
|
|
138
134
|
self,
|
|
139
|
-
|
|
135
|
+
message: T,
|
|
140
136
|
/,
|
|
141
137
|
dispatcher: Dispatcher[T, Any] | None = None,
|
|
142
138
|
) -> Self:
|
|
143
|
-
self.__steps.add_static(
|
|
139
|
+
self.__steps.add_static(message, dispatcher)
|
|
144
140
|
return self
|
|
145
141
|
|
|
146
|
-
async def dispatch(self,
|
|
147
|
-
return await self._invoke_with_middlewares(self.__steps.execute,
|
|
142
|
+
async def dispatch(self, message: I, /) -> O:
|
|
143
|
+
return await self._invoke_with_middlewares(self.__steps.execute, message)
|
|
148
144
|
|
|
149
145
|
|
|
150
146
|
class ContextPipeline[I]:
|
|
@@ -199,11 +195,11 @@ class ContextPipeline[I]:
|
|
|
199
195
|
|
|
200
196
|
def add_static_step[T](
|
|
201
197
|
self,
|
|
202
|
-
|
|
198
|
+
message: T,
|
|
203
199
|
/,
|
|
204
200
|
dispatcher: Dispatcher[T, Any] | None = None,
|
|
205
201
|
) -> Self:
|
|
206
|
-
self.__steps.add_static(
|
|
202
|
+
self.__steps.add_static(message, dispatcher)
|
|
207
203
|
return self
|
|
208
204
|
|
|
209
205
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -255,49 +251,49 @@ class ContextPipeline[I]:
|
|
|
255
251
|
|
|
256
252
|
async def __execute[Context](
|
|
257
253
|
self,
|
|
258
|
-
|
|
254
|
+
message: I,
|
|
259
255
|
/,
|
|
260
256
|
*,
|
|
261
257
|
context: Context,
|
|
262
258
|
context_type: type[Context] | None,
|
|
263
259
|
) -> Context:
|
|
264
|
-
async def handler(
|
|
265
|
-
await self.__steps.execute(
|
|
260
|
+
async def handler(first_message: I, /) -> Context:
|
|
261
|
+
await self.__steps.execute(first_message, context, context_type)
|
|
266
262
|
return context
|
|
267
263
|
|
|
268
|
-
return await self.__middleware_group.invoke(handler,
|
|
264
|
+
return await self.__middleware_group.invoke(handler, message)
|
|
269
265
|
|
|
270
266
|
|
|
271
267
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
272
268
|
class BoundContextPipeline[I, O](Dispatcher[I, O]):
|
|
273
269
|
dispatch_method: Callable[[I], Awaitable[O]]
|
|
274
270
|
|
|
275
|
-
async def dispatch(self,
|
|
276
|
-
return await self.dispatch_method(
|
|
271
|
+
async def dispatch(self, message: I, /) -> O:
|
|
272
|
+
return await self.dispatch_method(message)
|
|
277
273
|
|
|
278
274
|
|
|
279
275
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
280
276
|
class _AsyncPipelineConverter[**P, I, O](PipelineConverter[P, I, O]):
|
|
281
277
|
converter: ConvertAsync[P, I, O]
|
|
282
278
|
|
|
283
|
-
async def convert(self,
|
|
284
|
-
return await self.converter(
|
|
279
|
+
async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
|
|
280
|
+
return await self.converter(result, *args, **kwargs)
|
|
285
281
|
|
|
286
282
|
|
|
287
283
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
288
284
|
class _SyncPipelineConverter[**P, I, O](PipelineConverter[P, I, O]):
|
|
289
285
|
converter: ConvertSync[P, I, O]
|
|
290
286
|
|
|
291
|
-
async def convert(self,
|
|
292
|
-
return self.converter(
|
|
287
|
+
async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I:
|
|
288
|
+
return self.converter(result, *args, **kwargs)
|
|
293
289
|
|
|
294
290
|
|
|
295
291
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
296
292
|
class _StaticPipelineConverter[I](PipelineConverter[..., I, Any]):
|
|
297
|
-
|
|
293
|
+
message: I
|
|
298
294
|
|
|
299
|
-
async def convert(self,
|
|
300
|
-
return self.
|
|
295
|
+
async def convert(self, result: Any, /, *args: Any, **kwargs: Any) -> I:
|
|
296
|
+
return self.message
|
|
301
297
|
|
|
302
298
|
|
|
303
299
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
@@ -308,13 +304,13 @@ class _AsyncContextPipelineConverter[I, O](
|
|
|
308
304
|
|
|
309
305
|
async def convert(
|
|
310
306
|
self,
|
|
311
|
-
|
|
307
|
+
result: O,
|
|
312
308
|
/,
|
|
313
309
|
context: object,
|
|
314
310
|
context_type: type | None,
|
|
315
311
|
) -> I:
|
|
316
312
|
method = self.converter.__get__(context, context_type)
|
|
317
|
-
return await method(
|
|
313
|
+
return await method(result)
|
|
318
314
|
|
|
319
315
|
|
|
320
316
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
@@ -325,10 +321,10 @@ class _SyncContextPipelineConverter[I, O](
|
|
|
325
321
|
|
|
326
322
|
async def convert(
|
|
327
323
|
self,
|
|
328
|
-
|
|
324
|
+
result: O,
|
|
329
325
|
/,
|
|
330
326
|
context: object,
|
|
331
327
|
context_type: type | None,
|
|
332
328
|
) -> I:
|
|
333
329
|
method = self.converter.__get__(context, context_type)
|
|
334
|
-
return method(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
75
|
-
for key_type in _iter_key_types(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
101
|
-
for key_type in _iter_key_types(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
and isclass(
|
|
170
|
-
and issubclass(
|
|
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
|
-
|
|
173
|
+
message_or_handler_type,
|
|
174
174
|
fail_silently=fail_silently,
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
return partial(
|
|
178
178
|
self.__decorator,
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
self.registry.subscribe(
|
|
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(
|
|
197
|
+
def _build_key_types(message_type: Any) -> tuple[Any, ...]:
|
|
198
198
|
config = MatchingTypesConfig(ignore_none=True)
|
|
199
|
-
return matching_types(
|
|
199
|
+
return matching_types(message_type, config)
|
|
200
200
|
|
|
201
201
|
|
|
202
|
-
def _iter_key_types(
|
|
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(
|
|
208
|
+
return iter_matching_types(message_type, config)
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
def
|
|
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
|
-
|
|
216
|
+
message_type = parameter.annotation
|
|
217
217
|
|
|
218
|
-
if
|
|
218
|
+
if message_type is Parameter.empty:
|
|
219
219
|
break
|
|
220
220
|
|
|
221
|
-
return
|
|
221
|
+
return message_type
|
|
222
222
|
|
|
223
223
|
raise TypeError(
|
|
224
|
-
f"Unable to resolve
|
|
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
|
)
|
|
@@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Any, Self, overload
|
|
|
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.
|
|
7
|
-
from cq._core.
|
|
6
|
+
from cq._core.dispatchers.lazy import LazyDispatcher
|
|
7
|
+
from cq._core.dispatchers.pipe import (
|
|
8
8
|
ContextPipeline,
|
|
9
9
|
ConvertMethod,
|
|
10
10
|
ConvertMethodAsync,
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
46
|
+
self.task_group.start_soon(self.emit, event)
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# python-cq
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/python-cq)
|
|
4
|
+
[](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.
|
python_cq-0.17.0/PKG-INFO
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-cq
|
|
3
|
-
Version: 0.17.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
|
-
[](https://pypi.org/project/python-cq)
|
|
35
|
-
[](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
|
-
```
|
python_cq-0.17.0/docs/index.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# python-cq
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/python-cq)
|
|
4
|
-
[](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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|