python-cq 0.18.0__tar.gz → 0.19.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.
Files changed (35) hide show
  1. {python_cq-0.18.0 → python_cq-0.19.0}/PKG-INFO +1 -1
  2. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/dispatchers/abc.py +9 -10
  3. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/dispatchers/bus.py +3 -7
  4. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/dispatchers/pipe.py +1 -1
  5. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/middleware.py +15 -0
  6. python_cq-0.19.0/cq/_core/pump.py +56 -0
  7. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/queues/memory.py +22 -5
  8. python_cq-0.19.0/cq/middlewares/exc.py +56 -0
  9. {python_cq-0.18.0 → python_cq-0.19.0}/pyproject.toml +1 -1
  10. python_cq-0.18.0/cq/_core/pump.py +0 -34
  11. {python_cq-0.18.0 → python_cq-0.19.0}/.gitignore +0 -0
  12. {python_cq-0.18.0 → python_cq-0.19.0}/LICENSE +0 -0
  13. {python_cq-0.18.0 → python_cq-0.19.0}/cq/__init__.py +0 -0
  14. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/__init__.py +0 -0
  15. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/common/__init__.py +0 -0
  16. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/common/typing.py +0 -0
  17. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/cq.py +0 -0
  18. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/di.py +0 -0
  19. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/dispatchers/__init__.py +0 -0
  20. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/dispatchers/lazy.py +0 -0
  21. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/handler.py +0 -0
  22. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/message.py +0 -0
  23. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/middlewares/__init__.py +0 -0
  24. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/middlewares/scope.py +0 -0
  25. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/pipetools.py +0 -0
  26. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/queues/__init__.py +0 -0
  27. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/queues/abc.py +0 -0
  28. {python_cq-0.18.0 → python_cq-0.19.0}/cq/_core/related_events.py +0 -0
  29. {python_cq-0.18.0 → python_cq-0.19.0}/cq/exceptions.py +0 -0
  30. {python_cq-0.18.0 → python_cq-0.19.0}/cq/ext/__init__.py +0 -0
  31. {python_cq-0.18.0 → python_cq-0.19.0}/cq/ext/injection.py +0 -0
  32. {python_cq-0.18.0 → python_cq-0.19.0}/cq/middlewares/__init__.py +0 -0
  33. {python_cq-0.18.0 → python_cq-0.19.0}/cq/middlewares/retry.py +0 -0
  34. {python_cq-0.18.0 → python_cq-0.19.0}/cq/py.typed +0 -0
  35. {python_cq-0.18.0 → python_cq-0.19.0}/docs/index.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.18.0
3
+ Version: 0.19.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,7 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from collections.abc import Awaitable, Callable
3
3
  from typing import Protocol, Self, runtime_checkable
4
4
 
5
- from cq._core.middleware import Middleware, MiddlewareGroup
5
+ from cq._core.middleware import Middleware, MiddlewareGroup, deliver_message
6
6
 
7
7
 
8
8
  @runtime_checkable
@@ -29,17 +29,16 @@ class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
29
29
  self.__middleware_group.add(*middlewares)
30
30
  return self
31
31
 
