python-cq 0.25.2__tar.gz → 0.27.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.25.2 → python_cq-0.27.0}/PKG-INFO +4 -2
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/__init__.py +7 -31
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/middleware.py +23 -5
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/command_pipeline.py +8 -1
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/dispatchers/bus.py +19 -6
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/handler.py +80 -55
- python_cq-0.27.0/cq/default.py +37 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/docs/index.md +2 -1
- {python_cq-0.25.2 → python_cq-0.27.0}/pyproject.toml +2 -1
- {python_cq-0.25.2 → python_cq-0.27.0}/.gitignore +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/LICENSE +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/common/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/common/typing.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/message.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/queuing/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/queuing/pump.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/queuing/queues/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/queuing/queues/abc.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/queuing/queues/memory.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/related_events.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/di.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/dispatchers/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/dispatchers/abc.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/dispatchers/functions.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/dispatchers/lazy.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/dispatchers/pipe.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/_core/routing/router.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/exceptions.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/ext/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/ext/injection.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/middlewares/__init__.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/middlewares/contextlib.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/middlewares/exc.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/middlewares/retry.py +0 -0
- {python_cq-0.25.2 → python_cq-0.27.0}/cq/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-cq
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.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
|
|
@@ -26,6 +26,7 @@ Classifier: Typing :: Typed
|
|
|
26
26
|
Requires-Python: <3.16,>=3.12
|
|
27
27
|
Requires-Dist: anyio
|
|
28
28
|
Requires-Dist: type-analyzer
|
|
29
|
+
Requires-Dist: typing-extensions; python_version < '3.13'
|
|
29
30
|
Provides-Extra: injection
|
|
30
31
|
Requires-Dist: python-injection[async]; extra == 'injection'
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
@@ -84,7 +85,8 @@ pip install python-cq
|
|
|
84
85
|
|
|
85
86
|
```python
|
|
86
87
|
import asyncio
|
|
87
|
-
from cq import CommandBus
|
|
88
|
+
from cq import CommandBus
|
|
89
|
+
from cq.default import command_handler
|
|
88
90
|
from dataclasses import dataclass
|
|
89
91
|
from injection import inject
|
|
90
92
|
|
|
@@ -7,7 +7,12 @@ from ._core.message import (
|
|
|
7
7
|
Query,
|
|
8
8
|
QueryBus,
|
|
9
9
|
)
|
|
10
|
-
from ._core.middleware import
|
|
10
|
+
from ._core.middleware import (
|
|
11
|
+
Middleware,
|
|
12
|
+
MiddlewareResult,
|
|
13
|
+
resolve_handler_source,
|
|
14
|
+
through_middlewares,
|
|
15
|
+
)
|
|
11
16
|
from ._core.queuing.pump import Pump
|
|
12
17
|
from ._core.queuing.queues.abc import Consumer, Delivery, Producer, Queue
|
|
13
18
|
from ._core.queuing.queues.memory import MemoryQueue
|
|
@@ -45,36 +50,7 @@ __all__ = (
|
|
|
45
50
|
"Queue",
|
|
46
51
|
"RelatedEvents",
|
|
47
52
|
"Router",
|
|
48
|
-
"__router__",
|
|
49
|
-
"command_handler",
|
|
50
53
|
"dispatch_sequentially",
|
|
51
|
-
"event_handler",
|
|
52
|
-
"new_command_bus",
|
|
53
|
-
"new_event_bus",
|
|
54
|
-
"new_query_bus",
|
|
55
|
-
"query_handler",
|
|
56
54
|
"resolve_handler_source",
|
|
55
|
+
"through_middlewares",
|
|
57
56
|
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
from .ext.injection import InjectionAdapter
|
|
62
|
-
|
|
63
|
-
except ImportError: # pragma: no cover
|
|
64
|
-
__router__ = Router()
|
|
65
|
-
|
|
66
|
-
else:
|
|
67
|
-
__router__ = Router(InjectionAdapter())
|
|
68
|
-
del InjectionAdapter
|
|
69
|
-
|
|
70
|
-
__router__.register_defaults()
|
|
71
|
-
|
|
72
|
-
command_handler = __router__.command_handler
|
|
73
|
-
event_handler = __router__.event_handler
|
|
74
|
-
query_handler = __router__.query_handler
|
|
75
|
-
|
|
76
|
-
new_command_bus = __router__.new_command_bus
|
|
77
|
-
new_event_bus = __router__.new_event_bus
|
|
78
|
-
new_query_bus = __router__.new_query_bus
|
|
79
|
-
|
|
80
|
-
ContextCommandPipeline._set_default_di(__router__.di)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
+
from functools import wraps
|
|
3
4
|
from inspect import isasyncgenfunction
|
|
4
5
|
from typing import Any, Concatenate, Self, TypeGuard
|
|
5
6
|
|
|
6
|
-
from cq._core.routing.handler import
|
|
7
|
+
from cq._core.routing.handler import HandlerEntry, HandlerType
|
|
7
8
|
from cq.exceptions import MiddlewareError
|
|
8
9
|
|
|
9
10
|
type MiddlewareResult[T] = AsyncGenerator[None, T]
|
|
@@ -23,6 +24,13 @@ class MiddlewareGroup[**P, T]:
|
|
|
23
24
|
init=False,
|
|
24
25
|
)
|
|
25
26
|
|
|
27
|
+
def __call__(self, wrapped: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
28
|
+
@wraps(wrapped)
|
|
29
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
30
|
+
return await self.invoke(wrapped, *args, **kwargs)
|
|
31
|
+
|
|
32
|
+
return wrapper
|
|
33
|
+
|
|
26
34
|
def add(self, *middlewares: Middleware[P, T]) -> Self:
|
|
27
35
|
classic_middlewares = reversed(
|
|
28
36
|
tuple(self.__normalize(middleware) for middleware in middlewares)
|
|
@@ -48,6 +56,10 @@ class MiddlewareGroup[**P, T]:
|
|
|
48
56
|
|
|
49
57
|
return handler
|
|
50
58
|
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_middlewares(cls, *middlewares: Middleware[P, T]) -> Self:
|
|
61
|
+
return cls().add(*middlewares)
|
|
62
|
+
|
|
51
63
|
@staticmethod
|
|
52
64
|
def __normalize(middleware: Middleware[P, T]) -> ClassicMiddleware[P, T]:
|
|
53
65
|
if _is_gen_middleware(middleware):
|
|
@@ -56,6 +68,14 @@ class MiddlewareGroup[**P, T]:
|
|
|
56
68
|
return middleware # type: ignore[return-value]
|
|
57
69
|
|
|
58
70
|
|
|
71
|
+
def through_middlewares[**P, T](
|
|
72
|
+
first_middleware: Middleware[P, T],
|
|
73
|
+
/,
|
|
74
|
+
*middlewares: Middleware[P, T],
|
|
75
|
+
) -> MiddlewareGroup[P, T]:
|
|
76
|
+
return MiddlewareGroup.from_middlewares(first_middleware, *middlewares)
|
|
77
|
+
|
|
78
|
+
|
|
59
79
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
60
80
|
class _BoundMiddleware[**P, T]:
|
|
61
81
|
call_next: Callable[P, Awaitable[T]]
|
|
@@ -66,15 +86,13 @@ class _BoundMiddleware[**P, T]:
|
|
|
66
86
|
|
|
67
87
|
|
|
68
88
|
def resolve_handler_source[**P, T](
|
|
69
|
-
call_next: Callable[P, Awaitable[T]]
|
|
70
|
-
| _BoundMiddleware[P, T]
|
|
71
|
-
| HandleFunction[P, T],
|
|
89
|
+
call_next: Callable[P, Awaitable[T]] | _BoundMiddleware[P, T] | HandlerEntry[P, T],
|
|
72
90
|
/,
|
|
73
91
|
) -> HandlerType[P, T] | Any:
|
|
74
92
|
while isinstance(call_next, _BoundMiddleware):
|
|
75
93
|
call_next = call_next.call_next
|
|
76
94
|
|
|
77
|
-
if isinstance(call_next,
|
|
95
|
+
if isinstance(call_next, HandlerEntry):
|
|
78
96
|
return call_next.source
|
|
79
97
|
|
|
80
98
|
return call_next
|
|
@@ -21,7 +21,14 @@ class ContextCommandPipeline[C: Command](ContextPipeline[C]):
|
|
|
21
21
|
__default_di: ClassVar[DIAdapter]
|
|
22
22
|
|
|
23
23
|
def __init__(self, di: DIAdapter | None = None, /) -> None:
|
|
24
|
-
|
|
24
|
+
try:
|
|
25
|
+
di = di or self.__default_di
|
|
26
|
+
except AttributeError as exc:
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
"No DI adapter: import `cq.default` to use the default router, "
|
|
29
|
+
"or build the pipeline with `router.command_pipeline()`."
|
|
30
|
+
) from exc
|
|
31
|
+
|
|
25
32
|
super().__init__(LazyDispatcher(CommandBus, di))
|
|
26
33
|
self.__query_dispatcher = LazyDispatcher(QueryBus, di)
|
|
27
34
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
from collections.abc import Awaitable, Callable, Iterator
|
|
3
4
|
from typing import Any, Concatenate, Protocol, Self, runtime_checkable
|
|
@@ -8,13 +9,17 @@ from anyio.abc import TaskGroup
|
|
|
8
9
|
from cq._core.middleware import Middleware
|
|
9
10
|
from cq._core.routing.dispatchers.abc import BaseDispatcher, Dispatcher
|
|
10
11
|
from cq._core.routing.handler import (
|
|
11
|
-
|
|
12
|
-
HandlerFactory,
|
|
12
|
+
HandlerEntry,
|
|
13
13
|
HandlerRegistry,
|
|
14
14
|
MultipleHandlerRegistry,
|
|
15
15
|
SingleHandlerRegistry,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
if sys.version_info >= (3, 13): # pragma: no cover
|
|
19
|
+
from warnings import deprecated
|
|
20
|
+
else: # pragma: no cover
|
|
21
|
+
from typing_extensions import deprecated
|
|
22
|
+
|
|
18
23
|
type Listener[T] = Callable[[T], Awaitable[Any]]
|
|
19
24
|
|
|
20
25
|
|
|
@@ -30,11 +35,16 @@ class Bus[I, O](Dispatcher[I, O], Protocol):
|
|
|
30
35
|
def add_middlewares(self, *middlewares: Middleware[Concatenate[I, ...], O]) -> Self:
|
|
31
36
|
raise NotImplementedError
|
|
32
37
|
|
|
38
|
+
@deprecated(
|
|
39
|
+
"`Bus.subscribe` is intended for tests, register handlers with the router "
|
|
40
|
+
"decorators (`command_handler`, `event_handler`, `query_handler`) instead.",
|
|
41
|
+
category=None,
|
|
42
|
+
)
|
|
33
43
|
@abstractmethod
|
|
34
44
|
def subscribe(
|
|
35
45
|
self,
|
|
36
46
|
message_type: type[I],
|
|
37
|
-
|
|
47
|
+
function: Callable[Concatenate[I, ...], Awaitable[O]],
|
|
38
48
|
fail_silently: bool = ...,
|
|
39
49
|
) -> Self:
|
|
40
50
|
raise NotImplementedError
|
|
@@ -58,16 +68,19 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
|
|
|
58
68
|
def subscribe(
|
|
59
69
|
self,
|
|
60
70
|
message_type: type[I],
|
|
61
|
-
|
|
71
|
+
function: Callable[Concatenate[I, ...], Awaitable[O]],
|
|
62
72
|
fail_silently: bool = False,
|
|
63
73
|
) -> Self:
|
|
64
|
-
self.__registry.subscribe(
|
|
74
|
+
self.__registry.subscribe(
|
|
75
|
+
message_type,
|
|
76
|
+
HandlerEntry.from_function(function, fail_silently),
|
|
77
|
+
)
|
|
65
78
|
return self
|
|
66
79
|
|
|
67
80
|
def _handlers_from(
|
|
68
81
|
self,
|
|
69
82
|
message_type: type[I],
|
|
70
|
-
) -> Iterator[
|
|
83
|
+
) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
|
|
71
84
|
return self.__registry.handlers_from(message_type)
|
|
72
85
|
|
|
73
86
|
def _trigger_listeners(self, message: I, /, task_group: TaskGroup) -> None:
|
|
@@ -3,7 +3,7 @@ from collections import defaultdict
|
|
|
3
3
|
from collections.abc import Awaitable, Callable, Iterator, KeysView
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from functools import partial
|
|
6
|
-
from inspect import Parameter, isclass, unwrap
|
|
6
|
+
from inspect import Parameter, isclass, isfunction, unwrap
|
|
7
7
|
from inspect import signature as inspect_signature
|
|
8
8
|
from typing import (
|
|
9
9
|
TYPE_CHECKING,
|
|
@@ -34,23 +34,43 @@ class Handler[**P, T](Protocol):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
37
|
-
class
|
|
38
|
-
|
|
37
|
+
class HandlerEntry[**P, T]:
|
|
38
|
+
function: Callable[P, Awaitable[T]]
|
|
39
39
|
source: HandlerType[P, T] | Any
|
|
40
40
|
fail_silently: bool
|
|
41
41
|
|
|
42
42
|
async def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
|
|
43
|
-
|
|
44
|
-
return await handler.handle(*args, **kwargs)
|
|
43
|
+
return await self.function(*args, **kwargs)
|
|
45
44
|
|
|
46
45
|
@classmethod
|
|
47
|
-
def
|
|
46
|
+
def from_factory(
|
|
48
47
|
cls,
|
|
49
48
|
factory: HandlerFactory[P, T],
|
|
49
|
+
/,
|
|
50
50
|
source: HandlerType[P, T] | None = None,
|
|
51
51
|
fail_silently: bool = False,
|
|
52
52
|
) -> Self:
|
|
53
|
-
|
|
53
|
+
function = partial(cls.__handle_with, factory)
|
|
54
|
+
return cls(function, source or unwrap(factory), fail_silently)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_function(
|
|
58
|
+
cls,
|
|
59
|
+
function: Callable[P, Awaitable[T]],
|
|
60
|
+
/,
|
|
61
|
+
fail_silently: bool = False,
|
|
62
|
+
) -> Self:
|
|
63
|
+
return cls(function, function, fail_silently)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
async def __handle_with(
|
|
67
|
+
factory: HandlerFactory[P, T],
|
|
68
|
+
/,
|
|
69
|
+
*args: P.args,
|
|
70
|
+
**kwargs: P.kwargs,
|
|
71
|
+
) -> T:
|
|
72
|
+
handler = await factory()
|
|
73
|
+
return await handler.handle(*args, **kwargs)
|
|
54
74
|
|
|
55
75
|
|
|
56
76
|
@runtime_checkable
|
|
@@ -66,23 +86,21 @@ class HandlerRegistry[I, O](Protocol):
|
|
|
66
86
|
def handlers_from(
|
|
67
87
|
self,
|
|
68
88
|
message_type: type[I],
|
|
69
|
-
) -> Iterator[
|
|
89
|
+
) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
|
|
70
90
|
raise NotImplementedError
|
|
71
91
|
|
|
72
92
|
@abstractmethod
|
|
73
93
|
def subscribe(
|
|
74
94
|
self,
|
|
75
95
|
message_type: type[I],
|
|
76
|
-
|
|
77
|
-
handler_type: HandlerType[Concatenate[I, ...], O] | None = ...,
|
|
78
|
-
fail_silently: bool = ...,
|
|
96
|
+
entry: HandlerEntry[Concatenate[I, ...], O],
|
|
79
97
|
) -> Self:
|
|
80
98
|
raise NotImplementedError
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
84
102
|
class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
85
|
-
__values: dict[type[I], list[
|
|
103
|
+
__values: dict[type[I], list[HandlerEntry[Concatenate[I, ...], O]]] = field(
|
|
86
104
|
default_factory=partial(defaultdict, list),
|
|
87
105
|
init=False,
|
|
88
106
|
)
|
|
@@ -94,28 +112,24 @@ class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
|
94
112
|
def handlers_from(
|
|
95
113
|
self,
|
|
96
114
|
message_type: type[I],
|
|
97
|
-
) -> Iterator[
|
|
115
|
+
) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
|
|
98
116
|
for key_type in _iter_key_types(message_type):
|
|
99
117
|
yield from self.__values.get(key_type, ())
|
|
100
118
|
|
|
101
119
|
def subscribe(
|
|
102
120
|
self,
|
|
103
121
|
message_type: type[I],
|
|
104
|
-
|
|
105
|
-
handler_type: HandlerType[Concatenate[I, ...], O] | None = None,
|
|
106
|
-
fail_silently: bool = False,
|
|
122
|
+
entry: HandlerEntry[Concatenate[I, ...], O],
|
|
107
123
|
) -> Self:
|
|
108
|
-
function = HandleFunction.create(handler_factory, handler_type, fail_silently)
|
|
109
|
-
|
|
110
124
|
for key_type in _build_key_types(message_type):
|
|
111
|
-
self.__values[key_type].append(
|
|
125
|
+
self.__values[key_type].append(entry)
|
|
112
126
|
|
|
113
127
|
return self
|
|
114
128
|
|
|
115
129
|
|
|
116
130
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
117
131
|
class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
118
|
-
__values: dict[type[I],
|
|
132
|
+
__values: dict[type[I], HandlerEntry[Concatenate[I, ...], O]] = field(
|
|
119
133
|
default_factory=dict,
|
|
120
134
|
init=False,
|
|
121
135
|
)
|
|
@@ -127,21 +141,18 @@ class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
|
|
|
127
141
|
def handlers_from(
|
|
128
142
|
self,
|
|
129
143
|
message_type: type[I],
|
|
130
|
-
) -> Iterator[
|
|
144
|
+
) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
|
|
131
145
|
for key_type in _iter_key_types(message_type):
|
|
132
|
-
|
|
133
|
-
if
|
|
134
|
-
yield
|
|
146
|
+
entry = self.__values.get(key_type, None)
|
|
147
|
+
if entry is not None:
|
|
148
|
+
yield entry
|
|
135
149
|
|
|
136
150
|
def subscribe(
|
|
137
151
|
self,
|
|
138
152
|
message_type: type[I],
|
|
139
|
-
|
|
140
|
-
handler_type: HandlerType[Concatenate[I, ...], O] | None = None,
|
|
141
|
-
fail_silently: bool = False,
|
|
153
|
+
entry: HandlerEntry[Concatenate[I, ...], O],
|
|
142
154
|
) -> Self:
|
|
143
|
-
|
|
144
|
-
entries = {key_type: function for key_type in _build_key_types(message_type)}
|
|
155
|
+
entries = {key_type: entry for key_type in _build_key_types(message_type)}
|
|
145
156
|
|
|
146
157
|
for key_type in entries:
|
|
147
158
|
if key_type in self.__values:
|
|
@@ -163,7 +174,7 @@ class HandlerDecorator[I, O]:
|
|
|
163
174
|
@overload
|
|
164
175
|
def __call__(
|
|
165
176
|
self,
|
|
166
|
-
|
|
177
|
+
message_type: type[I],
|
|
167
178
|
/,
|
|
168
179
|
*,
|
|
169
180
|
fail_silently: bool = ...,
|
|
@@ -172,7 +183,7 @@ class HandlerDecorator[I, O]:
|
|
|
172
183
|
@overload
|
|
173
184
|
def __call__[T](
|
|
174
185
|
self,
|
|
175
|
-
|
|
186
|
+
handler: T,
|
|
176
187
|
/,
|
|
177
188
|
*,
|
|
178
189
|
fail_silently: bool = ...,
|
|
@@ -181,7 +192,7 @@ class HandlerDecorator[I, O]:
|
|
|
181
192
|
@overload
|
|
182
193
|
def __call__(
|
|
183
194
|
self,
|
|
184
|
-
|
|
195
|
+
nothing: None = ...,
|
|
185
196
|
/,
|
|
186
197
|
*,
|
|
187
198
|
fail_silently: bool = ...,
|
|
@@ -189,40 +200,55 @@ class HandlerDecorator[I, O]:
|
|
|
189
200
|
|
|
190
201
|
def __call__[T](
|
|
191
202
|
self,
|
|
192
|
-
|
|
203
|
+
message_or_handler: type[I] | T | None = None,
|
|
193
204
|
/,
|
|
194
205
|
*,
|
|
195
206
|
fail_silently: bool = False,
|
|
196
207
|
) -> Any:
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
and isclass(message_or_handler_type)
|
|
200
|
-
and issubclass(message_or_handler_type, Handler)
|
|
201
|
-
):
|
|
202
|
-
return self.__decorator(
|
|
203
|
-
message_or_handler_type, # type: ignore[arg-type]
|
|
204
|
-
fail_silently=fail_silently,
|
|
205
|
-
)
|
|
208
|
+
if self.__try_subscribe(message_or_handler, None, fail_silently):
|
|
209
|
+
return message_or_handler
|
|
206
210
|
|
|
207
211
|
return partial(
|
|
208
212
|
self.__decorator,
|
|
209
|
-
message_type=
|
|
213
|
+
message_type=message_or_handler, # type: ignore[arg-type]
|
|
210
214
|
fail_silently=fail_silently,
|
|
211
215
|
)
|
|
212
216
|
|
|
213
|
-
def __decorator(
|
|
217
|
+
def __decorator[T](
|
|
214
218
|
self,
|
|
215
|
-
wrapped:
|
|
219
|
+
wrapped: T,
|
|
216
220
|
/,
|
|
217
221
|
*,
|
|
218
|
-
message_type: type[I] | None
|
|
219
|
-
fail_silently: bool
|
|
220
|
-
) ->
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
message_type: type[I] | None,
|
|
223
|
+
fail_silently: bool,
|
|
224
|
+
) -> T:
|
|
225
|
+
if not self.__try_subscribe(wrapped, message_type, fail_silently):
|
|
226
|
+
raise TypeError(f"`{wrapped}` isn't a handler.")
|
|
227
|
+
|
|
224
228
|
return wrapped
|
|
225
229
|
|
|
230
|
+
def __try_subscribe(
|
|
231
|
+
self,
|
|
232
|
+
handler: Any,
|
|
233
|
+
message_type: type[I] | None,
|
|
234
|
+
fail_silently: bool,
|
|
235
|
+
) -> bool:
|
|
236
|
+
if isfunction(handler):
|
|
237
|
+
entry = HandlerEntry.from_function(handler, fail_silently)
|
|
238
|
+
callee = handler
|
|
239
|
+
|
|
240
|
+
elif isclass(handler) and issubclass(handler, Handler):
|
|
241
|
+
factory = self.di.wire(handler)
|
|
242
|
+
entry = HandlerEntry.from_factory(factory, handler, fail_silently)
|
|
243
|
+
callee = handler.handle.__get__(NotImplemented, handler)
|
|
244
|
+
|
|
245
|
+
else:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
message_type = message_type or _resolve_message_type(callee)
|
|
249
|
+
self.registry.subscribe(message_type, entry)
|
|
250
|
+
return True
|
|
251
|
+
|
|
226
252
|
|
|
227
253
|
def _build_key_types(message_type: Any) -> tuple[Any, ...]:
|
|
228
254
|
config = MatchingTypesConfig(ignore_none=True)
|
|
@@ -239,10 +265,9 @@ def _iter_key_types(message_type: Any) -> Iterator[Any]:
|
|
|
239
265
|
|
|
240
266
|
|
|
241
267
|
def _resolve_message_type[I, O](
|
|
242
|
-
|
|
268
|
+
handler: Callable[Concatenate[I, ...], Awaitable[O]],
|
|
243
269
|
) -> type[I]:
|
|
244
|
-
|
|
245
|
-
signature = inspect_signature(fake_method, eval_str=True)
|
|
270
|
+
signature = inspect_signature(handler, eval_str=True)
|
|
246
271
|
|
|
247
272
|
for parameter in signature.parameters.values():
|
|
248
273
|
message_type = parameter.annotation
|
|
@@ -253,6 +278,6 @@ def _resolve_message_type[I, O](
|
|
|
253
278
|
return message_type
|
|
254
279
|
|
|
255
280
|
raise TypeError(
|
|
256
|
-
f"Unable to resolve message type for handler `{
|
|
281
|
+
f"Unable to resolve message type for handler `{handler}`, "
|
|
257
282
|
"`handle` method must have a type annotation for its first parameter."
|
|
258
283
|
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
3
|
+
from ._core.routing.command_pipeline import ContextCommandPipeline
|
|
4
|
+
from ._core.routing.di import DIAdapter
|
|
5
|
+
from ._core.routing.router import Router
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"command_handler",
|
|
9
|
+
"event_handler",
|
|
10
|
+
"new_command_bus",
|
|
11
|
+
"new_event_bus",
|
|
12
|
+
"new_query_bus",
|
|
13
|
+
"query_handler",
|
|
14
|
+
"router",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __detect_di_adapter() -> DIAdapter | None:
|
|
19
|
+
with suppress(ImportError):
|
|
20
|
+
from .ext.injection import InjectionAdapter
|
|
21
|
+
|
|
22
|
+
return InjectionAdapter()
|
|
23
|
+
|
|
24
|
+
return None # pragma: no cover
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
router = Router(__detect_di_adapter()).register_defaults()
|
|
28
|
+
|
|
29
|
+
command_handler = router.command_handler
|
|
30
|
+
event_handler = router.event_handler
|
|
31
|
+
query_handler = router.query_handler
|
|
32
|
+
|
|
33
|
+
new_command_bus = router.new_command_bus
|
|
34
|
+
new_event_bus = router.new_event_bus
|
|
35
|
+
new_query_bus = router.new_query_bus
|
|
36
|
+
|
|
37
|
+
ContextCommandPipeline._set_default_di(router.di)
|
|
@@ -20,7 +20,7 @@ test = [
|
|
|
20
20
|
|
|
21
21
|
[project]
|
|
22
22
|
name = "python-cq"
|
|
23
|
-
version = "0.
|
|
23
|
+
version = "0.27.0"
|
|
24
24
|
description = "CQRS library for async Python projects."
|
|
25
25
|
license = "MIT"
|
|
26
26
|
license-files = ["LICENSE"]
|
|
@@ -48,6 +48,7 @@ classifiers = [
|
|
|
48
48
|
dependencies = [
|
|
49
49
|
"anyio",
|
|
50
50
|
"type-analyzer",
|
|
51
|
+
"typing-extensions; python_version < '3.13'",
|
|
51
52
|
]
|
|
52
53
|
|
|
53
54
|
[project.optional-dependencies]
|
|
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
|
|
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
|