python-cq 0.21.1__py3-none-any.whl → 0.22.0__py3-none-any.whl

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.
cq/_core/cq.py CHANGED
@@ -10,7 +10,6 @@ from cq._core.handler import (
10
10
  SingleHandlerRegistry,
11
11
  )
12
12
  from cq._core.message import Command, Event, Query
13
- from cq._core.middlewares.scope import CommandDispatchScopeMiddleware
14
13
 
15
14
 
16
15
  class CQ:
@@ -57,8 +56,7 @@ class CQ:
57
56
 
58
57
  def new_command_bus(self) -> Bus[Command, Any]:
59
58
  bus = SimpleBus(self.__command_registry)
60
- command_middleware = CommandDispatchScopeMiddleware(self.__di)
61
- bus.add_middlewares(command_middleware)
59
+ bus.add_middlewares(self.__di.command_scope())
62
60
  return bus
63
61
 
64
62
  def new_event_bus(self) -> Bus[Event, None]:
cq/_core/di.py CHANGED
@@ -3,10 +3,12 @@ from __future__ import annotations
3
3
  from abc import abstractmethod
4
4
  from collections.abc import Awaitable, Callable
5
5
  from contextlib import nullcontext
6
- from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol, runtime_checkable
6
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
7
+
8
+ from cq.middlewares.contextlib import AsyncContextManagerMiddleware
7
9
 
8
10
  if TYPE_CHECKING: # pragma: no cover
9
- from cq import CommandBus, EventBus, QueryBus
11
+ from cq import Command, CommandBus, EventBus, Middleware, QueryBus
10
12
 
11
13
 
12
14
  @runtime_checkable
@@ -22,30 +24,19 @@ class DIAdapter(Protocol):
22
24
  __slots__ = ()
23
25
 
24
26
  @abstractmethod
25
- def command_scope(self) -> AsyncContextManager[None]:
27
+ def command_scope(self) -> Middleware[[Command], Any]:
26
28
  """
27
- Return an async context manager that delimits the lifetime of a
28
- command dispatch.
29
+ Return a middleware that wraps each command dispatch.
29
30
 
30
31
  **Responsibilities**
31
32
 
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.
33
+ The middleware must at minimum manage the lifecycle of a
34
+ ``RelatedEvents`` instance and register it so that it is resolvable
35
+ via injection for the duration of the dispatch.
44
36
 
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.
37
+ If you already have an async context manager for the scope, wrap it
38
+ with ``cq.middlewares.contextlib.AsyncContextManagerMiddleware``
39
+ instead of writing the middleware by hand.
49
40
  """
50
41
 
51
42
  raise NotImplementedError
@@ -97,8 +88,8 @@ class DIAdapter(Protocol):
97
88
  class NoDI(DIAdapter):
98
89
  __slots__ = ()
99
90
 
100
- def command_scope(self) -> AsyncContextManager[None]:
101
- return nullcontext()
91
+ def command_scope(self) -> Middleware[[Command], Any]:
92
+ return AsyncContextManagerMiddleware(nullcontext())
102
93
 
103
94
  def lazy[T](self, tp: type[T], /) -> Callable[[], Awaitable[T]]:
104
95
  tp_str = getattr(tp, "__name__", str(tp))
cq/_core/pipetools.py CHANGED
@@ -11,7 +11,6 @@ from cq._core.dispatchers.pipe import (
11
11
  ConvertMethodSync,
12
12
  )
13
13
  from cq._core.message import Command, CommandBus, Query, QueryBus
14
- from cq._core.middlewares.scope import CommandDispatchScopeMiddleware
15
14
 
16
15
 
17
16
  class ContextCommandPipeline[C: Command](ContextPipeline[C]):
@@ -22,8 +21,6 @@ class ContextCommandPipeline[C: Command](ContextPipeline[C]):
22
21
  def __init__(self, di: DIAdapter) -> None:
23
22
  super().__init__(LazyDispatcher(CommandBus, di))
24
23
  self.__query_dispatcher = LazyDispatcher(QueryBus, di)
25
- command_middleware = CommandDispatchScopeMiddleware(di)
26
- self.add_middlewares(command_middleware)
27
24
 
28
25
  def add_static_query_step[Q: Query](self, query: Q, /) -> Self:
29
26
  return self.add_static_step(query, dispatcher=self.__query_dispatcher)
cq/ext/injection.py CHANGED
@@ -1,15 +1,15 @@
1
1
  from collections.abc import AsyncIterator
2
- from contextlib import AsyncExitStack, asynccontextmanager
2
+ from contextlib import AsyncExitStack
3
3
  from dataclasses import dataclass, field
4
4
  from enum import StrEnum
5
- from typing import Any, AsyncContextManager, Awaitable, Callable
5
+ from typing import Any, Awaitable, Callable
6
6
 
7
7
  from injection import Module, adefine_scope, mod
8
8
  from injection.exceptions import ScopeAlreadyDefinedError
9
9
 
10
10
  from cq._core.di import DIAdapter
11
- from cq._core.message import CommandBus, EventBus, QueryBus
12
- from cq._core.middleware import MiddlewareResult
11
+ from cq._core.message import Command, CommandBus, EventBus, QueryBus
12
+ from cq._core.middleware import Middleware, MiddlewareResult
13
13
  from cq._core.related_events import AnyIORelatedEvents, RelatedEvents
14
14
 
15
15
  __all__ = ("CQScope", "InjectionAdapter", "InjectionScopeMiddleware")
@@ -24,12 +24,11 @@ class InjectionAdapter(DIAdapter):
24
24
  module: Module = field(default_factory=mod)
25
25
  threadsafe: bool | None = field(default=None)
26
26
 
27
- def command_scope(self) -> AsyncContextManager[None]:
28
- return InjectionScopeMiddleware( # type: ignore[return-value]
27
+ def command_scope(self) -> Middleware[[Command], Any]:
28
+ return InjectionScopeMiddleware(
29
29
  CQScope.COMMAND_DISPATCH,
30
- exist_ok=True,
31
30
  threadsafe=self.threadsafe,
32
- )._cm
31
+ )
33
32
 
34
33
  def lazy[T](self, tp: type[T], /) -> Callable[[], Awaitable[T]]:
35
34
  awaitable = self.module.aget_lazy_instance(tp, threadsafe=self.threadsafe)
@@ -83,12 +82,6 @@ class InjectionScopeMiddleware:
83
82
  threadsafe: bool | None = field(default=None, kw_only=True)
84
83
 
85
84
  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
85
  async with AsyncExitStack() as stack:
93
86
  try:
94
87
  await stack.enter_async_context(
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, AsyncContextManager, ContextManager
5
+
6
+ if TYPE_CHECKING: # pragma: no cover
7
+ from cq import MiddlewareResult
8
+
9
+ __all__ = ("AsyncContextManagerMiddleware", "ContextManagerMiddleware")
10
+
11
+
12
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
13
+ class AsyncContextManagerMiddleware:
14
+ context: AsyncContextManager[Any]
15
+
16
+ async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
17
+ async with self.context:
18
+ yield
19
+
20
+
21
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
22
+ class ContextManagerMiddleware:
23
+ context: ContextManager[Any]
24
+
25
+ async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
26
+ with self.context:
27
+ yield
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.21.1
3
+ Version: 0.22.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
@@ -2,12 +2,12 @@ cq/__init__.py,sha256=efZeCSuvUsBJ00dVLEs3DcNd1fdPhgYhH4xkCQVpO9Y,1963
2
2
  cq/exceptions.py,sha256=lmIpWDbPPwdS9bAAKxUvampA1KKmB706wWaB2mKZyPc,111
3
3
  cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  cq/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- cq/_core/cq.py,sha256=2D7zaovflEyGrrN6cOd4MqEllqzl_fFY4QgKsXETzp4,2438
6
- cq/_core/di.py,sha256=_LXnfCNemZMweInOA6IVjjnhc9z8F5A3cLImF8hT58Y,3681
5
+ cq/_core/cq.py,sha256=YDYcLMHAIlyCJ1NSm0sR_JnsVJ0wYZ1ndkUE0sArzic,2304
6
+ cq/_core/di.py,sha256=vqHvDTvXNRH8C_we1fKMicvQ6SuGM8aedHQaMrP6Llo,3369
7
7
  cq/_core/handler.py,sha256=2lc5B1WS3NOm_lJakOF9aTlto5Hh9cvJl6dppYknWzs,7276
8
8
  cq/_core/message.py,sha256=HpoYHstxRTWoUw_FuqDkytzyvK0Mb0U96Bz15ZEBd70,278
9
9
  cq/_core/middleware.py,sha256=RX_bLoqBL2p_dyCQs-jeozGVe6s7L9O4hDiwFxEprDY,3686
10
- cq/_core/pipetools.py,sha256=HGKWctffJliE6qdR9ER3BXiplpt98En_B-MQFd6aqEo,1766
10
+ cq/_core/pipetools.py,sha256=o2FqEfnIaoFwfY0maxI2rg6bK4vlBJK3KdHvQL4vPk0,1583
11
11
  cq/_core/pump.py,sha256=Kxrmdex1Yf2YhbA45e7n7Whwdp9fAi_npaAND8_cSrc,1749
12
12
  cq/_core/related_events.py,sha256=vPltQdmQjnfb3eOB2lO6dxqjJEsGOjOyCNj7_2ehNAk,1389
13
13
  cq/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,17 +17,16 @@ cq/_core/dispatchers/abc.py,sha256=E_amQ95TvcmicNRcUzftvRcqI0-Un-qqpm4XcepLyg0,1
17
17
  cq/_core/dispatchers/bus.py,sha256=eOhZXZxBOFS5Emx4h7a5ulgT5hWtyQtwxTIU5RqHRGw,3305
18
18
  cq/_core/dispatchers/lazy.py,sha256=byvwDZBBoTpnd40MCSTWNQ2ubyK_ib-pjxYoumqfSaY,715
19
19
  cq/_core/dispatchers/pipe.py,sha256=gdq0ihPlBgcAVSQrR0ovcFVdQhUB6Ejj6Lg5K76L2Rg,9723
20
- cq/_core/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- cq/_core/middlewares/scope.py,sha256=4LFH8uccow6LJa-1Eu8HiSjbz69D_T0dQJ7SpdPhLUA,405
22
20
  cq/_core/queues/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
21
  cq/_core/queues/abc.py,sha256=zXb9Nre9gaKPCbQbkUErnJGRn14UP1K7GpR_N7DFhkE,690
24
22
  cq/_core/queues/memory.py,sha256=VIZN3jIcnE7ij4NKhCJu6jEfvSgk5_EadtQejsxR2Lo,1811
25
23
  cq/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- cq/ext/injection.py,sha256=Lhrzy2hY7ZJG1lrvc3OHVTab3CfyRC-sz5xLPXfvfHA,3359
24
+ cq/ext/injection.py,sha256=6up8NqcUYwMjbGdxV653Ai4Bb49OckF6vIhocwz_bjk,3113
27
25
  cq/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ cq/middlewares/contextlib.py,sha256=8fXNI6Y3pOjLbDz8JRHb1KXU8cbPWksUV6qhcRCvd5s,821
28
27
  cq/middlewares/exc.py,sha256=ecAMoksj6B4osySSZ9g_cBR35_tiABQdpII-Drxkz44,1604
29
28
  cq/middlewares/retry.py,sha256=L6X_Wa2TZ0IYNXmi8oexTXQyM_lkjJt5Ew4D-2VzwBY,957
30
- python_cq-0.21.1.dist-info/METADATA,sha256=XzdL2djlbFj8hthtkHPlsYBbD3qs9cU9LX9KcjtBkDs,4507
31
- python_cq-0.21.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
32
- python_cq-0.21.1.dist-info/licenses/LICENSE,sha256=oC77BOa9kaaQni5rW-Z-ytz3E5h4EVg248BHg9UFgyg,1063
33
- python_cq-0.21.1.dist-info/RECORD,,
29
+ python_cq-0.22.0.dist-info/METADATA,sha256=XtA7nmM9n-qTSqojG1-YVSA3mGY87YTjC0ykeiYNQ90,4507
30
+ python_cq-0.22.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
31
+ python_cq-0.22.0.dist-info/licenses/LICENSE,sha256=oC77BOa9kaaQni5rW-Z-ytz3E5h4EVg248BHg9UFgyg,1063
32
+ python_cq-0.22.0.dist-info/RECORD,,
File without changes
@@ -1,14 +0,0 @@
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