python-cq 0.6.0__tar.gz → 0.7.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.6.0 → python_cq-0.7.0}/PKG-INFO +1 -1
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/handler.py +45 -10
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/message.py +10 -19
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/related_events.py +6 -4
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/scope.py +1 -1
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/middlewares/retry.py +4 -4
- python_cq-0.7.0/cq/middlewares/scope.py +32 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/pyproject.toml +2 -1
- python_cq-0.6.0/cq/middlewares/scope.py +0 -23
- {python_cq-0.6.0 → python_cq-0.7.0}/.gitignore +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/README.md +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/__init__.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/__init__.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/dispatcher/__init__.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/dispatcher/base.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/dispatcher/bus.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/dispatcher/pipe.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/_core/middleware.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/exceptions.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/middlewares/__init__.py +0 -0
- {python_cq-0.6.0 → python_cq-0.7.0}/cq/py.typed +0 -0
|
@@ -3,7 +3,8 @@ from collections import defaultdict
|
|
|
3
3
|
from collections.abc import Awaitable, Callable, Iterator
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from functools import partial
|
|
6
|
-
from inspect import getmro, isclass
|
|
6
|
+
from inspect import Parameter, getmro, isclass
|
|
7
|
+
from inspect import signature as inspect_signature
|
|
7
8
|
from typing import Any, Protocol, Self, runtime_checkable
|
|
8
9
|
|
|
9
10
|
import injection
|
|
@@ -88,16 +89,50 @@ class HandlerDecorator[I, O]:
|
|
|
88
89
|
manager: HandlerManager[I, O]
|
|
89
90
|
injection_module: injection.Module = field(default_factory=injection.mod)
|
|
90
91
|
|
|
91
|
-
def __call__(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
def __call__(
|
|
93
|
+
self,
|
|
94
|
+
input_or_handler_type: type[I] | HandlerType[[I], O] | None = None,
|
|
95
|
+
/,
|
|
96
|
+
) -> Any:
|
|
97
|
+
if input_or_handler_type is None:
|
|
98
|
+
return self.__decorator
|
|
99
|
+
|
|
100
|
+
elif isclass(input_or_handler_type) and issubclass(
|
|
101
|
+
input_or_handler_type,
|
|
102
|
+
Handler,
|
|
103
|
+
):
|
|
104
|
+
return self.__decorator(input_or_handler_type)
|
|
105
|
+
|
|
106
|
+
return partial(self.__decorator, input_type=input_or_handler_type) # type: ignore[arg-type]
|
|
107
|
+
|
|
108
|
+
def __decorator(
|
|
109
|
+
self,
|
|
110
|
+
wrapped: HandlerType[[I], O],
|
|
111
|
+
*,
|
|
112
|
+
input_type: type[I] | None = None,
|
|
113
|
+
) -> HandlerType[[I], O]:
|
|
114
|
+
factory = self.injection_module.make_async_factory(wrapped)
|
|
115
|
+
input_type = input_type or _resolve_input_type(wrapped)
|
|
116
|
+
self.manager.subscribe(input_type, factory)
|
|
117
|
+
return wrapped
|
|
95
118
|
|
|
96
|
-
factory = self.injection_module.make_async_factory(wrapped)
|
|
97
|
-
self.manager.subscribe(input_type, factory)
|
|
98
|
-
return wrapped
|
|
99
119
|
|
|
100
|
-
|
|
120
|
+
def _resolve_input_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]:
|
|
121
|
+
fake_handle_method = handler_type.handle.__get__(NotImplemented)
|
|
122
|
+
signature = inspect_signature(fake_handle_method, eval_str=True)
|
|
123
|
+
|
|
124
|
+
for parameter in signature.parameters.values():
|
|
125
|
+
input_type = parameter.annotation
|
|
126
|
+
|
|
127
|
+
if input_type is Parameter.empty:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
return input_type
|
|
131
|
+
|
|
132
|
+
raise TypeError(
|
|
133
|
+
f"Unable to resolve input type for handler `{handler_type}`, "
|
|
134
|
+
"`handle` method must have a type annotation for its first parameter."
|
|
135
|
+
)
|
|
101
136
|
|
|
102
137
|
|
|
103
138
|
def _make_handle_function[I, O](
|
|
@@ -106,6 +141,6 @@ def _make_handle_function[I, O](
|
|
|
106
141
|
return partial(__handle, factory=factory)
|
|
107
142
|
|
|
108
143
|
|
|
109
|
-
async def __handle[I, O](input_value: I, factory: HandlerFactory[[I], O]) -> O:
|
|
144
|
+
async def __handle[I, O](input_value: I, *, factory: HandlerFactory[[I], O]) -> O:
|
|
110
145
|
handler = await factory()
|
|
111
146
|
return await handler.handle(input_value)
|
|
@@ -33,31 +33,22 @@ query_handler: HandlerDecorator[Query, Any] = HandlerDecorator(
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
@injection.singleton(
|
|
37
|
-
|
|
38
|
-
ignore_type_hint=True, # type: ignore[call-arg]
|
|
39
|
-
inject=False,
|
|
40
|
-
mode="fallback",
|
|
41
|
-
)
|
|
42
|
-
def new_command_bus[T]() -> CommandBus[T]:
|
|
36
|
+
@injection.singleton(inject=False, mode="fallback")
|
|
37
|
+
def new_command_bus() -> CommandBus: # type: ignore[type-arg]
|
|
43
38
|
bus = SimpleBus(command_handler.manager)
|
|
44
|
-
|
|
39
|
+
transaction_scope_middleware = InjectionScopeMiddleware(
|
|
40
|
+
CQScope.TRANSACTION,
|
|
41
|
+
exist_ok=True,
|
|
42
|
+
)
|
|
43
|
+
bus.add_middlewares(transaction_scope_middleware)
|
|
45
44
|
return bus
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
@injection.singleton(
|
|
49
|
-
inject=False,
|
|
50
|
-
mode="fallback",
|
|
51
|
-
)
|
|
47
|
+
@injection.singleton(inject=False, mode="fallback")
|
|
52
48
|
def new_event_bus() -> EventBus:
|
|
53
49
|
return TaskBus(event_handler.manager)
|
|
54
50
|
|
|
55
51
|
|
|
56
|
-
@injection.singleton(
|
|
57
|
-
|
|
58
|
-
ignore_type_hint=True, # type: ignore[call-arg]
|
|
59
|
-
inject=False,
|
|
60
|
-
mode="fallback",
|
|
61
|
-
)
|
|
62
|
-
def new_query_bus[T]() -> QueryBus[T]:
|
|
52
|
+
@injection.singleton(inject=False, mode="fallback")
|
|
53
|
+
def new_query_bus() -> QueryBus: # type: ignore[type-arg]
|
|
63
54
|
return SimpleBus(query_handler.manager)
|
|
@@ -23,18 +23,20 @@ class RelatedEvents(Protocol):
|
|
|
23
23
|
class SimpleRelatedEvents(RelatedEvents):
|
|
24
24
|
items: list[Event] = field(default_factory=list)
|
|
25
25
|
|
|
26
|
+
def __bool__(self) -> bool:
|
|
27
|
+
return bool(self.items)
|
|
28
|
+
|
|
26
29
|
def add(self, *events: Event) -> None:
|
|
27
30
|
self.items.extend(events)
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
@injection.scoped(CQScope.
|
|
33
|
+
@injection.scoped(CQScope.TRANSACTION, mode="fallback")
|
|
31
34
|
async def related_events_recipe(event_bus: EventBus) -> AsyncIterator[RelatedEvents]:
|
|
32
35
|
yield (instance := SimpleRelatedEvents())
|
|
33
|
-
events = instance.items
|
|
34
36
|
|
|
35
|
-
if not
|
|
37
|
+
if not instance:
|
|
36
38
|
return
|
|
37
39
|
|
|
38
40
|
async with anyio.create_task_group() as task_group:
|
|
39
|
-
for event in
|
|
41
|
+
for event in instance.items:
|
|
40
42
|
task_group.start_soon(event_bus.dispatch, event)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import
|
|
1
|
+
from collections.abc import Sequence
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
import anyio
|
|
@@ -19,7 +19,7 @@ class RetryMiddleware:
|
|
|
19
19
|
self,
|
|
20
20
|
retry: int,
|
|
21
21
|
delay: float = 0,
|
|
22
|
-
exceptions:
|
|
22
|
+
exceptions: Sequence[type[BaseException]] = (Exception,),
|
|
23
23
|
) -> None:
|
|
24
24
|
self.__delay = delay
|
|
25
25
|
self.__exceptions = tuple(exceptions)
|
|
@@ -32,9 +32,9 @@ class RetryMiddleware:
|
|
|
32
32
|
try:
|
|
33
33
|
yield
|
|
34
34
|
|
|
35
|
-
except self.__exceptions
|
|
35
|
+
except self.__exceptions:
|
|
36
36
|
if attempt == retry:
|
|
37
|
-
raise
|
|
37
|
+
raise
|
|
38
38
|
|
|
39
39
|
else:
|
|
40
40
|
break
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
|
|
21
|
+
async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
|
|
22
|
+
async with AsyncExitStack() as stack:
|
|
23
|
+
try:
|
|
24
|
+
await stack.enter_async_context(
|
|
25
|
+
adefine_scope(self.scope_name),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
except ScopeAlreadyDefinedError:
|
|
29
|
+
if not self.exist_ok:
|
|
30
|
+
raise
|
|
31
|
+
|
|
32
|
+
yield
|
|
@@ -11,6 +11,7 @@ dev = [
|
|
|
11
11
|
example = [
|
|
12
12
|
"fastapi",
|
|
13
13
|
"msgspec",
|
|
14
|
+
"pydantic",
|
|
14
15
|
]
|
|
15
16
|
test = [
|
|
16
17
|
"pytest",
|
|
@@ -20,7 +21,7 @@ test = [
|
|
|
20
21
|
|
|
21
22
|
[project]
|
|
22
23
|
name = "python-cq"
|
|
23
|
-
version = "0.
|
|
24
|
+
version = "0.7.0"
|
|
24
25
|
description = "Lightweight CQRS library."
|
|
25
26
|
license = { text = "MIT" }
|
|
26
27
|
readme = "README.md"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
-
|
|
5
|
-
from injection import adefine_scope
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
8
|
-
from cq import MiddlewareResult
|
|
9
|
-
|
|
10
|
-
__all__ = ("InjectionScopeMiddleware",)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class InjectionScopeMiddleware:
|
|
14
|
-
__slots__ = ("__scope_name",)
|
|
15
|
-
|
|
16
|
-
__scope_name: str
|
|
17
|
-
|
|
18
|
-
def __init__(self, scope_name: str) -> None:
|
|
19
|
-
self.__scope_name = scope_name
|
|
20
|
-
|
|
21
|
-
async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
|
|
22
|
-
async with adefine_scope(self.__scope_name):
|
|
23
|
-
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
|