32
- async def _invoke_with_middlewares(
32
+ async def _deliver(
33
33
  self,
34
- handler: Callable[[I], Awaitable[O]],
35
34
  message: I,
35
+ handler: Callable[[I], Awaitable[O]],
36
36
  /,
37
37
  fail_silently: bool = False,
38
38
  ) -> O:
39
- try:
40
- return await self.__middleware_group.invoke(handler, message)
41
- except Exception:
42
- if fail_silently:
43
- return NotImplemented
44
-
45
- raise
39
+ return await deliver_message(
40
+ message,
41
+ handler,
42
+ self.__middleware_group,
43
+ fail_silently,
44
+ )
@@ -83,11 +83,7 @@ class SimpleBus[I, O](BaseBus[I, O]):
83
83
  self._trigger_listeners(message, task_group)
84
84
 
85
85
  for handler in self._handlers_from(type(message)):
86
- return await self._invoke_with_middlewares(
87
- handler,
88
- message,
89
- handler.fail_silently,
90
- )
86
+ return await self._deliver(message, handler, handler.fail_silently)
91
87
 
92
88
  return NotImplemented
93
89
 
@@ -104,8 +100,8 @@ class TaskBus[I](BaseBus[I, None]):
104
100
 
105
101
  for handler in self._handlers_from(type(message)):
106
102
  task_group.start_soon(
107
- self._invoke_with_middlewares,
108
- handler,
103
+ self._deliver,
109
104
  message,
105
+ handler,
110
106
  handler.fail_silently,
111
107
  )
@@ -140,7 +140,7 @@ class Pipe[I, O](BaseDispatcher[I, O]):
140
140
  return self
141
141
 
142
142
  async def dispatch(self, message: I, /) -> O:
143
- return await self._invoke_with_middlewares(self.__steps.execute, message)
143
+ return await self._deliver(message, self.__steps.execute)
144
144
 
145
145
 
146
146
  class ContextPipeline[I]:
@@ -115,6 +115,21 @@ class _GeneratorMiddleware[**P, T]:
115
115
  return value
116
116
 
117
117
 
118
+ async def deliver_message[I, O](
119
+ message: I,
120
+ handler: Callable[[I], Awaitable[O]],
121
+ middleware_group: MiddlewareGroup[[I], O],
122
+ fail_silently: bool = False,
123
+ ) -> O:
124
+ try:
125
+ return await middleware_group.invoke(handler, message)
126
+ except Exception:
127
+ if fail_silently:
128
+ return NotImplemented
129
+
130
+ raise
131
+
132
+
118
133
  def _is_gen_middleware[**P, T](
119
134
  middleware: Middleware[P, T],
120
135
  ) -> TypeGuard[GeneratorMiddleware[P, T]]:
@@ -0,0 +1,56 @@
1
+ from collections.abc import AsyncIterator, Awaitable, Callable
2
+ from contextlib import asynccontextmanager
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Self
5
+
6
+ import anyio
7
+
8
+ from cq._core.middleware import Middleware, MiddlewareGroup, deliver_message
9
+ from cq._core.queues.abc import Consumer
10
+
11
+
12
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
13
+ class Pump[T]:
14
+ consumer: Consumer[T]
15
+ dispatcher: Callable[[T], Awaitable[Any]]
16
+ fail_silently: bool = field(default=False)
17
+ __middleware_group: MiddlewareGroup[[T], Any] = field(
18
+ default_factory=MiddlewareGroup,
19
+ init=False,
20
+ )
21
+
22
+ def add_middlewares(self, *middlewares: Middleware[[T], Any]) -> Self:
23
+ self.__middleware_group.add(*middlewares)
24
+ return self
25
+
26
+ async def drain(self) -> None:
27
+ async for message in self.consumer:
28
+ await deliver_message(
29
+ message,
30
+ self.dispatcher,
31
+ self.__middleware_group,
32
+ self.fail_silently,
33
+ )
34
+
35
+ @asynccontextmanager
36
+ async def draining(
37
+ self,
38
+ /,
39
+ *,
40
+ concurrency: int | None = None,
41
+ graceful: bool = False,
42
+ ) -> AsyncIterator[None]:
43
+ if concurrency is None:
44
+ concurrency = 1
45
+ elif concurrency < 1:
46
+ raise ValueError(f"`concurrency` must be at least 1, got {concurrency}.")
47
+
48
+ async with anyio.create_task_group() as task_group:
49
+ for _ in range(concurrency):
50
+ task_group.start_soon(self.drain)
51
+
52
+ try:
53
+ yield
54
+ finally:
55
+ if not graceful:
56
+ task_group.cancel_scope.cancel()
@@ -1,10 +1,12 @@
1
- from collections.abc import AsyncIterator, Awaitable, Callable
1
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
2
2
  from contextlib import asynccontextmanager
3
+ from types import TracebackType
3
4
  from typing import Any, Self
4
5
 
5
6
  import anyio
6
7
  from anyio.abc import ObjectReceiveStream, ObjectSendStream
7
8
 
9
+ from cq._core.middleware import Middleware
8
10
  from cq._core.pump import Pump
9
11
  from cq._core.queues.abc import Queue
10
12
 
@@ -18,6 +20,17 @@ class MemoryQueue[T](Queue[T]):
18
20
  def __init__(self, maxsize: int = 0) -> None:
19
21
  self.__producer, self.__consumer = anyio.create_memory_object_stream(maxsize)
20
22
 
23
+ async def __aenter__(self) -> Self:
24
+ return self
25
+
26
+ async def __aexit__(
27
+ self,
28
+ exc_type: type[BaseException] | None,
29
+ exc_value: BaseException | None,
30
+ traceback: TracebackType | None,
31
+ ) -> None:
32
+ await self.close()
33
+
21
34
  def __aiter__(self) -> AsyncIterator[T]:
22
35
  return aiter(self.__consumer)
23
36
 
@@ -30,13 +43,17 @@ class MemoryQueue[T](Queue[T]):
30
43
  dispatcher: Callable[[T], Awaitable[Any]],
31
44
  /,
32
45
  *,
46
+ concurrency: int | None = None,
33
47
  fail_silently: bool = False,
48
+ middlewares: Sequence[Middleware[[T], Any]] = (),
34
49
  ) -> AsyncIterator[Self]:
