python-cq 0.18.0__tar.gz → 0.20.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 (36) hide show
  1. {python_cq-0.18.0 → python_cq-0.20.0}/PKG-INFO +1 -1
  2. {python_cq-0.18.0 → python_cq-0.20.0}/cq/__init__.py +2 -1
  3. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/dispatchers/abc.py +1 -1
  4. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/dispatchers/bus.py +2 -6
  5. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/dispatchers/pipe.py +1 -1
  6. python_cq-0.20.0/cq/_core/pump.py +56 -0
  7. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/queues/abc.py +7 -6
  8. python_cq-0.20.0/cq/_core/queues/memory.py +60 -0
  9. python_cq-0.20.0/cq/middlewares/exc.py +56 -0
  10. {python_cq-0.18.0 → python_cq-0.20.0}/pyproject.toml +1 -1
  11. python_cq-0.18.0/cq/_core/pump.py +0 -34
  12. python_cq-0.18.0/cq/_core/queues/memory.py +0 -42
  13. {python_cq-0.18.0 → python_cq-0.20.0}/.gitignore +0 -0
  14. {python_cq-0.18.0 → python_cq-0.20.0}/LICENSE +0 -0
  15. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/__init__.py +0 -0
  16. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/common/__init__.py +0 -0
  17. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/common/typing.py +0 -0
  18. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/cq.py +0 -0
  19. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/di.py +0 -0
  20. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/dispatchers/__init__.py +0 -0
  21. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/dispatchers/lazy.py +0 -0
  22. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/handler.py +0 -0
  23. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/message.py +0 -0
  24. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/middleware.py +0 -0
  25. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/middlewares/__init__.py +0 -0
  26. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/middlewares/scope.py +0 -0
  27. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/pipetools.py +0 -0
  28. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/queues/__init__.py +0 -0
  29. {python_cq-0.18.0 → python_cq-0.20.0}/cq/_core/related_events.py +0 -0
  30. {python_cq-0.18.0 → python_cq-0.20.0}/cq/exceptions.py +0 -0
  31. {python_cq-0.18.0 → python_cq-0.20.0}/cq/ext/__init__.py +0 -0
  32. {python_cq-0.18.0 → python_cq-0.20.0}/cq/ext/injection.py +0 -0
  33. {python_cq-0.18.0 → python_cq-0.20.0}/cq/middlewares/__init__.py +0 -0
  34. {python_cq-0.18.0 → python_cq-0.20.0}/cq/middlewares/retry.py +0 -0
  35. {python_cq-0.18.0 → python_cq-0.20.0}/cq/py.typed +0 -0
  36. {python_cq-0.18.0 → python_cq-0.20.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.20.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
@@ -16,7 +16,7 @@ from ._core.message import (
16
16
  from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source
17
17
  from ._core.pipetools import ContextCommandPipeline as _ContextCommandPipeline
18
18
  from ._core.pump import Pump
19
- from ._core.queues.abc import Consumer, Producer, Queue
19
+ from ._core.queues.abc import Consumer, Delivery, Producer, Queue
20
20
  from ._core.queues.memory import MemoryQueue
21
21
  from ._core.related_events import AnyIORelatedEvents, RelatedEvents
22
22
 
@@ -30,6 +30,7 @@ __all__ = (
30
30
  "Consumer",
31
31
  "ContextCommandPipeline",
32
32
  "ContextPipeline",
33
+ "Delivery",
33
34
  "DIAdapter",
34
35
  "Dispatcher",
35
36
  "Event",
@@ -29,7 +29,7 @@ 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 _invoke(
33
33
  self,
34
34
  handler: Callable[[I], Awaitable[O]],
35
35
  message: I,
@@ -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._invoke(handler, message, handler.fail_silently)
91
87
 
92
88
  return NotImplemented
93
89
 
@@ -104,7 +100,7 @@ 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,
103
+ self._invoke,
108
104
  handler,
109
105
  message,
110
106
  handler.fail_silently,
@@ -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._invoke(self.__steps.execute, message)
144
144
 
145
145
 
146
146
  class ContextPipeline[I]:
@@ -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
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 delivery in self.consumer:
28
+ try:
29
+ async with delivery as message:
30
+ await self.__middleware_group.invoke(self.dispatcher, message)
31
+ except Exception:
32
+ if not self.fail_silently:
33
+ raise
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,6 +1,6 @@
1
1
  from abc import abstractmethod
2
- from collections.abc import AsyncIterator
3
- from typing import Protocol, runtime_checkable
2
+ from collections.abc import AsyncIterable
3
+ from typing import AsyncContextManager, Protocol, runtime_checkable
4
4
 
5
5
 
6
6
  @runtime_checkable
@@ -16,12 +16,13 @@ class Producer[T](Protocol):
16
16
 
17
17
 
18
18
  @runtime_checkable
19
- class Consumer[T](Protocol):
19
+ class Delivery[T](AsyncContextManager[T], Protocol):
20
20
  __slots__ = ()
21
21
 
22
- @abstractmethod
23
- def __aiter__(self) -> AsyncIterator[T]:
24
- raise NotImplementedError
22
+
23
+ @runtime_checkable
24
+ class Consumer[T](AsyncIterable[Delivery[T]], Protocol):
25
+ __slots__ = ()
25
26
 
26
27
 
27
28
  @runtime_checkable
@@ -0,0 +1,60 @@
1
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
2
+ from contextlib import asynccontextmanager, nullcontext
3
+ from types import TracebackType
4
+ from typing import Any, Self
5
+
6
+ import anyio
7
+ from anyio.abc import ObjectReceiveStream, ObjectSendStream
8
+
9
+ from cq._core.middleware import Middleware
10
+ from cq._core.pump import Pump
11
+ from cq._core.queues.abc import Delivery, Queue
12
+
13
+
14
+ class MemoryQueue[T](Queue[T]):
15
+ __slots__ = ("__consumer", "__producer")
16
+
17
+ __consumer: ObjectReceiveStream[T]
18
+ __producer: ObjectSendStream[T]
19
+
20
+ def __init__(self, maxsize: int = 0) -> None:
21
+ self.__producer, self.__consumer = anyio.create_memory_object_stream(maxsize)
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
+
34
+ async def __aiter__(self) -> AsyncIterator[Delivery[T]]:
35
+ async for message in self.__consumer:
36
+ yield nullcontext(message)
37
+
38
+ async def close(self) -> None:
39
+ await self.__producer.aclose()
40
+
41
+ @asynccontextmanager
42
+ async def draining(
43
+ self,
44
+ dispatcher: Callable[[T], Awaitable[Any]],
45
+ /,
46
+ *,
47
+ concurrency: int | None = None,
48
+ fail_silently: bool = False,
49
+ middlewares: Sequence[Middleware[[T], Any]] = (),
50
+ ) -> AsyncIterator[Self]:
51
+ async with (
52
+ Pump(self, dispatcher, fail_silently)
53
+ .add_middlewares(*middlewares)
54
+ .draining(concurrency=concurrency, graceful=True)
55
+ ):
56
+ async with self:
57
+ yield self
58
+
59
+ async def send(self, message: T, /) -> None:
60
+ 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.20.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()
@@ -1,42 +0,0 @@
1
- from collections.abc import AsyncIterator, Awaitable, Callable
2
- from contextlib import asynccontextmanager
3
- from typing import Any, Self
4
-
5
- import anyio
6
- from anyio.abc import ObjectReceiveStream, ObjectSendStream
7
-
8
- from cq._core.pump import Pump
9
- from cq._core.queues.abc import Queue
10
-
11
-
12
- class MemoryQueue[T](Queue[T]):
13
- __slots__ = ("__consumer", "__producer")
14
-
15
- __consumer: ObjectReceiveStream[T]
16
- __producer: ObjectSendStream[T]
17
-
18
- def __init__(self, maxsize: int = 0) -> None:
19
- self.__producer, self.__consumer = anyio.create_memory_object_stream(maxsize)
20
-
21
- def __aiter__(self) -> AsyncIterator[T]:
22
- return aiter(self.__consumer)
23
-
24
- async def close(self) -> None:
25
- await self.__producer.aclose()
26
-
27
- @asynccontextmanager
28
- async def draining(
29
- self,
30
- dispatcher: Callable[[T], Awaitable[Any]],
31
- /,
32
- *,
33
- fail_silently: bool = False,
34
- ) -> AsyncIterator[Self]:
35
- async with Pump(self, dispatcher, fail_silently).draining(graceful=True):
36
- try:
37
- yield self
38
- finally:
39
- await self.close()
40
-
41
- async def send(self, message: T, /) -> None:
42
- await self.__producer.send(message)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes