python-cq 0.12.2__tar.gz → 0.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_cq-0.13.0/PKG-INFO +70 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/dispatcher/bus.py +13 -13
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/handler.py +16 -12
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/message.py +8 -8
- python_cq-0.13.0/cq/_core/pipetools.py +66 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/middlewares/scope.py +3 -3
- python_cq-0.13.0/docs/index.md +40 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/pyproject.toml +8 -8
- python_cq-0.12.2/PKG-INFO +0 -55
- python_cq-0.12.2/README.md +0 -26
- python_cq-0.12.2/cq/_core/pipetools.py +0 -31
- {python_cq-0.12.2 → python_cq-0.13.0}/.gitignore +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/LICENSE +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/__init__.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/__init__.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/dispatcher/__init__.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/dispatcher/base.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/dispatcher/lazy.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/dispatcher/pipe.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/middleware.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/related_events.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/_core/scope.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/exceptions.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/ext/__init__.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/ext/fastapi.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/middlewares/__init__.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/middlewares/retry.py +0 -0
- {python_cq-0.12.2 → python_cq-0.13.0}/cq/py.typed +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-cq
|
|
3
|
+
Version: 0.13.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: python-injection
|
|
28
|
+
Requires-Dist: type-analyzer
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# python-cq
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/python-cq)
|
|
34
|
+
[](https://pypistats.org/packages/python-cq)
|
|
35
|
+
|
|
36
|
+
**python-cq** is a Python package designed to organize your code following CQRS principles. It builds on top of [python-injection](https://github.com/100nm/python-injection) for dependency injection.
|
|
37
|
+
|
|
38
|
+
## What is CQRS?
|
|
39
|
+
|
|
40
|
+
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read operations from write operations. This separation helps to:
|
|
41
|
+
|
|
42
|
+
- **Clarify intent**: each operation has a single, well-defined responsibility
|
|
43
|
+
- **Improve maintainability**: smaller, focused handlers are easier to understand and modify
|
|
44
|
+
- **Simplify testing**: isolated handlers are straightforward to unit test
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
To get the most out of **python-cq**, familiarity with the following concepts is recommended:
|
|
51
|
+
|
|
52
|
+
- **CQRS** and the distinction between Commands, Queries and Events
|
|
53
|
+
- **Domain Driven Design (DDD)**, particularly aggregates and bounded contexts
|
|
54
|
+
|
|
55
|
+
This knowledge will help you design coherent handlers and organize your code effectively.
|
|
56
|
+
|
|
57
|
+
## Message types
|
|
58
|
+
|
|
59
|
+
**python-cq** provides three types of messages to model your application's operations:
|
|
60
|
+
|
|
61
|
+
- **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.
|
|
62
|
+
- **Query**: represents a request for information. A query is handled by exactly one handler and returns data without side effects.
|
|
63
|
+
- **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.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
Requires Python 3.12 or higher.
|
|
68
|
+
```bash
|
|
69
|
+
pip install python-cq
|
|
70
|
+
```
|
|
@@ -8,9 +8,9 @@ from anyio.abc import TaskGroup
|
|
|
8
8
|
from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
|
|
9
9
|
from cq._core.handler import (
|
|
10
10
|
HandlerFactory,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
HandlerRegistry,
|
|
12
|
+
MultipleHandlerRegistry,
|
|
13
|
+
SingleHandlerRegistry,
|
|
14
14
|
)
|
|
15
15
|
from cq._core.middleware import Middleware
|
|
16
16
|
|
|
@@ -35,29 +35,29 @@ class Bus[I, O](Dispatcher[I, O], Protocol):
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
|
|
38
|
-
__slots__ = ("__listeners", "
|
|
38
|
+
__slots__ = ("__listeners", "__registry")
|
|
39
39
|
|
|
40
40
|
__listeners: list[Listener[I]]
|
|
41
|
-
|
|
41
|
+
__registry: HandlerRegistry[I, O]
|
|
42
42
|
|
|
43
|
-
def __init__(self,
|
|
43
|
+
def __init__(self, registry: HandlerRegistry[I, O], /) -> None:
|
|
44
44
|
super().__init__()
|
|
45
45
|
self.__listeners = []
|
|
46
|
-
self.
|
|
46
|
+
self.__registry = registry
|
|
47
47
|
|
|
48
48
|
def add_listeners(self, *listeners: Listener[I]) -> Self:
|
|
49
49
|
self.__listeners.extend(listeners)
|
|
50
50
|
return self
|
|
51
51
|
|
|
52
52
|
def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
|
|
53
|
-
self.
|
|
53
|
+
self.__registry.subscribe(input_type, factory)
|
|
54
54
|
return self
|
|
55
55
|
|
|
56
56
|
def _handlers_from(
|
|
57
57
|
self,
|
|
58
58
|
input_type: type[I],
|
|
59
59
|
) -> Iterator[Callable[[I], Awaitable[O]]]:
|
|
60
|
-
return self.
|
|
60
|
+
return self.__registry.handlers_from(input_type)
|
|
61
61
|
|
|
62
62
|
def _trigger_listeners(self, input_value: I, /, task_group: TaskGroup) -> None:
|
|
63
63
|
for listener in self.__listeners:
|
|
@@ -67,8 +67,8 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
|
|
|
67
67
|
class SimpleBus[I, O](BaseBus[I, O]):
|
|
68
68
|
__slots__ = ()
|
|
69
69
|
|
|
70
|
-
def __init__(self,
|
|
71
|
-
super().__init__(
|
|
70
|
+
def __init__(self, registry: HandlerRegistry[I, O] | None = None, /) -> None:
|
|
71
|
+
super().__init__(registry or SingleHandlerRegistry())
|
|
72
72
|
|
|
73
73
|
async def dispatch(self, input_value: I, /) -> O:
|
|
74
74
|
async with anyio.create_task_group() as task_group:
|
|
@@ -83,8 +83,8 @@ class SimpleBus[I, O](BaseBus[I, O]):
|
|
|
83
83
|
class TaskBus[I](BaseBus[I, None]):
|
|
84
84
|
__slots__ = ()
|
|
85
85
|
|
|
86
|
-
def __init__(self,
|
|
87
|
-
super().__init__(
|
|
86
|
+
def __init__(self, registry: HandlerRegistry[I, None] | None = None, /) -> None:
|
|
87
|
+
super().__init__(registry or MultipleHandlerRegistry())
|
|
88
88
|
|
|
89
89
|
async def dispatch(self, input_value: I, /) -> None:
|
|
90
90
|
async with anyio.create_task_group() as task_group:
|
|
@@ -24,7 +24,7 @@ class Handler[**P, T](Protocol):
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@runtime_checkable
|
|
27
|
-
class
|
|
27
|
+
class HandlerRegistry[I, O](Protocol):
|
|
28
28
|
__slots__ = ()
|
|
29
29
|
|
|
30
30
|
@abstractmethod
|
|
@@ -40,7 +40,7 @@ class HandlerManager[I, O](Protocol):
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
43
|
-
class
|
|
43
|
+
class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
44
44
|
__factories: dict[type[I], list[HandlerFactory[[I], O]]] = field(
|
|
45
45
|
default_factory=partial(defaultdict, list),
|
|
46
46
|
init=False,
|
|
@@ -62,7 +62,7 @@ class MultipleHandlerManager[I, O](HandlerManager[I, O]):
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
65
|
-
class
|
|
65
|
+
class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
66
66
|
__factories: dict[type[I], HandlerFactory[[I], O]] = field(
|
|
67
67
|
default_factory=dict,
|
|
68
68
|
init=False,
|
|
@@ -90,9 +90,13 @@ class SingleHandlerManager[I, O](HandlerManager[I, O]):
|
|
|
90
90
|
return self
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
class _Decorator(Protocol):
|
|
94
|
+
def __call__[T](self, wrapped: T, /) -> T: ...
|
|
95
|
+
|
|
96
|
+
|
|
93
97
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
94
98
|
class HandlerDecorator[I, O]:
|
|
95
|
-
|
|
99
|
+
registry: HandlerRegistry[I, O]
|
|
96
100
|
injection_module: injection.Module = field(default_factory=injection.mod)
|
|
97
101
|
|
|
98
102
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -104,16 +108,16 @@ class HandlerDecorator[I, O]:
|
|
|
104
108
|
/,
|
|
105
109
|
*,
|
|
106
110
|
threadsafe: bool | None = ...,
|
|
107
|
-
) ->
|
|
111
|
+
) -> _Decorator: ...
|
|
108
112
|
|
|
109
113
|
@overload
|
|
110
|
-
def __call__(
|
|
114
|
+
def __call__[T](
|
|
111
115
|
self,
|
|
112
|
-
input_or_handler_type:
|
|
116
|
+
input_or_handler_type: T,
|
|
113
117
|
/,
|
|
114
118
|
*,
|
|
115
119
|
threadsafe: bool | None = ...,
|
|
116
|
-
) ->
|
|
120
|
+
) -> T: ...
|
|
117
121
|
|
|
118
122
|
@overload
|
|
119
123
|
def __call__(
|
|
@@ -122,11 +126,11 @@ class HandlerDecorator[I, O]:
|
|
|
122
126
|
/,
|
|
123
127
|
*,
|
|
124
128
|
threadsafe: bool | None = ...,
|
|
125
|
-
) ->
|
|
129
|
+
) -> _Decorator: ...
|
|
126
130
|
|
|
127
|
-
def __call__(
|
|
131
|
+
def __call__[T](
|
|
128
132
|
self,
|
|
129
|
-
input_or_handler_type: type[I] |
|
|
133
|
+
input_or_handler_type: type[I] | T | None = None,
|
|
130
134
|
/,
|
|
131
135
|
*,
|
|
132
136
|
threadsafe: bool | None = None,
|
|
@@ -154,7 +158,7 @@ class HandlerDecorator[I, O]:
|
|
|
154
158
|
) -> HandlerType[[I], O]:
|
|
155
159
|
factory = self.injection_module.make_async_factory(wrapped, threadsafe)
|
|
156
160
|
input_type = input_type or _resolve_input_type(wrapped)
|
|
157
|
-
self.
|
|
161
|
+
self.registry.subscribe(input_type, factory)
|
|
158
162
|
return wrapped
|
|
159
163
|
|
|
160
164
|
|
|
@@ -6,8 +6,8 @@ from cq._core.dispatcher.base import Dispatcher
|
|
|
6
6
|
from cq._core.dispatcher.bus import Bus, SimpleBus, TaskBus
|
|
7
7
|
from cq._core.handler import (
|
|
8
8
|
HandlerDecorator,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
MultipleHandlerRegistry,
|
|
10
|
+
SingleHandlerRegistry,
|
|
11
11
|
)
|
|
12
12
|
from cq._core.scope import CQScope
|
|
13
13
|
from cq.middlewares.scope import InjectionScopeMiddleware
|
|
@@ -24,13 +24,13 @@ AnyCommandBus = CommandBus[Any]
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
command_handler: Final[HandlerDecorator[Command, Any]] = HandlerDecorator(
|
|
27
|
-
|
|
27
|
+
SingleHandlerRegistry(),
|
|
28
28
|
)
|
|
29
29
|
event_handler: Final[HandlerDecorator[Event, None]] = HandlerDecorator(
|
|
30
|
-
|
|
30
|
+
MultipleHandlerRegistry(),
|
|
31
31
|
)
|
|
32
32
|
query_handler: Final[HandlerDecorator[Query, Any]] = HandlerDecorator(
|
|
33
|
-
|
|
33
|
+
SingleHandlerRegistry(),
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
@@ -41,7 +41,7 @@ query_handler: Final[HandlerDecorator[Query, Any]] = HandlerDecorator(
|
|
|
41
41
|
mode="fallback",
|
|
42
42
|
)
|
|
43
43
|
def new_command_bus(*, threadsafe: bool | None = None) -> Bus[Command, Any]:
|
|
44
|
-
bus = SimpleBus(command_handler.
|
|
44
|
+
bus = SimpleBus(command_handler.registry)
|
|
45
45
|
transaction_scope_middleware = InjectionScopeMiddleware(
|
|
46
46
|
CQScope.TRANSACTION,
|
|
47
47
|
exist_ok=True,
|
|
@@ -58,7 +58,7 @@ def new_command_bus(*, threadsafe: bool | None = None) -> Bus[Command, Any]:
|
|
|
58
58
|
mode="fallback",
|
|
59
59
|
)
|
|
60
60
|
def new_event_bus() -> Bus[Event, None]:
|
|
61
|
-
return TaskBus(event_handler.
|
|
61
|
+
return TaskBus(event_handler.registry)
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
@injection.injectable(
|
|
@@ -68,4 +68,4 @@ def new_event_bus() -> Bus[Event, None]:
|
|
|
68
68
|
mode="fallback",
|
|
69
69
|
)
|
|
70
70
|
def new_query_bus() -> Bus[Query, Any]:
|
|
71
|
-
return SimpleBus(query_handler.
|
|
71
|
+
return SimpleBus(query_handler.registry)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Callable, overload
|
|
2
|
+
|
|
3
|
+
import injection
|
|
4
|
+
|
|
5
|
+
from cq import Dispatcher
|
|
6
|
+
from cq._core.dispatcher.lazy import LazyDispatcher
|
|
7
|
+
from cq._core.dispatcher.pipe import ContextPipeline, PipeConverterMethod
|
|
8
|
+
from cq._core.message import AnyCommandBus, Command, Query, QueryBus
|
|
9
|
+
from cq._core.scope import CQScope
|
|
10
|
+
from cq.middlewares.scope import InjectionScopeMiddleware
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContextCommandPipeline[I: Command](ContextPipeline[I]):
|
|
14
|
+
__slots__ = ("__query_dispatcher",)
|
|
15
|
+
|
|
16
|
+
__query_dispatcher: Dispatcher[Query, Any]
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
/,
|
|
21
|
+
*,
|
|
22
|
+
injection_module: injection.Module | None = None,
|
|
23
|
+
threadsafe: bool | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
command_dispatcher = LazyDispatcher(
|
|
26
|
+
AnyCommandBus,
|
|
27
|
+
injection_module=injection_module,
|
|
28
|
+
threadsafe=threadsafe,
|
|
29
|
+
)
|
|
30
|
+
super().__init__(command_dispatcher)
|
|
31
|
+
|
|
32
|
+
self.__query_dispatcher = LazyDispatcher(
|
|
33
|
+
QueryBus,
|
|
34
|
+
injection_module=injection_module,
|
|
35
|
+
threadsafe=threadsafe,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
transaction_scope_middleware = InjectionScopeMiddleware(
|
|
39
|
+
CQScope.TRANSACTION,
|
|
40
|
+
exist_ok=True,
|
|
41
|
+
threadsafe=threadsafe,
|
|
42
|
+
)
|
|
43
|
+
self.add_middlewares(transaction_scope_middleware)
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
def query_step[T: Query](
|
|
49
|
+
self,
|
|
50
|
+
wrapped: PipeConverterMethod[T, Any],
|
|
51
|
+
/,
|
|
52
|
+
) -> PipeConverterMethod[T, Any]: ...
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def query_step[T: Query](
|
|
56
|
+
self,
|
|
57
|
+
wrapped: None = ...,
|
|
58
|
+
/,
|
|
59
|
+
) -> Callable[[PipeConverterMethod[T, Any]], PipeConverterMethod[T, Any]]: ...
|
|
60
|
+
|
|
61
|
+
def query_step[T: Query](
|
|
62
|
+
self,
|
|
63
|
+
wrapped: PipeConverterMethod[T, Any] | None = None,
|
|
64
|
+
/,
|
|
65
|
+
) -> Any:
|
|
66
|
+
return self.step(wrapped, dispatcher=self.__query_dispatcher)
|
|
@@ -21,13 +21,13 @@ class InjectionScopeMiddleware:
|
|
|
21
21
|
|
|
22
22
|
async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
|
|
23
23
|
async with AsyncExitStack() as stack:
|
|
24
|
-
cm = adefine_scope(self.scope_name, threadsafe=self.threadsafe)
|
|
25
24
|
try:
|
|
26
|
-
await stack.enter_async_context(
|
|
25
|
+
await stack.enter_async_context(
|
|
26
|
+
adefine_scope(self.scope_name, threadsafe=self.threadsafe)
|
|
27
|
+
)
|
|
27
28
|
|
|
28
29
|
except ScopeAlreadyDefinedError:
|
|
29
30
|
if not self.exist_ok:
|
|
30
31
|
raise
|
|
31
32
|
|
|
32
|
-
del cm
|
|
33
33
|
yield
|
|
@@ -0,0 +1,40 @@
|
|
|
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 builds on top of [python-injection](https://github.com/100nm/python-injection) for dependency injection.
|
|
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
|
+
```bash
|
|
39
|
+
pip install python-cq
|
|
40
|
+
```
|
|
@@ -8,10 +8,9 @@ dev = [
|
|
|
8
8
|
"mypy",
|
|
9
9
|
"ruff",
|
|
10
10
|
]
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"pydantic",
|
|
11
|
+
docs = [
|
|
12
|
+
"mkdocs",
|
|
13
|
+
"mkdocs-material",
|
|
15
14
|
]
|
|
16
15
|
test = [
|
|
17
16
|
"fastapi",
|
|
@@ -23,11 +22,11 @@ test = [
|
|
|
23
22
|
|
|
24
23
|
[project]
|
|
25
24
|
name = "python-cq"
|
|
26
|
-
version = "0.
|
|
27
|
-
description = "
|
|
25
|
+
version = "0.13.0"
|
|
26
|
+
description = "CQRS library for async Python projects."
|
|
28
27
|
license = "MIT"
|
|
29
28
|
license-files = ["LICENSE"]
|
|
30
|
-
readme = "
|
|
29
|
+
readme = "docs/index.md"
|
|
31
30
|
requires-python = ">=3.12, <3.15"
|
|
32
31
|
authors = [{ name = "remimd" }]
|
|
33
32
|
keywords = ["cqrs"]
|
|
@@ -54,6 +53,7 @@ dependencies = [
|
|
|
54
53
|
]
|
|
55
54
|
|
|
56
55
|
[project.urls]
|
|
56
|
+
Documentation = "https://python-cq.remimd.dev"
|
|
57
57
|
Repository = "https://github.com/100nm/python-cq"
|
|
58
58
|
|
|
59
59
|
[tool.coverage.report]
|
|
@@ -104,5 +104,5 @@ extend-select = ["F", "I", "N"]
|
|
|
104
104
|
fixable = ["ALL"]
|
|
105
105
|
|
|
106
106
|
[tool.uv]
|
|
107
|
-
default-groups = ["dev", "
|
|
107
|
+
default-groups = ["dev", "docs", "test"]
|
|
108
108
|
package = true
|
python_cq-0.12.2/PKG-INFO
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-cq
|
|
3
|
-
Version: 0.12.2
|
|
4
|
-
Summary: Lightweight CQRS library for async Python projects.
|
|
5
|
-
Project-URL: Repository, https://github.com/100nm/python-cq
|
|
6
|
-
Author: remimd
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Keywords: cqrs
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Natural Language :: English
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
-
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: <3.15,>=3.12
|
|
25
|
-
Requires-Dist: anyio
|
|
26
|
-
Requires-Dist: python-injection
|
|
27
|
-
Requires-Dist: type-analyzer
|
|
28
|
-
Description-Content-Type: text/markdown
|
|
29
|
-
|
|
30
|
-
# python-cq
|
|
31
|
-
|
|
32
|
-
[](https://github.com/100nm/python-cq)
|
|
33
|
-
[](https://pypi.org/project/python-cq)
|
|
34
|
-
[](https://pypistats.org/packages/python-cq)
|
|
35
|
-
[](https://github.com/astral-sh/ruff)
|
|
36
|
-
|
|
37
|
-
Lightweight library for separating Python code according to **Command and Query Responsibility Segregation** principles.
|
|
38
|
-
|
|
39
|
-
Dependency injection is handled by [python-injection](https://github.com/100nm/python-injection).
|
|
40
|
-
|
|
41
|
-
Easy to use with [FastAPI](https://github.com/fastapi/fastapi).
|
|
42
|
-
|
|
43
|
-
## Installation
|
|
44
|
-
|
|
45
|
-
⚠️ _Requires Python 3.12 or higher_
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install python-cq
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Resources
|
|
52
|
-
|
|
53
|
-
* [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
|
|
54
|
-
* [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
|
|
55
|
-
* [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
|
python_cq-0.12.2/README.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# python-cq
|
|
2
|
-
|
|
3
|
-
[](https://github.com/100nm/python-cq)
|
|
4
|
-
[](https://pypi.org/project/python-cq)
|
|
5
|
-
[](https://pypistats.org/packages/python-cq)
|
|
6
|
-
[](https://github.com/astral-sh/ruff)
|
|
7
|
-
|
|
8
|
-
Lightweight library for separating Python code according to **Command and Query Responsibility Segregation** principles.
|
|
9
|
-
|
|
10
|
-
Dependency injection is handled by [python-injection](https://github.com/100nm/python-injection).
|
|
11
|
-
|
|
12
|
-
Easy to use with [FastAPI](https://github.com/fastapi/fastapi).
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
⚠️ _Requires Python 3.12 or higher_
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pip install python-cq
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Resources
|
|
23
|
-
|
|
24
|
-
* [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
|
|
25
|
-
* [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
|
|
26
|
-
* [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import injection
|
|
2
|
-
|
|
3
|
-
from cq._core.dispatcher.lazy import LazyDispatcher
|
|
4
|
-
from cq._core.dispatcher.pipe import ContextPipeline
|
|
5
|
-
from cq._core.message import AnyCommandBus, Command
|
|
6
|
-
from cq._core.scope import CQScope
|
|
7
|
-
from cq.middlewares.scope import InjectionScopeMiddleware
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ContextCommandPipeline[I: Command](ContextPipeline[I]):
|
|
11
|
-
__slots__ = ()
|
|
12
|
-
|
|
13
|
-
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
/,
|
|
16
|
-
*,
|
|
17
|
-
injection_module: injection.Module | None = None,
|
|
18
|
-
threadsafe: bool | None = None,
|
|
19
|
-
) -> None:
|
|
20
|
-
dispatcher = LazyDispatcher(
|
|
21
|
-
AnyCommandBus,
|
|
22
|
-
injection_module=injection_module,
|
|
23
|
-
threadsafe=threadsafe,
|
|
24
|
-
)
|
|
25
|
-
super().__init__(dispatcher)
|
|
26
|
-
transaction_scope_middleware = InjectionScopeMiddleware(
|
|
27
|
-
CQScope.TRANSACTION,
|
|
28
|
-
exist_ok=True,
|
|
29
|
-
threadsafe=threadsafe,
|
|
30
|
-
)
|
|
31
|
-
self.add_middlewares(transaction_scope_middleware)
|
|
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
|
|
File without changes
|
|
File without changes
|