35
- async with Pump(self, dispatcher, fail_silently).draining(graceful=True):
36
- try:
50
+ async with (
51
+ Pump(self, dispatcher, fail_silently)
52
+ .add_middlewares(*middlewares)
53
+ .draining(concurrency=concurrency, graceful=True)
54
+ ):
55
+ async with self:
37
56
  yield self
38
- finally:
39
- await self.close()
40
57
 
41
58
  async def send(self, message: T, /) -> None:
42
59
  await self.__producer.send(message)
@@ -0,0 +1,56 @@
1
+ from collections.abc import Awaitable, Callable, Sequence
2
+ from typing import Any, Concatenate, Self
3
+
4
+ from cq import MiddlewareResult
5
+
6
+ __all__ = ("CaptureExceptionMiddleware",)
7
+
8
+
9
+ class CaptureExceptionMiddleware[**P, Exc: BaseException]:
10
+ __slots__ = ("__exceptions", "__on_error", "__reraise")
11
+
12
+ __exceptions: tuple[type[Exc], ...]
13
+ __on_error: Callable[Concatenate[Exc, P], Awaitable[Any]]
14
+ __reraise: bool
15
+
16
+ def __init__(
17
+ self,
18
+ on_error: Callable[Concatenate[Exc, P], Awaitable[Any]],
19
+ /,
20
+ exceptions: Sequence[type[Exc]] | None = None,
21
+ reraise: bool = False,
22
+ ) -> None:
23
+ self.__exceptions = (Exception,) if exceptions is None else tuple(exceptions) # type: ignore[assignment]
24
+ self.__on_error = on_error
25
+ self.__reraise = reraise
26
+
27
+ async def __call__(
28
+ self,
29
+ /,
30
+ *args: P.args,
31
+ **kwargs: P.kwargs,
32
+ ) -> MiddlewareResult[Any]:
33
+ try:
34
+ yield
35
+ except self.__exceptions as exc:
36
+ await self.__on_error(exc, *args, **kwargs)
37
+ if self.__reraise:
38
+ raise
39
+
40
+ @classmethod
41
+ def sync(
42
+ cls,
43
+ on_error: Callable[Concatenate[Exc, P], Any],
44
+ /,
45
+ exceptions: Sequence[type[Exc]] | None = None,
46
+ reraise: bool = False,
47
+ ) -> Self:
48
+ async def async_on_error(
49
+ exception: Exc,
50
+ /,
51
+ *args: P.args,
52
+ **kwargs: P.kwargs,
53
+ ) -> Any:
54
+ return on_error(exception, *args, **kwargs)
55
+
56
+ return cls(async_on_error, exceptions, reraise)
@@ -20,7 +20,7 @@ test = [
20
20
 
21
21
  [project]
22
22
  name = "python-cq"
23
- version = "0.18.0"
23
+ version = "0.19.0"
24
24
  description = "CQRS library for async Python projects."
25
25
  license = "MIT"
26
26
  license-files = ["LICENSE"]
@@ -1,34 +0,0 @@
1
- from collections.abc import AsyncIterator, Awaitable, Callable
2
- from contextlib import asynccontextmanager
3
- from dataclasses import dataclass, field
4
- from typing import Any
5
-
6
- import anyio
7
-
8
- from cq._core.queues.abc import Consumer
9
-
10
-
11
- @dataclass(repr=False, eq=False, frozen=True, slots=True)
12
- class Pump[T]:
13
- consumer: Consumer[T]
14
- dispatcher: Callable[[T], Awaitable[Any]]
15
- fail_silently: bool = field(default=False)
16
-
17
- async def drain(self) -> None:
18
- async for message in self.consumer:
19
- try:
20
- await self.dispatcher(message)
21
- except Exception:
22
- if not self.fail_silently:
23
- raise
24
-
25
- @asynccontextmanager
26
- async def draining(self, /, *, graceful: bool = False) -> AsyncIterator[None]:
27
- async with anyio.create_task_group() as task_group:
28
- task_group.start_soon(self.drain)
29
-
30
- try:
31
- yield
32
- finally:
33
- if not graceful:
34
- task_group.cancel_scope.cancel()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes