python-cq 0.1.3__tar.gz → 0.2.1__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.1.3 → python_cq-0.2.1}/PKG-INFO +3 -1
- {python_cq-0.1.3 → python_cq-0.2.1}/README.md +1 -0
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/__init__.py +11 -2
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/_core/command.py +2 -1
- python_cq-0.2.1/cq/_core/dispatcher/base.py +50 -0
- {python_cq-0.1.3/cq/_core → python_cq-0.2.1/cq/_core/dispatcher}/bus.py +16 -48
- python_cq-0.2.1/cq/_core/dispatcher/pipe.py +64 -0
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/_core/event.py +1 -1
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/_core/middleware.py +7 -6
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/_core/query.py +1 -1
- python_cq-0.2.1/cq/exceptions.py +7 -0
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/middlewares/retry.py +2 -0
- python_cq-0.2.1/cq/py.typed +0 -0
- {python_cq-0.1.3 → python_cq-0.2.1}/pyproject.toml +1 -2
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/_core/__init__.py +0 -0
- {python_cq-0.1.3/cq/middlewares → python_cq-0.2.1/cq/_core/dispatcher}/__init__.py +0 -0
- {python_cq-0.1.3 → python_cq-0.2.1}/cq/_core/dto.py +0 -0
- /python_cq-0.1.3/cq/py.typed → /python_cq-0.2.1/cq/middlewares/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-cq
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Lightweight CQRS library.
|
|
5
5
|
Home-page: https://github.com/100nm/python-cq
|
|
6
6
|
License: MIT
|
|
@@ -15,6 +15,7 @@ Classifier: Operating System :: OS Independent
|
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
@@ -48,5 +49,6 @@ pip install python-cq
|
|
|
48
49
|
## Resources
|
|
49
50
|
|
|
50
51
|
* [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
|
|
52
|
+
* [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
|
|
51
53
|
* [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
|
|
52
54
|
|
|
@@ -21,4 +21,5 @@ pip install python-cq
|
|
|
21
21
|
## Resources
|
|
22
22
|
|
|
23
23
|
* [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
|
|
24
|
+
* [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
|
|
24
25
|
* [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
from ._core.
|
|
2
|
-
|
|
1
|
+
from ._core.command import (
|
|
2
|
+
AnyCommandBus,
|
|
3
|
+
Command,
|
|
4
|
+
CommandBus,
|
|
5
|
+
command_handler,
|
|
6
|
+
find_command_bus,
|
|
7
|
+
)
|
|
8
|
+
from ._core.dispatcher.bus import Bus
|
|
9
|
+
from ._core.dispatcher.pipe import Pipe
|
|
3
10
|
from ._core.dto import DTO
|
|
4
11
|
from ._core.event import Event, EventBus, event_handler, find_event_bus
|
|
5
12
|
from ._core.middleware import Middleware, MiddlewareResult
|
|
6
13
|
from ._core.query import Query, QueryBus, find_query_bus, query_handler
|
|
7
14
|
|
|
8
15
|
__all__ = (
|
|
16
|
+
"AnyCommandBus",
|
|
9
17
|
"Bus",
|
|
10
18
|
"Command",
|
|
11
19
|
"CommandBus",
|
|
@@ -14,6 +22,7 @@ __all__ = (
|
|
|
14
22
|
"EventBus",
|
|
15
23
|
"Middleware",
|
|
16
24
|
"MiddlewareResult",
|
|
25
|
+
"Pipe",
|
|
17
26
|
"Query",
|
|
18
27
|
"QueryBus",
|
|
19
28
|
"command_handler",
|
|
@@ -3,7 +3,7 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
import injection
|
|
5
5
|
|
|
6
|
-
from cq._core.bus import Bus, SimpleBus, SubscriberDecorator
|
|
6
|
+
from cq._core.dispatcher.bus import Bus, SimpleBus, SubscriberDecorator
|
|
7
7
|
from cq._core.dto import DTO
|
|
8
8
|
|
|
9
9
|
|
|
@@ -12,6 +12,7 @@ class Command(DTO, ABC):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
type CommandBus[T] = Bus[Command, T]
|
|
15
|
+
AnyCommandBus = CommandBus[Any]
|
|
15
16
|
command_handler: SubscriberDecorator[Command, Any] = SubscriberDecorator(CommandBus)
|
|
16
17
|
|
|
17
18
|
injection.set_constant(SimpleBus(), CommandBus, alias=True)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import Awaitable
|
|
4
|
+
from typing import Callable, Protocol, Self, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from cq._core.middleware import Middleware, MiddlewareGroup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class Dispatcher[I, O](Protocol):
|
|
11
|
+
__slots__ = ()
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def dispatch(self, input_value: I, /) -> O:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def dispatch_no_wait(self, *input_values: I) -> None:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
|
|
27
|
+
__slots__ = ("__middleware_group",)
|
|
28
|
+
|
|
29
|
+
__middleware_group: MiddlewareGroup[[I], O]
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.__middleware_group = MiddlewareGroup()
|
|
33
|
+
|
|
34
|
+
def dispatch_no_wait(self, *input_values: I) -> None:
|
|
35
|
+
asyncio.gather(
|
|
36
|
+
*(self.dispatch(input_value) for input_value in input_values),
|
|
37
|
+
return_exceptions=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
|
|
41
|
+
self.__middleware_group.add(*middlewares)
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
async def _invoke_with_middlewares(
|
|
45
|
+
self,
|
|
46
|
+
handler: Callable[[I], Awaitable[O]],
|
|
47
|
+
input_value: I,
|
|
48
|
+
/,
|
|
49
|
+
) -> O:
|
|
50
|
+
return await self.__middleware_group.invoke(handler, input_value)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from abc import
|
|
2
|
+
from abc import abstractmethod
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from dataclasses import dataclass, field
|
|
@@ -9,7 +9,7 @@ from typing import Protocol, Self, TypeAliasType, runtime_checkable
|
|
|
9
9
|
|
|
10
10
|
import injection
|
|
11
11
|
|
|
12
|
-
from cq._core.
|
|
12
|
+
from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
|
|
13
13
|
|
|
14
14
|
type HandlerType[**P, T] = type[Handler[P, T]]
|
|
15
15
|
type HandlerFactory[**P, T] = Callable[..., Handler[P, T]]
|
|
@@ -27,43 +27,21 @@ class Handler[**P, T](Protocol):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@runtime_checkable
|
|
30
|
-
class Bus[I, O](Protocol):
|
|
30
|
+
class Bus[I, O](Dispatcher[I, O], Protocol):
|
|
31
31
|
__slots__ = ()
|
|
32
32
|
|
|
33
|
-
@abstractmethod
|
|
34
|
-
async def dispatch(self, input_value: I, /) -> O:
|
|
35
|
-
raise NotImplementedError
|
|
36
|
-
|
|
37
|
-
def dispatch_no_wait(self, first_input_value: I, /, *input_values: I) -> None:
|
|
38
|
-
asyncio.gather(
|
|
39
|
-
*(
|
|
40
|
-
self.dispatch(input_value)
|
|
41
|
-
for input_value in (first_input_value, *input_values)
|
|
42
|
-
),
|
|
43
|
-
return_exceptions=True,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
33
|
@abstractmethod
|
|
47
34
|
def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
|
|
48
35
|
raise NotImplementedError
|
|
49
36
|
|
|
50
|
-
@abstractmethod
|
|
51
|
-
def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
|
|
52
|
-
raise NotImplementedError
|
|
53
|
-
|
|
54
37
|
|
|
55
38
|
@dataclass(eq=False, frozen=True, slots=True)
|
|
56
39
|
class SubscriberDecorator[I, O]:
|
|
57
40
|
bus_type: BusType[I, O] | TypeAliasType | GenericAlias
|
|
58
41
|
injection_module: injection.Module = field(default_factory=injection.mod)
|
|
59
42
|
|
|
60
|
-
def __call__[
|
|
61
|
-
|
|
62
|
-
first_input_type: type[I],
|
|
63
|
-
/,
|
|
64
|
-
*input_types: type[I],
|
|
65
|
-
) -> Callable[[T], T]:
|
|
66
|
-
def decorator(wrapped: T) -> T:
|
|
43
|
+
def __call__(self, first_input_type: type[I], /, *input_types: type[I]): # type: ignore[no-untyped-def]
|
|
44
|
+
def decorator(wrapped): # type: ignore[no-untyped-def]
|
|
67
45
|
if not isclass(wrapped) or not issubclass(wrapped, Handler):
|
|
68
46
|
raise TypeError(f"`{wrapped}` isn't a valid handler.")
|
|
69
47
|
|
|
@@ -73,7 +51,7 @@ class SubscriberDecorator[I, O]:
|
|
|
73
51
|
for input_type in (first_input_type, *input_types):
|
|
74
52
|
bus.subscribe(input_type, factory)
|
|
75
53
|
|
|
76
|
-
return wrapped
|
|
54
|
+
return wrapped
|
|
77
55
|
|
|
78
56
|
return decorator
|
|
79
57
|
|
|
@@ -81,23 +59,7 @@ class SubscriberDecorator[I, O]:
|
|
|
81
59
|
return self.injection_module.find_instance(self.bus_type)
|
|
82
60
|
|
|
83
61
|
|
|
84
|
-
class
|
|
85
|
-
__slots__ = ("__middleware_group",)
|
|
86
|
-
|
|
87
|
-
__middleware_group: MiddlewareGroup[[I], O]
|
|
88
|
-
|
|
89
|
-
def __init__(self) -> None:
|
|
90
|
-
self.__middleware_group = MiddlewareGroup()
|
|
91
|
-
|
|
92
|
-
def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
|
|
93
|
-
self.__middleware_group.add(*middlewares)
|
|
94
|
-
return self
|
|
95
|
-
|
|
96
|
-
async def _invoke(self, handler: Handler[[I], O], input_value: I, /) -> O:
|
|
97
|
-
return await self.__middleware_group.invoke(handler.handle, input_value)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class SimpleBus[I, O](_BaseBus[I, O]):
|
|
62
|
+
class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
|
|
101
63
|
__slots__ = ("__handlers",)
|
|
102
64
|
|
|
103
65
|
__handlers: dict[type[I], HandlerFactory[[I], O]]
|
|
@@ -114,7 +76,10 @@ class SimpleBus[I, O](_BaseBus[I, O]):
|
|
|
114
76
|
except KeyError:
|
|
115
77
|
return NotImplemented
|
|
116
78
|
|
|
117
|
-
return await self.
|
|
79
|
+
return await self._invoke_with_middlewares(
|
|
80
|
+
handler_factory().handle,
|
|
81
|
+
input_value,
|
|
82
|
+
)
|
|
118
83
|
|
|
119
84
|
def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
|
|
120
85
|
if input_type in self.__handlers:
|
|
@@ -126,7 +91,7 @@ class SimpleBus[I, O](_BaseBus[I, O]):
|
|
|
126
91
|
return self
|
|
127
92
|
|
|
128
93
|
|
|
129
|
-
class TaskBus[I](
|
|
94
|
+
class TaskBus[I](BaseDispatcher[I, None], Bus[I, None]):
|
|
130
95
|
__slots__ = ("__handlers",)
|
|
131
96
|
|
|
132
97
|
__handlers: dict[type[I], list[HandlerFactory[[I], None]]]
|
|
@@ -143,7 +108,10 @@ class TaskBus[I](_BaseBus[I, None]):
|
|
|
143
108
|
|
|
144
109
|
await asyncio.gather(
|
|
145
110
|
*(
|
|
146
|
-
self.
|
|
111
|
+
self._invoke_with_middlewares(
|
|
112
|
+
handler_factory().handle,
|
|
113
|
+
input_value,
|
|
114
|
+
)
|
|
147
115
|
for handler_factory in handler_factories
|
|
148
116
|
)
|
|
149
117
|
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Awaitable, Self
|
|
4
|
+
|
|
5
|
+
from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
|
|
6
|
+
|
|
7
|
+
type PipeConverter[I, O] = Callable[[O], Awaitable[I]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
11
|
+
class PipeStep[I, O]:
|
|
12
|
+
converter: PipeConverter[I, O]
|
|
13
|
+
dispatcher: Dispatcher[I, Any] | None = field(default=None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Pipe[I, O](BaseDispatcher[I, O]):
|
|
17
|
+
__slots__ = ("__dispatcher", "__steps")
|
|
18
|
+
|
|
19
|
+
__dispatcher: Dispatcher[Any, Any]
|
|
20
|
+
__steps: list[PipeStep[Any, Any]]
|
|
21
|
+
|
|
22
|
+
def __init__(self, dispatcher: Dispatcher[Any, Any]) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.__dispatcher = dispatcher
|
|
25
|
+
self.__steps = []
|
|
26
|
+
|
|
27
|
+
def step[T]( # type: ignore[no-untyped-def]
|
|
28
|
+
self,
|
|
29
|
+
wrapped: PipeConverter[T, Any] | None = None,
|
|
30
|
+
/,
|
|
31
|
+
*,
|
|
32
|
+
dispatcher: Dispatcher[T, Any] | None = None,
|
|
33
|
+
):
|
|
34
|
+
def decorator(wp): # type: ignore[no-untyped-def]
|
|
35
|
+
step = PipeStep(wp, dispatcher)
|
|
36
|
+
self.__steps.append(step)
|
|
37
|
+
return wp
|
|
38
|
+
|
|
39
|
+
return decorator(wrapped) if wrapped else decorator
|
|
40
|
+
|
|
41
|
+
def add_static_step[T](
|
|
42
|
+
self,
|
|
43
|
+
input_value: T,
|
|
44
|
+
*,
|
|
45
|
+
dispatcher: Dispatcher[T, Any] | None = None,
|
|
46
|
+
) -> Self:
|
|
47
|
+
@self.step(dispatcher=dispatcher)
|
|
48
|
+
async def converter(_: Any) -> T:
|
|
49
|
+
return input_value
|
|
50
|
+
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
async def dispatch(self, input_value: I, /) -> O:
|
|
54
|
+
return await self._invoke_with_middlewares(self.__execute, input_value)
|
|
55
|
+
|
|
56
|
+
async def __execute(self, input_value: I) -> O:
|
|
57
|
+
dispatcher = self.__dispatcher
|
|
58
|
+
|
|
59
|
+
for step in self.__steps:
|
|
60
|
+
output_value = await dispatcher.dispatch(input_value)
|
|
61
|
+
input_value = await step.converter(output_value)
|
|
62
|
+
dispatcher = step.dispatcher or self.__dispatcher
|
|
63
|
+
|
|
64
|
+
return await dispatcher.dispatch(input_value)
|
|
@@ -2,17 +2,15 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator
|
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from typing import Self
|
|
4
4
|
|
|
5
|
+
from cq.exceptions import MiddlewareError
|
|
6
|
+
|
|
5
7
|
type MiddlewareResult[T] = AsyncGenerator[None, T]
|
|
6
8
|
type Middleware[**P, T] = Callable[P, MiddlewareResult[T]]
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
@dataclass(eq=False, frozen=True, slots=True)
|
|
11
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
10
12
|
class MiddlewareGroup[**P, T]:
|
|
11
|
-
__middlewares: list[Middleware[P, T]] = field(
|
|
12
|
-
default_factory=list,
|
|
13
|
-
init=False,
|
|
14
|
-
repr=False,
|
|
15
|
-
)
|
|
13
|
+
__middlewares: list[Middleware[P, T]] = field(default_factory=list, init=False)
|
|
16
14
|
|
|
17
15
|
@property
|
|
18
16
|
def __stack(self) -> Iterator[Middleware[P, T]]:
|
|
@@ -51,6 +49,9 @@ class MiddlewareGroup[**P, T]:
|
|
|
51
49
|
await generator.athrow(exc)
|
|
52
50
|
else:
|
|
53
51
|
await generator.asend(value)
|
|
52
|
+
raise MiddlewareError(
|
|
53
|
+
f"Too many `yield` keywords in `{middleware}`."
|
|
54
|
+
)
|
|
54
55
|
|
|
55
56
|
except StopAsyncIteration:
|
|
56
57
|
...
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-cq"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Lightweight CQRS library."
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = ["remimd"]
|
|
@@ -51,7 +51,6 @@ check_untyped_defs = true
|
|
|
51
51
|
disallow_any_generics = true
|
|
52
52
|
disallow_subclassing_any = true
|
|
53
53
|
disallow_untyped_defs = true
|
|
54
|
-
enable_incomplete_feature = ["NewGenericSyntax"]
|
|
55
54
|
follow_imports = "silent"
|
|
56
55
|
no_implicit_reexport = true
|
|
57
56
|
plugins = ["pydantic.mypy"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|