python-cq 0.15.2__tar.gz → 0.16.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.15.2 → python_cq-0.16.0}/PKG-INFO +11 -3
- python_cq-0.16.0/cq/__init__.py +75 -0
- python_cq-0.16.0/cq/_core/cq.py +63 -0
- python_cq-0.16.0/cq/_core/di.py +113 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/dispatcher/base.py +0 -9
- python_cq-0.16.0/cq/_core/dispatcher/lazy.py +24 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/handler.py +3 -10
- python_cq-0.16.0/cq/_core/message.py +13 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/middleware.py +2 -1
- python_cq-0.16.0/cq/_core/middlewares/scope.py +14 -0
- python_cq-0.16.0/cq/_core/pipetools.py +52 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/related_events.py +16 -12
- python_cq-0.16.0/cq/ext/injection.py +102 -0
- python_cq-0.16.0/cq/py.typed +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/docs/index.md +8 -1
- {python_cq-0.15.2 → python_cq-0.16.0}/pyproject.toml +6 -4
- python_cq-0.15.2/cq/__init__.py +0 -51
- python_cq-0.15.2/cq/_core/dispatcher/lazy.py +0 -28
- python_cq-0.15.2/cq/_core/message.py +0 -71
- python_cq-0.15.2/cq/_core/pipetools.py +0 -75
- python_cq-0.15.2/cq/_core/scope.py +0 -5
- python_cq-0.15.2/cq/ext/fastapi.py +0 -73
- python_cq-0.15.2/cq/middlewares/scope.py +0 -33
- {python_cq-0.15.2 → python_cq-0.16.0}/.gitignore +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/LICENSE +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/__init__.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/common/__init__.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/common/typing.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/dispatcher/__init__.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/dispatcher/bus.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/_core/dispatcher/pipe.py +0 -0
- {python_cq-0.15.2/cq/ext → python_cq-0.16.0/cq/_core/middlewares}/__init__.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/exceptions.py +0 -0
- {python_cq-0.15.2/cq/middlewares → python_cq-0.16.0/cq/ext}/__init__.py +0 -0
- /python_cq-0.15.2/cq/py.typed → /python_cq-0.16.0/cq/middlewares/__init__.py +0 -0
- {python_cq-0.15.2 → python_cq-0.16.0}/cq/middlewares/retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-cq
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: CQRS library for async Python projects.
|
|
5
5
|
Project-URL: Documentation, https://python-cq.remimd.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/100nm/python-cq
|
|
@@ -24,8 +24,9 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
24
24
|
Classifier: Typing :: Typed
|
|
25
25
|
Requires-Python: <3.15,>=3.12
|
|
26
26
|
Requires-Dist: anyio
|
|
27
|
-
Requires-Dist: python-injection
|
|
28
27
|
Requires-Dist: type-analyzer
|
|
28
|
+
Provides-Extra: injection
|
|
29
|
+
Requires-Dist: python-injection[async]; extra == 'injection'
|
|
29
30
|
Description-Content-Type: text/markdown
|
|
30
31
|
|
|
31
32
|
# python-cq
|
|
@@ -33,7 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
33
34
|
[](https://pypi.org/project/python-cq)
|
|
34
35
|
[](https://pypistats.org/packages/python-cq)
|
|
35
36
|
|
|
36
|
-
**python-cq** is a Python package designed to organize your code following CQRS principles. It
|
|
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.
|
|
37
38
|
|
|
38
39
|
## What is CQRS?
|
|
39
40
|
|
|
@@ -65,6 +66,13 @@ This knowledge will help you design coherent handlers and organize your code eff
|
|
|
65
66
|
## Installation
|
|
66
67
|
|
|
67
68
|
Requires Python 3.12 or higher.
|
|
69
|
+
|
|
70
|
+
Without dependency injection:
|
|
68
71
|
```bash
|
|
69
72
|
pip install python-cq
|
|
70
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
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from ._core.cq import CQ
|
|
2
|
+
from ._core.di import DIAdapter
|
|
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
|
|
7
|
+
from ._core.message import (
|
|
8
|
+
AnyCommandBus,
|
|
9
|
+
Command,
|
|
10
|
+
CommandBus,
|
|
11
|
+
Event,
|
|
12
|
+
EventBus,
|
|
13
|
+
Query,
|
|
14
|
+
QueryBus,
|
|
15
|
+
)
|
|
16
|
+
from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source
|
|
17
|
+
from ._core.pipetools import ContextCommandPipeline as _ContextCommandPipeline
|
|
18
|
+
from ._core.related_events import AnyIORelatedEvents, RelatedEvents
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
"AnyCommandBus",
|
|
22
|
+
"AnyIORelatedEvents",
|
|
23
|
+
"Bus",
|
|
24
|
+
"CQ",
|
|
25
|
+
"Command",
|
|
26
|
+
"CommandBus",
|
|
27
|
+
"ContextCommandPipeline",
|
|
28
|
+
"ContextPipeline",
|
|
29
|
+
"DIAdapter",
|
|
30
|
+
"Dispatcher",
|
|
31
|
+
"Event",
|
|
32
|
+
"EventBus",
|
|
33
|
+
"Middleware",
|
|
34
|
+
"MiddlewareResult",
|
|
35
|
+
"Pipe",
|
|
36
|
+
"Query",
|
|
37
|
+
"QueryBus",
|
|
38
|
+
"RelatedEvents",
|
|
39
|
+
"command_handler",
|
|
40
|
+
"event_handler",
|
|
41
|
+
"new_command_bus",
|
|
42
|
+
"new_event_bus",
|
|
43
|
+
"new_query_bus",
|
|
44
|
+
"query_handler",
|
|
45
|
+
"resolve_handler_source",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from cq.ext.injection import InjectionAdapter as _InjectionAdapter
|
|
50
|
+
|
|
51
|
+
except ImportError: # pragma: no cover
|
|
52
|
+
_default = CQ(_NoDI())
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
_default = CQ(_InjectionAdapter())
|
|
56
|
+
|
|
57
|
+
_default.register_defaults()
|
|
58
|
+
|
|
59
|
+
command_handler = _default.command_handler
|
|
60
|
+
event_handler = _default.event_handler
|
|
61
|
+
query_handler = _default.query_handler
|
|
62
|
+
|
|
63
|
+
new_command_bus = _default.new_command_bus
|
|
64
|
+
new_event_bus = _default.new_event_bus
|
|
65
|
+
new_query_bus = _default.new_query_bus
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ContextCommandPipeline[C: Command](_ContextCommandPipeline[C]):
|
|
69
|
+
__slots__ = ()
|
|
70
|
+
|
|
71
|
+
def __init__(self, di: DIAdapter = _default.di) -> None:
|
|
72
|
+
super().__init__(di)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
del _default
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
from cq._core.di import DIAdapter
|
|
4
|
+
from cq._core.dispatcher.bus import Bus, SimpleBus, TaskBus
|
|
5
|
+
from cq._core.handler import (
|
|
6
|
+
HandlerDecorator,
|
|
7
|
+
HandlerRegistry,
|
|
8
|
+
MultipleHandlerRegistry,
|
|
9
|
+
SingleHandlerRegistry,
|
|
10
|
+
)
|
|
11
|
+
from cq._core.message import Command, Event, Query
|
|
12
|
+
from cq._core.middlewares.scope import CommandDispatchScopeMiddleware
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CQ:
|
|
16
|
+
__slots__ = ("__command_registry", "__di", "__event_registry", "__query_registry")
|
|
17
|
+
|
|
18
|
+
__command_registry: HandlerRegistry[Command, Any]
|
|
19
|
+
__di: DIAdapter
|
|
20
|
+
__event_registry: HandlerRegistry[Event, Any]
|
|
21
|
+
__query_registry: HandlerRegistry[Query, Any]
|
|
22
|
+
|
|
23
|
+
def __init__(self, di: DIAdapter, /) -> None:
|
|
24
|
+
self.__di = di
|
|
25
|
+
self.__command_registry = SingleHandlerRegistry()
|
|
26
|
+
self.__event_registry = MultipleHandlerRegistry()
|
|
27
|
+
self.__query_registry = SingleHandlerRegistry()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def di(self) -> DIAdapter:
|
|
31
|
+
return self.__di
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def command_handler(self) -> HandlerDecorator[Command, Any]:
|
|
35
|
+
return HandlerDecorator(self.__command_registry, self.__di)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def event_handler(self) -> HandlerDecorator[Event, Any]:
|
|
39
|
+
return HandlerDecorator(self.__event_registry, self.__di)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def query_handler(self) -> HandlerDecorator[Query, Any]:
|
|
43
|
+
return HandlerDecorator(self.__query_registry, self.__di)
|
|
44
|
+
|
|
45
|
+
def new_command_bus(self) -> Bus[Command, Any]:
|
|
46
|
+
bus = SimpleBus(self.__command_registry)
|
|
47
|
+
command_middleware = CommandDispatchScopeMiddleware(self.__di)
|
|
48
|
+
bus.add_middlewares(command_middleware)
|
|
49
|
+
return bus
|
|
50
|
+
|
|
51
|
+
def new_event_bus(self) -> Bus[Event, None]:
|
|
52
|
+
return TaskBus(self.__event_registry)
|
|
53
|
+
|
|
54
|
+
def new_query_bus(self) -> Bus[Query, Any]:
|
|
55
|
+
return SimpleBus(self.__query_registry)
|
|
56
|
+
|
|
57
|
+
def register_defaults(self) -> Self:
|
|
58
|
+
self.__di.register_defaults(
|
|
59
|
+
self.new_command_bus,
|
|
60
|
+
self.new_event_bus,
|
|
61
|
+
self.new_query_bus,
|
|
62
|
+
)
|
|
63
|
+
return self
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from contextlib import nullcontext
|
|
6
|
+
from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from cq import CommandBus, EventBus, QueryBus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class DIAdapter(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Protocol for integrating a dependency injection container with python-cq.
|
|
16
|
+
|
|
17
|
+
Implement this protocol to connect your DI framework to the CQ buses.
|
|
18
|
+
A concrete implementation (``InjectionAdapter``) is provided via the
|
|
19
|
+
``python-cq[injection]`` extra for projects that use *python-injection*.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__slots__ = ()
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def command_scope(self) -> AsyncContextManager[None]:
|
|
26
|
+
"""
|
|
27
|
+
Return an async context manager that delimits the lifetime of a
|
|
28
|
+
command dispatch.
|
|
29
|
+
|
|
30
|
+
**Responsibilities**
|
|
31
|
+
|
|
32
|
+
The scope must at minimum manage the lifecycle of a ``RelatedEvents``
|
|
33
|
+
instance and register it so that it is resolvable via injection for
|
|
34
|
+
the duration of the scope.
|
|
35
|
+
|
|
36
|
+
**Nested calls**
|
|
37
|
+
|
|
38
|
+
``command_scope`` is entered in two distinct situations:
|
|
39
|
+
|
|
40
|
+
1. Around a standard command dispatch (via
|
|
41
|
+
``CommandDispatchScopeMiddleware``).
|
|
42
|
+
2. Around each step of a ``ContextCommandPipeline``, which itself
|
|
43
|
+
wraps a command dispatch.
|
|
44
|
+
|
|
45
|
+
This means two nested calls can occur for a single logical command.
|
|
46
|
+
Implementations must detect re-entrant activation (e.g. a scope
|
|
47
|
+
already active on the current task) and silently ignore the inner
|
|
48
|
+
call instead of opening a second, conflicting scope.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def lazy[T](self, tp: type[T]) -> Callable[[], Awaitable[T]]:
|
|
55
|
+
"""
|
|
56
|
+
Return a callable that resolves an instance of ``tp`` in two steps.
|
|
57
|
+
|
|
58
|
+
1. ``lazy(tp)`` obtains a resolver from the DI framework for ``tp``.
|
|
59
|
+
2. Calling and awaiting the returned callable performs the actual
|
|
60
|
+
resolution and returns the instance.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
def register_defaults(
|
|
66
|
+
self,
|
|
67
|
+
command_bus: Callable[..., CommandBus[Any]],
|
|
68
|
+
event_bus: Callable[..., EventBus],
|
|
69
|
+
query_bus: Callable[..., QueryBus[Any]],
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Register the CQ buses as default providers in the DI container.
|
|
73
|
+
|
|
74
|
+
Called once during setup so that handlers and middlewares can
|
|
75
|
+
declare ``CommandBus``, ``EventBus``, or ``QueryBus`` as
|
|
76
|
+
constructor dependencies and receive the configured instances.
|
|
77
|
+
|
|
78
|
+
The default implementation is a no-op for adapters that do not
|
|
79
|
+
need automatic bus registration.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def wire[T](self, tp: type[T]) -> Callable[..., Awaitable[T]]:
|
|
86
|
+
"""
|
|
87
|
+
Return an async factory that instantiates ``tp`` with injected
|
|
88
|
+
dependencies.
|
|
89
|
+
|
|
90
|
+
Used internally to build handler instances whose dependencies are
|
|
91
|
+
resolved by the container.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NoDI(DIAdapter):
|
|
98
|
+
__slots__ = ()
|
|
99
|
+
|
|
100
|
+
def command_scope(self) -> AsyncContextManager[None]:
|
|
101
|
+
return nullcontext()
|
|
102
|
+
|
|
103
|
+
def lazy[T](self, tp: type[T], /) -> Callable[[], Awaitable[T]]:
|
|
104
|
+
tp_str = getattr(tp, "__name__", str(tp))
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
f"Can't lazily resolve {tp_str}: no DI container configured."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def wire[T](self, tp: type[T], /) -> Callable[..., Awaitable[T]]:
|
|
110
|
+
async def factory() -> T:
|
|
111
|
+
return tp()
|
|
112
|
+
|
|
113
|
+
return factory
|
|
@@ -15,15 +15,6 @@ class Dispatcher[I, O](Protocol):
|
|
|
15
15
|
raise NotImplementedError
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@runtime_checkable
|
|
19
|
-
class DeferredDispatcher[I](Protocol):
|
|
20
|
-
__slots__ = ()
|
|
21
|
-
|
|
22
|
-
@abstractmethod
|
|
23
|
-
async def defer(self, input_value: I, /) -> None:
|
|
24
|
-
raise NotImplementedError
|
|
25
|
-
|
|
26
|
-
|
|
27
18
|
class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
|
|
28
19
|
__slots__ = ("__middleware_group",)
|
|
29
20
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from types import GenericAlias
|
|
3
|
+
from typing import TypeAliasType
|
|
4
|
+
|
|
5
|
+
from cq._core.di import DIAdapter
|
|
6
|
+
from cq._core.dispatcher.base import Dispatcher
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LazyDispatcher[I, O](Dispatcher[I, O]):
|
|
10
|
+
__slots__ = ("__resolve",)
|
|
11
|
+
|
|
12
|
+
__resolve: Callable[[], Awaitable[Dispatcher[I, O]]]
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
dispatcher_type: type[Dispatcher[I, O]] | TypeAliasType | GenericAlias,
|
|
17
|
+
/,
|
|
18
|
+
di: DIAdapter,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.__resolve = di.lazy(dispatcher_type) # type: ignore[arg-type]
|
|
21
|
+
|
|
22
|
+
async def dispatch(self, input_value: I, /) -> O:
|
|
23
|
+
dispatcher = await self.__resolve()
|
|
24
|
+
return await dispatcher.dispatch(input_value)
|
|
@@ -7,10 +7,10 @@ from inspect import Parameter, isclass, unwrap
|
|
|
7
7
|
from inspect import signature as inspect_signature
|
|
8
8
|
from typing import TYPE_CHECKING, Any, Protocol, Self, overload, runtime_checkable
|
|
9
9
|
|
|
10
|
-
import injection
|
|
11
10
|
from type_analyzer import MatchingTypesConfig, iter_matching_types, matching_types
|
|
12
11
|
|
|
13
12
|
from cq._core.common.typing import Decorator
|
|
13
|
+
from cq._core.di import DIAdapter, NoDI
|
|
14
14
|
|
|
15
15
|
type HandlerType[**P, T] = type[Handler[P, T]]
|
|
16
16
|
type HandlerFactory[**P, T] = Callable[..., Awaitable[Handler[P, T]]]
|
|
@@ -126,7 +126,7 @@ class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
|
126
126
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
127
127
|
class HandlerDecorator[I, O]:
|
|
128
128
|
registry: HandlerRegistry[I, O]
|
|
129
|
-
|
|
129
|
+
di: DIAdapter = field(default_factory=NoDI)
|
|
130
130
|
|
|
131
131
|
if TYPE_CHECKING: # pragma: no cover
|
|
132
132
|
|
|
@@ -137,7 +137,6 @@ class HandlerDecorator[I, O]:
|
|
|
137
137
|
/,
|
|
138
138
|
*,
|
|
139
139
|
fail_silently: bool = ...,
|
|
140
|
-
threadsafe: bool | None = ...,
|
|
141
140
|
) -> Decorator: ...
|
|
142
141
|
|
|
143
142
|
@overload
|
|
@@ -147,7 +146,6 @@ class HandlerDecorator[I, O]:
|
|
|
147
146
|
/,
|
|
148
147
|
*,
|
|
149
148
|
fail_silently: bool = ...,
|
|
150
|
-
threadsafe: bool | None = ...,
|
|
151
149
|
) -> T: ...
|
|
152
150
|
|
|
153
151
|
@overload
|
|
@@ -157,7 +155,6 @@ class HandlerDecorator[I, O]:
|
|
|
157
155
|
/,
|
|
158
156
|
*,
|
|
159
157
|
fail_silently: bool = ...,
|
|
160
|
-
threadsafe: bool | None = ...,
|
|
161
158
|
) -> Decorator: ...
|
|
162
159
|
|
|
163
160
|
def __call__[T](
|
|
@@ -166,7 +163,6 @@ class HandlerDecorator[I, O]:
|
|
|
166
163
|
/,
|
|
167
164
|
*,
|
|
168
165
|
fail_silently: bool = False,
|
|
169
|
-
threadsafe: bool | None = None,
|
|
170
166
|
) -> Any:
|
|
171
167
|
if (
|
|
172
168
|
input_or_handler_type is not None
|
|
@@ -176,14 +172,12 @@ class HandlerDecorator[I, O]:
|
|
|
176
172
|
return self.__decorator(
|
|
177
173
|
input_or_handler_type,
|
|
178
174
|
fail_silently=fail_silently,
|
|
179
|
-
threadsafe=threadsafe,
|
|
180
175
|
)
|
|
181
176
|
|
|
182
177
|
return partial(
|
|
183
178
|
self.__decorator,
|
|
184
179
|
input_type=input_or_handler_type, # type: ignore[arg-type]
|
|
185
180
|
fail_silently=fail_silently,
|
|
186
|
-
threadsafe=threadsafe,
|
|
187
181
|
)
|
|
188
182
|
|
|
189
183
|
def __decorator(
|
|
@@ -193,9 +187,8 @@ class HandlerDecorator[I, O]:
|
|
|
193
187
|
*,
|
|
194
188
|
input_type: type[I] | None = None,
|
|
195
189
|
fail_silently: bool = False,
|
|
196
|
-
threadsafe: bool | None = None,
|
|
197
190
|
) -> HandlerType[[I], O]:
|
|
198
|
-
factory = self.
|
|
191
|
+
factory = self.di.wire(wrapped)
|
|
199
192
|
input_type = input_type or _resolve_input_type(wrapped)
|
|
200
193
|
self.registry.subscribe(input_type, factory, wrapped, fail_silently)
|
|
201
194
|
return wrapped
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from cq._core.dispatcher.base import Dispatcher
|
|
4
|
+
|
|
5
|
+
Command = object
|
|
6
|
+
Event = object
|
|
7
|
+
Query = object
|
|
8
|
+
|
|
9
|
+
type CommandBus[T] = Dispatcher[Command, T]
|
|
10
|
+
type EventBus = Dispatcher[Event, None]
|
|
11
|
+
type QueryBus[T] = Dispatcher[Query, T]
|
|
12
|
+
|
|
13
|
+
AnyCommandBus = CommandBus[Any]
|
|
@@ -9,7 +9,8 @@ from cq.exceptions import MiddlewareError
|
|
|
9
9
|
type MiddlewareResult[T] = AsyncGenerator[None, T]
|
|
10
10
|
type GeneratorMiddleware[**P, T] = Callable[P, MiddlewareResult[T]]
|
|
11
11
|
type ClassicMiddleware[**P, T] = Callable[
|
|
12
|
-
Concatenate[Callable[P, Awaitable[T]], P],
|
|
12
|
+
Concatenate[Callable[P, Awaitable[T]], P],
|
|
13
|
+
Awaitable[T],
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
type Middleware[**P, T] = ClassicMiddleware[P, T] | GeneratorMiddleware[P, T]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from cq._core.di import DIAdapter
|
|
5
|
+
from cq._core.middleware import MiddlewareResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
9
|
+
class CommandDispatchScopeMiddleware:
|
|
10
|
+
di: DIAdapter
|
|
11
|
+
|
|
12
|
+
async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
|
|
13
|
+
async with self.di.command_scope():
|
|
14
|
+
yield
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
2
|
+
|
|
3
|
+
from cq import Dispatcher
|
|
4
|
+
from cq._core.common.typing import Decorator
|
|
5
|
+
from cq._core.di import DIAdapter
|
|
6
|
+
from cq._core.dispatcher.lazy import LazyDispatcher
|
|
7
|
+
from cq._core.dispatcher.pipe import (
|
|
8
|
+
ContextPipeline,
|
|
9
|
+
ConvertMethod,
|
|
10
|
+
ConvertMethodAsync,
|
|
11
|
+
ConvertMethodSync,
|
|
12
|
+
)
|
|
13
|
+
from cq._core.message import Command, CommandBus, Query, QueryBus
|
|
14
|
+
from cq._core.middlewares.scope import CommandDispatchScopeMiddleware
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContextCommandPipeline[C: Command](ContextPipeline[C]):
|
|
18
|
+
__slots__ = ("__query_dispatcher",)
|
|
19
|
+
|
|
20
|
+
__query_dispatcher: Dispatcher[Query, Any]
|
|
21
|
+
|
|
22
|
+
def __init__(self, di: DIAdapter) -> None:
|
|
23
|
+
super().__init__(LazyDispatcher(CommandBus, di))
|
|
24
|
+
self.__query_dispatcher = LazyDispatcher(QueryBus, di)
|
|
25
|
+
command_middleware = CommandDispatchScopeMiddleware(di)
|
|
26
|
+
self.add_middlewares(command_middleware)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
29
|
+
|
|
30
|
+
@overload
|
|
31
|
+
def query_step[Q: Query](
|
|
32
|
+
self,
|
|
33
|
+
wrapped: ConvertMethodAsync[Q, Any],
|
|
34
|
+
/,
|
|
35
|
+
) -> ConvertMethodAsync[Q, Any]: ...
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def query_step[Q: Query](
|
|
39
|
+
self,
|
|
40
|
+
wrapped: ConvertMethodSync[Q, Any],
|
|
41
|
+
/,
|
|
42
|
+
) -> ConvertMethodSync[Q, Any]: ...
|
|
43
|
+
|
|
44
|
+
@overload
|
|
45
|
+
def query_step(self, wrapped: None = ..., /) -> Decorator: ...
|
|
46
|
+
|
|
47
|
+
def query_step[Q: Query]( # type: ignore[misc]
|
|
48
|
+
self,
|
|
49
|
+
wrapped: ConvertMethod[Q, Any] | None = None,
|
|
50
|
+
/,
|
|
51
|
+
) -> Any:
|
|
52
|
+
return self.step(wrapped, dispatcher=self.__query_dispatcher)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from collections.abc import AsyncIterator
|
|
3
2
|
from dataclasses import dataclass, field
|
|
4
|
-
from
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any, Protocol, Self, runtime_checkable
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import injection
|
|
6
|
+
from anyio import create_task_group
|
|
8
7
|
from anyio.abc import TaskGroup
|
|
9
8
|
|
|
10
9
|
from cq._core.message import Event, EventBus
|
|
11
|
-
from cq._core.scope import CQScope
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
@runtime_checkable
|
|
@@ -23,21 +21,27 @@ class RelatedEvents(Protocol):
|
|
|
23
21
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
24
22
|
class AnyIORelatedEvents(RelatedEvents):
|
|
25
23
|
event_bus: EventBus
|
|
26
|
-
task_group: TaskGroup
|
|
24
|
+
task_group: TaskGroup = field(default_factory=create_task_group)
|
|
27
25
|
history: list[Event] = field(default_factory=list, init=False)
|
|
28
26
|
|
|
29
27
|
def __bool__(self) -> bool: # pragma: no cover
|
|
30
28
|
return bool(self.history)
|
|
31
29
|
|
|
30
|
+
async def __aenter__(self) -> Self:
|
|
31
|
+
await self.task_group.__aenter__()
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
async def __aexit__(
|
|
35
|
+
self,
|
|
36
|
+
exc_type: type[BaseException] | None,
|
|
37
|
+
exc_value: BaseException | None,
|
|
38
|
+
traceback: TracebackType | None,
|
|
39
|
+
) -> Any:
|
|
40
|
+
return await self.task_group.__aexit__(exc_type, exc_value, traceback)
|
|
41
|
+
|
|
32
42
|
def add(self, *events: Event) -> None:
|
|
33
43
|
self.history.extend(events)
|
|
34
44
|
dispatch_method = self.event_bus.dispatch
|
|
35
45
|
|
|
36
46
|
for event in events:
|
|
37
47
|
self.task_group.start_soon(dispatch_method, event)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@injection.scoped(CQScope.TRANSACTION, mode="fallback")
|
|
41
|
-
async def related_events_recipe(event_bus: EventBus) -> AsyncIterator[RelatedEvents]:
|
|
42
|
-
async with anyio.create_task_group() as task_group:
|
|
43
|
-
yield AnyIORelatedEvents(event_bus, task_group)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any, AsyncContextManager, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from injection import Module, adefine_scope, mod
|
|
8
|
+
from injection.exceptions import ScopeAlreadyDefinedError
|
|
9
|
+
|
|
10
|
+
from cq._core.di import DIAdapter
|
|
11
|
+
from cq._core.message import CommandBus, EventBus, QueryBus
|
|
12
|
+
from cq._core.middleware import MiddlewareResult
|
|
13
|
+
from cq._core.related_events import AnyIORelatedEvents, RelatedEvents
|
|
14
|
+
|
|
15
|
+
__all__ = ("CQScope", "InjectionAdapter", "InjectionScopeMiddleware")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CQScope(StrEnum):
|
|
19
|
+
COMMAND_DISPATCH = "__cq_command_dispatch__"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
23
|
+
class InjectionAdapter(DIAdapter):
|
|
24
|
+
module: Module = field(default_factory=mod)
|
|
25
|
+
threadsafe: bool | None = field(default=None)
|
|
26
|
+
|
|
27
|
+
def command_scope(self) -> AsyncContextManager[None]:
|
|
28
|
+
return InjectionScopeMiddleware( # type: ignore[return-value]
|
|
29
|
+
CQScope.COMMAND_DISPATCH,
|
|
30
|
+
exist_ok=True,
|
|
31
|
+
threadsafe=self.threadsafe,
|
|
32
|
+
)._cm
|
|
33
|
+
|
|
34
|
+
def lazy[T](self, tp: type[T], /) -> Callable[[], Awaitable[T]]:
|
|
35
|
+
awaitable = self.module.aget_lazy_instance(tp, threadsafe=self.threadsafe)
|
|
36
|
+
return lambda: awaitable
|
|
37
|
+
|
|
38
|
+
def register_defaults(
|
|
39
|
+
self,
|
|
40
|
+
command_bus: Callable[..., CommandBus[Any]],
|
|
41
|
+
event_bus: Callable[..., EventBus],
|
|
42
|
+
query_bus: Callable[..., QueryBus[Any]],
|
|
43
|
+
) -> None:
|
|
44
|
+
self.module.injectable(
|
|
45
|
+
command_bus,
|
|
46
|
+
ignore_type_hint=True,
|
|
47
|
+
inject=False,
|
|
48
|
+
on=CommandBus,
|
|
49
|
+
mode="fallback",
|
|
50
|
+
)
|
|
51
|
+
self.module.injectable(
|
|
52
|
+
event_bus,
|
|
53
|
+
ignore_type_hint=True,
|
|
54
|
+
inject=False,
|
|
55
|
+
on=EventBus,
|
|
56
|
+
mode="fallback",
|
|
57
|
+
)
|
|
58
|
+
self.module.injectable(
|
|
59
|
+
query_bus,
|
|
60
|
+
ignore_type_hint=True,
|
|
61
|
+
inject=False,
|
|
62
|
+
on=QueryBus,
|
|
63
|
+
mode="fallback",
|
|
64
|
+
)
|
|
65
|
+
self.module.scoped(
|
|
66
|
+
CQScope.COMMAND_DISPATCH,
|
|
67
|
+
mode="fallback",
|
|
68
|
+
)(self.__new_related_events)
|
|
69
|
+
|
|
70
|
+
def wire[T](self, tp: type[T], /) -> Callable[..., Awaitable[T]]:
|
|
71
|
+
return self.module.make_async_factory(tp, self.threadsafe)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
async def __new_related_events(event_bus: EventBus) -> AsyncIterator[RelatedEvents]:
|
|
75
|
+
async with AnyIORelatedEvents(event_bus) as events:
|
|
76
|
+
yield events
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
80
|
+
class InjectionScopeMiddleware:
|
|
81
|
+
scope_name: str
|
|
82
|
+
exist_ok: bool = field(default=False, kw_only=True)
|
|
83
|
+
threadsafe: bool | None = field(default=None, kw_only=True)
|
|
84
|
+
|
|
85
|
+
async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
|
|
86
|
+
async with self._cm: # type: ignore[attr-defined]
|
|
87
|
+
yield
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
@asynccontextmanager
|
|
91
|
+
async def _cm(self) -> AsyncIterator[None]:
|
|
92
|
+
async with AsyncExitStack() as stack:
|
|
93
|
+
try:
|
|
94
|
+
await stack.enter_async_context(
|
|
95
|
+
adefine_scope(self.scope_name, threadsafe=self.threadsafe)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
except ScopeAlreadyDefinedError:
|
|
99
|
+
if not self.exist_ok:
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
yield
|
|
File without changes
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://pypi.org/project/python-cq)
|
|
4
4
|
[](https://pypistats.org/packages/python-cq)
|
|
5
5
|
|
|
6
|
-
**python-cq** is a Python package designed to organize your code following CQRS principles. It
|
|
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
7
|
|
|
8
8
|
## What is CQRS?
|
|
9
9
|
|
|
@@ -35,6 +35,13 @@ This knowledge will help you design coherent handlers and organize your code eff
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
37
37
|
Requires Python 3.12 or higher.
|
|
38
|
+
|
|
39
|
+
Without dependency injection:
|
|
38
40
|
```bash
|
|
39
41
|
pip install python-cq
|
|
40
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
|
+
```
|
|
@@ -13,8 +13,6 @@ docs = [
|
|
|
13
13
|
"mkdocs-material",
|
|
14
14
|
]
|
|
15
15
|
test = [
|
|
16
|
-
"fastapi",
|
|
17
|
-
"httpx",
|
|
18
16
|
"pytest",
|
|
19
17
|
"pytest-asyncio",
|
|
20
18
|
"pytest-cov",
|
|
@@ -22,7 +20,7 @@ test = [
|
|
|
22
20
|
|
|
23
21
|
[project]
|
|
24
22
|
name = "python-cq"
|
|
25
|
-
version = "0.
|
|
23
|
+
version = "0.16.0"
|
|
26
24
|
description = "CQRS library for async Python projects."
|
|
27
25
|
license = "MIT"
|
|
28
26
|
license-files = ["LICENSE"]
|
|
@@ -48,10 +46,14 @@ classifiers = [
|
|
|
48
46
|
]
|
|
49
47
|
dependencies = [
|
|
50
48
|
"anyio",
|
|
51
|
-
"python-injection",
|
|
52
49
|
"type-analyzer",
|
|
53
50
|
]
|
|
54
51
|
|
|
52
|
+
[project.optional-dependencies]
|
|
53
|
+
injection = [
|
|
54
|
+
"python-injection[async]",
|
|
55
|
+
]
|
|
56
|
+
|
|
55
57
|
[project.urls]
|
|
56
58
|
Documentation = "https://python-cq.remimd.dev"
|
|
57
59
|
Repository = "https://github.com/100nm/python-cq"
|
python_cq-0.15.2/cq/__init__.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from ._core.dispatcher.base import DeferredDispatcher, Dispatcher
|
|
2
|
-
from ._core.dispatcher.bus import Bus
|
|
3
|
-
from ._core.dispatcher.lazy import LazyDispatcher
|
|
4
|
-
from ._core.dispatcher.pipe import ContextPipeline, Pipe
|
|
5
|
-
from ._core.message import (
|
|
6
|
-
AnyCommandBus,
|
|
7
|
-
Command,
|
|
8
|
-
CommandBus,
|
|
9
|
-
Event,
|
|
10
|
-
EventBus,
|
|
11
|
-
Query,
|
|
12
|
-
QueryBus,
|
|
13
|
-
command_handler,
|
|
14
|
-
event_handler,
|
|
15
|
-
new_command_bus,
|
|
16
|
-
new_event_bus,
|
|
17
|
-
new_query_bus,
|
|
18
|
-
query_handler,
|
|
19
|
-
)
|
|
20
|
-
from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source
|
|
21
|
-
from ._core.pipetools import ContextCommandPipeline
|
|
22
|
-
from ._core.related_events import RelatedEvents
|
|
23
|
-
from ._core.scope import CQScope
|
|
24
|
-
|
|
25
|
-
__all__ = (
|
|
26
|
-
"AnyCommandBus",
|
|
27
|
-
"Bus",
|
|
28
|
-
"CQScope",
|
|
29
|
-
"Command",
|
|
30
|
-
"CommandBus",
|
|
31
|
-
"ContextCommandPipeline",
|
|
32
|
-
"ContextPipeline",
|
|
33
|
-
"DeferredDispatcher",
|
|
34
|
-
"Dispatcher",
|
|
35
|
-
"Event",
|
|
36
|
-
"EventBus",
|
|
37
|
-
"LazyDispatcher",
|
|
38
|
-
"Middleware",
|
|
39
|
-
"MiddlewareResult",
|
|
40
|
-
"Pipe",
|
|
41
|
-
"Query",
|
|
42
|
-
"QueryBus",
|
|
43
|
-
"RelatedEvents",
|
|
44
|
-
"command_handler",
|
|
45
|
-
"event_handler",
|
|
46
|
-
"new_command_bus",
|
|
47
|
-
"new_event_bus",
|
|
48
|
-
"new_query_bus",
|
|
49
|
-
"query_handler",
|
|
50
|
-
"resolve_handler_source",
|
|
51
|
-
)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from collections.abc import Awaitable
|
|
2
|
-
from types import GenericAlias
|
|
3
|
-
from typing import TypeAliasType
|
|
4
|
-
|
|
5
|
-
import injection
|
|
6
|
-
|
|
7
|
-
from cq._core.dispatcher.base import Dispatcher
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class LazyDispatcher[I, O](Dispatcher[I, O]):
|
|
11
|
-
__slots__ = ("__value",)
|
|
12
|
-
|
|
13
|
-
__value: Awaitable[Dispatcher[I, O]]
|
|
14
|
-
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
dispatcher_type: type[Dispatcher[I, O]] | TypeAliasType | GenericAlias,
|
|
18
|
-
/,
|
|
19
|
-
*,
|
|
20
|
-
injection_module: injection.Module | None = None,
|
|
21
|
-
threadsafe: bool | None = None,
|
|
22
|
-
) -> None:
|
|
23
|
-
module = injection_module or injection.mod()
|
|
24
|
-
self.__value = module.aget_lazy_instance(dispatcher_type, threadsafe=threadsafe)
|
|
25
|
-
|
|
26
|
-
async def dispatch(self, input_value: I, /) -> O:
|
|
27
|
-
dispatcher = await self.__value
|
|
28
|
-
return await dispatcher.dispatch(input_value)
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
from typing import Any, Final
|
|
2
|
-
|
|
3
|
-
import injection
|
|
4
|
-
|
|
5
|
-
from cq._core.dispatcher.base import Dispatcher
|
|
6
|
-
from cq._core.dispatcher.bus import Bus, SimpleBus, TaskBus
|
|
7
|
-
from cq._core.handler import (
|
|
8
|
-
HandlerDecorator,
|
|
9
|
-
MultipleHandlerRegistry,
|
|
10
|
-
SingleHandlerRegistry,
|
|
11
|
-
)
|
|
12
|
-
from cq._core.scope import CQScope
|
|
13
|
-
from cq.middlewares.scope import InjectionScopeMiddleware
|
|
14
|
-
|
|
15
|
-
Command = object
|
|
16
|
-
Event = object
|
|
17
|
-
Query = object
|
|
18
|
-
|
|
19
|
-
type CommandBus[T] = Dispatcher[Command, T]
|
|
20
|
-
type EventBus = Dispatcher[Event, None]
|
|
21
|
-
type QueryBus[T] = Dispatcher[Query, T]
|
|
22
|
-
|
|
23
|
-
AnyCommandBus = CommandBus[Any]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
command_handler: Final[HandlerDecorator[Command, Any]] = HandlerDecorator(
|
|
27
|
-
SingleHandlerRegistry(),
|
|
28
|
-
)
|
|
29
|
-
event_handler: Final[HandlerDecorator[Event, Any]] = HandlerDecorator(
|
|
30
|
-
MultipleHandlerRegistry(),
|
|
31
|
-
)
|
|
32
|
-
query_handler: Final[HandlerDecorator[Query, Any]] = HandlerDecorator(
|
|
33
|
-
SingleHandlerRegistry(),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@injection.injectable(
|
|
38
|
-
ignore_type_hint=True,
|
|
39
|
-
inject=False,
|
|
40
|
-
on=CommandBus,
|
|
41
|
-
mode="fallback",
|
|
42
|
-
)
|
|
43
|
-
def new_command_bus(*, threadsafe: bool | None = None) -> Bus[Command, Any]:
|
|
44
|
-
bus = SimpleBus(command_handler.registry)
|
|
45
|
-
transaction_scope_middleware = InjectionScopeMiddleware(
|
|
46
|
-
CQScope.TRANSACTION,
|
|
47
|
-
exist_ok=True,
|
|
48
|
-
threadsafe=threadsafe,
|
|
49
|
-
)
|
|
50
|
-
bus.add_middlewares(transaction_scope_middleware)
|
|
51
|
-
return bus
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@injection.injectable(
|
|
55
|
-
ignore_type_hint=True,
|
|
56
|
-
inject=False,
|
|
57
|
-
on=EventBus,
|
|
58
|
-
mode="fallback",
|
|
59
|
-
)
|
|
60
|
-
def new_event_bus() -> Bus[Event, None]:
|
|
61
|
-
return TaskBus(event_handler.registry)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@injection.injectable(
|
|
65
|
-
ignore_type_hint=True,
|
|
66
|
-
inject=False,
|
|
67
|
-
on=QueryBus,
|
|
68
|
-
mode="fallback",
|
|
69
|
-
)
|
|
70
|
-
def new_query_bus() -> Bus[Query, Any]:
|
|
71
|
-
return SimpleBus(query_handler.registry)
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING, Any, overload
|
|
2
|
-
|
|
3
|
-
import injection
|
|
4
|
-
|
|
5
|
-
from cq import Dispatcher
|
|
6
|
-
from cq._core.common.typing import Decorator
|
|
7
|
-
from cq._core.dispatcher.lazy import LazyDispatcher
|
|
8
|
-
from cq._core.dispatcher.pipe import (
|
|
9
|
-
ContextPipeline,
|
|
10
|
-
ConvertMethod,
|
|
11
|
-
ConvertMethodAsync,
|
|
12
|
-
ConvertMethodSync,
|
|
13
|
-
)
|
|
14
|
-
from cq._core.message import AnyCommandBus, Command, Query, QueryBus
|
|
15
|
-
from cq._core.scope import CQScope
|
|
16
|
-
from cq.middlewares.scope import InjectionScopeMiddleware
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ContextCommandPipeline[I: Command](ContextPipeline[I]):
|
|
20
|
-
__slots__ = ("__query_dispatcher",)
|
|
21
|
-
|
|
22
|
-
__query_dispatcher: Dispatcher[Query, Any]
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
/,
|
|
27
|
-
*,
|
|
28
|
-
injection_module: injection.Module | None = None,
|
|
29
|
-
threadsafe: bool | None = None,
|
|
30
|
-
) -> None:
|
|
31
|
-
command_dispatcher = LazyDispatcher(
|
|
32
|
-
AnyCommandBus,
|
|
33
|
-
injection_module=injection_module,
|
|
34
|
-
threadsafe=threadsafe,
|
|
35
|
-
)
|
|
36
|
-
super().__init__(command_dispatcher)
|
|
37
|
-
|
|
38
|
-
self.__query_dispatcher = LazyDispatcher(
|
|
39
|
-
QueryBus,
|
|
40
|
-
injection_module=injection_module,
|
|
41
|
-
threadsafe=threadsafe,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
transaction_scope_middleware = InjectionScopeMiddleware(
|
|
45
|
-
CQScope.TRANSACTION,
|
|
46
|
-
exist_ok=True,
|
|
47
|
-
threadsafe=threadsafe,
|
|
48
|
-
)
|
|
49
|
-
self.add_middlewares(transaction_scope_middleware)
|
|
50
|
-
|
|
51
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
52
|
-
|
|
53
|
-
@overload
|
|
54
|
-
def query_step[T: Query](
|
|
55
|
-
self,
|
|
56
|
-
wrapped: ConvertMethodAsync[T, Any],
|
|
57
|
-
/,
|
|
58
|
-
) -> ConvertMethodAsync[T, Any]: ...
|
|
59
|
-
|
|
60
|
-
@overload
|
|
61
|
-
def query_step[T: Query](
|
|
62
|
-
self,
|
|
63
|
-
wrapped: ConvertMethodSync[T, Any],
|
|
64
|
-
/,
|
|
65
|
-
) -> ConvertMethodSync[T, Any]: ...
|
|
66
|
-
|
|
67
|
-
@overload
|
|
68
|
-
def query_step(self, wrapped: None = ..., /) -> Decorator: ...
|
|
69
|
-
|
|
70
|
-
def query_step[T: Query]( # type: ignore[misc]
|
|
71
|
-
self,
|
|
72
|
-
wrapped: ConvertMethod[T, Any] | None = None,
|
|
73
|
-
/,
|
|
74
|
-
) -> Any:
|
|
75
|
-
return self.step(wrapped, dispatcher=self.__query_dispatcher)
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import TYPE_CHECKING, Annotated, Any
|
|
3
|
-
|
|
4
|
-
from fastapi import BackgroundTasks, Depends
|
|
5
|
-
from injection.ext.fastapi import Inject
|
|
6
|
-
|
|
7
|
-
from cq import (
|
|
8
|
-
Command,
|
|
9
|
-
CommandBus,
|
|
10
|
-
DeferredDispatcher,
|
|
11
|
-
Dispatcher,
|
|
12
|
-
Event,
|
|
13
|
-
EventBus,
|
|
14
|
-
Query,
|
|
15
|
-
QueryBus,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
__all__ = (
|
|
19
|
-
"DeferredCommandBus",
|
|
20
|
-
"DeferredEventBus",
|
|
21
|
-
"DeferredQueryBus",
|
|
22
|
-
"FastAPIDeferredDispatcher",
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
27
|
-
class FastAPIDeferredDispatcher[I](DeferredDispatcher[I]):
|
|
28
|
-
background_tasks: BackgroundTasks
|
|
29
|
-
dispatcher: Dispatcher[I, Any]
|
|
30
|
-
|
|
31
|
-
async def defer(self, input_value: I, /) -> None:
|
|
32
|
-
self.background_tasks.add_task(self.dispatcher.dispatch, input_value)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
async def new_deferred_command_bus[T](
|
|
36
|
-
background_tasks: BackgroundTasks,
|
|
37
|
-
command_bus: Inject[CommandBus[T]],
|
|
38
|
-
) -> DeferredDispatcher[Command]:
|
|
39
|
-
return FastAPIDeferredDispatcher(background_tasks, command_bus)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
async def new_deferred_event_bus(
|
|
43
|
-
background_tasks: BackgroundTasks,
|
|
44
|
-
event_bus: Inject[EventBus],
|
|
45
|
-
) -> DeferredDispatcher[Event]:
|
|
46
|
-
return FastAPIDeferredDispatcher(background_tasks, event_bus)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
async def new_deferred_query_bus[T](
|
|
50
|
-
background_tasks: BackgroundTasks,
|
|
51
|
-
query_bus: Inject[QueryBus[T]],
|
|
52
|
-
) -> DeferredDispatcher[Query]:
|
|
53
|
-
return FastAPIDeferredDispatcher(background_tasks, query_bus)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
57
|
-
type DeferredCommandBus = DeferredDispatcher[Command]
|
|
58
|
-
type DeferredEventBus = DeferredDispatcher[Event]
|
|
59
|
-
type DeferredQueryBus = DeferredDispatcher[Query]
|
|
60
|
-
|
|
61
|
-
else:
|
|
62
|
-
DeferredCommandBus = Annotated[
|
|
63
|
-
DeferredDispatcher[Command],
|
|
64
|
-
Depends(new_deferred_command_bus, use_cache=False),
|
|
65
|
-
]
|
|
66
|
-
DeferredEventBus = Annotated[
|
|
67
|
-
DeferredDispatcher[Event],
|
|
68
|
-
Depends(new_deferred_event_bus, use_cache=False),
|
|
69
|
-
]
|
|
70
|
-
DeferredQueryBus = Annotated[
|
|
71
|
-
DeferredDispatcher[Query],
|
|
72
|
-
Depends(new_deferred_query_bus, use_cache=False),
|
|
73
|
-
]
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from contextlib import AsyncExitStack
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
-
|
|
7
|
-
from injection import adefine_scope
|
|
8
|
-
from injection.exceptions import ScopeAlreadyDefinedError
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
11
|
-
from cq import MiddlewareResult
|
|
12
|
-
|
|
13
|
-
__all__ = ("InjectionScopeMiddleware",)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
17
|
-
class InjectionScopeMiddleware:
|
|
18
|
-
scope_name: str
|
|
19
|
-
exist_ok: bool = field(default=False, kw_only=True)
|
|
20
|
-
threadsafe: bool | None = field(default=None, kw_only=True)
|
|
21
|
-
|
|
22
|
-
async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
|
|
23
|
-
async with AsyncExitStack() as stack:
|
|
24
|
-
try:
|
|
25
|
-
await stack.enter_async_context(
|
|
26
|
-
adefine_scope(self.scope_name, threadsafe=self.threadsafe)
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
except ScopeAlreadyDefinedError:
|
|
30
|
-
if not self.exist_ok:
|
|
31
|
-
raise
|
|
32
|
-
|
|
33
|
-
yield
|
|
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
|