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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Lightweight CQRS library.
5
5
  Project-URL: Repository, https://github.com/100nm/python-cq
6
6
  Author: remimd
@@ -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__(self, input_type: type[I], /) -> Any:
92
- def decorator(wrapped: type[Handler[[I], O]]) -> type[Handler[[I], O]]:
93
- if not isclass(wrapped) or not issubclass(wrapped, Handler):
94
- raise TypeError(f"`{wrapped}` isn't a valid handler.")
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
- return decorator
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
- on=CommandBus,
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
- bus.add_middlewares(InjectionScopeMiddleware(CQScope.ON_COMMAND))
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
- on=QueryBus,
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.ON_COMMAND, mode="fallback")
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 events:
37
+ if not instance:
36
38
  return
37
39
 
38
40
  async with anyio.create_task_group() as task_group:
39
- for event in events:
41
+ for event in instance.items:
40
42
  task_group.start_soon(event_bus.dispatch, event)
@@ -2,4 +2,4 @@ from enum import StrEnum, auto
2
2
 
3
3
 
4
4
  class CQScope(StrEnum):
5
- ON_COMMAND = auto()
5
+ TRANSACTION = auto()
@@ -1,4 +1,4 @@
1
- from collections.abc import Iterable
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: Iterable[type[BaseException]] = (Exception,),
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 as exc:
35
+ except self.__exceptions:
36
36
  if attempt == retry:
37
- raise exc
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.6.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