python-cq 0.1.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/__init__.py +23 -0
- cq/_core/__init__.py +0 -0
- cq/_core/bus.py +156 -0
- cq/_core/command.py +21 -0
- cq/_core/dto.py +10 -0
- cq/_core/event.py +20 -0
- cq/_core/middleware.py +74 -0
- cq/_core/query.py +21 -0
- python_cq-0.1.0.dist-info/METADATA +52 -0
- python_cq-0.1.0.dist-info/RECORD +11 -0
- python_cq-0.1.0.dist-info/WHEEL +4 -0
cq/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ._core.command import Command, CommandBus, command_handler, find_command_bus
|
|
2
|
+
from ._core.dto import DTO
|
|
3
|
+
from ._core.event import Event, EventBus, event_handler, find_event_bus
|
|
4
|
+
from ._core.middleware import Middleware, MiddlewareResult
|
|
5
|
+
from ._core.query import Query, QueryBus, find_query_bus, query_handler
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"Command",
|
|
9
|
+
"CommandBus",
|
|
10
|
+
"DTO",
|
|
11
|
+
"Event",
|
|
12
|
+
"EventBus",
|
|
13
|
+
"Middleware",
|
|
14
|
+
"MiddlewareResult",
|
|
15
|
+
"Query",
|
|
16
|
+
"QueryBus",
|
|
17
|
+
"command_handler",
|
|
18
|
+
"event_handler",
|
|
19
|
+
"find_command_bus",
|
|
20
|
+
"find_event_bus",
|
|
21
|
+
"find_query_bus",
|
|
22
|
+
"query_handler",
|
|
23
|
+
)
|
cq/_core/__init__.py
ADDED
|
File without changes
|
cq/_core/bus.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from inspect import isclass
|
|
7
|
+
from types import GenericAlias
|
|
8
|
+
from typing import Protocol, Self, TypeAliasType, runtime_checkable
|
|
9
|
+
|
|
10
|
+
import injection
|
|
11
|
+
|
|
12
|
+
from cq._core.middleware import Middleware, MiddlewareGroup
|
|
13
|
+
|
|
14
|
+
type HandlerType[**P, T] = type[Handler[P, T]]
|
|
15
|
+
type HandlerFactory[**P, T] = Callable[..., Handler[P, T]]
|
|
16
|
+
|
|
17
|
+
type BusType[I, O] = type[Bus[I, O]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class Handler[**P, T](Protocol):
|
|
22
|
+
__slots__ = ()
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def handle(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class Bus[I, O](Protocol):
|
|
31
|
+
__slots__ = ()
|
|
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
|
+
)
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(eq=False, frozen=True, slots=True)
|
|
55
|
+
class SubscriberDecorator[I, O]:
|
|
56
|
+
bus_type: BusType[I, O] | TypeAliasType | GenericAlias
|
|
57
|
+
injection_module: injection.Module = field(default_factory=injection.mod)
|
|
58
|
+
|
|
59
|
+
def __call__[T](
|
|
60
|
+
self,
|
|
61
|
+
first_input_type: type[I],
|
|
62
|
+
/,
|
|
63
|
+
*input_types: type[I],
|
|
64
|
+
) -> Callable[[T], T]:
|
|
65
|
+
def decorator(wrapped: T) -> T:
|
|
66
|
+
if not isclass(wrapped) or not issubclass(wrapped, Handler):
|
|
67
|
+
raise TypeError(f"`{wrapped}` isn't a valid handler.")
|
|
68
|
+
|
|
69
|
+
bus = self.__find_bus()
|
|
70
|
+
factory = self.injection_module.make_injected_function(wrapped)
|
|
71
|
+
|
|
72
|
+
for input_type in (first_input_type, *input_types):
|
|
73
|
+
bus.subscribe(input_type, factory)
|
|
74
|
+
|
|
75
|
+
return wrapped # type: ignore[return-value]
|
|
76
|
+
|
|
77
|
+
return decorator
|
|
78
|
+
|
|
79
|
+
def __find_bus(self) -> Bus[I, O]:
|
|
80
|
+
return self.injection_module.find_instance(self.bus_type)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _BaseBus[I, O](Bus[I, O], ABC):
|
|
84
|
+
__slots__ = ("__middleware_group",)
|
|
85
|
+
|
|
86
|
+
__middleware_group: MiddlewareGroup[[I], O]
|
|
87
|
+
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self.__middleware_group = MiddlewareGroup()
|
|
90
|
+
|
|
91
|
+
def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
|
|
92
|
+
self.__middleware_group.add(*middlewares)
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
async def _invoke(self, handler: Handler[[I], O], input_value: I, /) -> O:
|
|
96
|
+
return await self.__middleware_group.invoke(handler.handle, input_value)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SimpleBus[I, O](_BaseBus[I, O]):
|
|
100
|
+
__slots__ = ("__handlers",)
|
|
101
|
+
|
|
102
|
+
__handlers: dict[type[I], HandlerFactory[[I], O]]
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
super().__init__()
|
|
106
|
+
self.__handlers = {}
|
|
107
|
+
|
|
108
|
+
async def dispatch(self, input_value: I, /) -> O:
|
|
109
|
+
input_type = type(input_value)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
handler_factory = self.__handlers[input_type]
|
|
113
|
+
except KeyError:
|
|
114
|
+
return NotImplemented
|
|
115
|
+
|
|
116
|
+
return await self._invoke(handler_factory(), input_value)
|
|
117
|
+
|
|
118
|
+
def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
|
|
119
|
+
if input_type in self.__handlers:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"A handler is already registered for the input type: `{input_type}`."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self.__handlers[input_type] = factory
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TaskBus[I](_BaseBus[I, None]):
|
|
129
|
+
__slots__ = ("__handlers",)
|
|
130
|
+
|
|
131
|
+
__handlers: dict[type[I], list[HandlerFactory[[I], None]]]
|
|
132
|
+
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
super().__init__()
|
|
135
|
+
self.__handlers = defaultdict(list)
|
|
136
|
+
|
|
137
|
+
async def dispatch(self, input_value: I, /) -> None:
|
|
138
|
+
handler_factories = self.__handlers.get(type(input_value))
|
|
139
|
+
|
|
140
|
+
if not handler_factories:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
await asyncio.gather(
|
|
144
|
+
*(
|
|
145
|
+
self._invoke(handler_factory(), input_value)
|
|
146
|
+
for handler_factory in handler_factories
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def subscribe(
|
|
151
|
+
self,
|
|
152
|
+
input_type: type[I],
|
|
153
|
+
factory: HandlerFactory[[I], None],
|
|
154
|
+
) -> Self:
|
|
155
|
+
self.__handlers[input_type].append(factory)
|
|
156
|
+
return self
|
cq/_core/command.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import injection
|
|
5
|
+
|
|
6
|
+
from cq._core.bus import Bus, SimpleBus, SubscriberDecorator
|
|
7
|
+
from cq._core.dto import DTO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Command(DTO, ABC):
|
|
11
|
+
__slots__ = ()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type CommandBus[T] = Bus[Command, T]
|
|
15
|
+
command_handler: SubscriberDecorator[Command, Any] = SubscriberDecorator(CommandBus)
|
|
16
|
+
|
|
17
|
+
injection.set_constant(SimpleBus(), CommandBus, alias=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_command_bus[T]() -> CommandBus[T]:
|
|
21
|
+
return injection.find_instance(CommandBus)
|
cq/_core/dto.py
ADDED
cq/_core/event.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
|
|
3
|
+
import injection
|
|
4
|
+
|
|
5
|
+
from cq._core.bus import Bus, SubscriberDecorator, TaskBus
|
|
6
|
+
from cq._core.dto import DTO
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Event(DTO, ABC):
|
|
10
|
+
__slots__ = ()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
type EventBus = Bus[Event, None]
|
|
14
|
+
event_handler: SubscriberDecorator[Event, None] = SubscriberDecorator(EventBus)
|
|
15
|
+
|
|
16
|
+
injection.set_constant(TaskBus(), EventBus, alias=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_event_bus() -> EventBus:
|
|
20
|
+
return injection.find_instance(EventBus)
|
cq/_core/middleware.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
type MiddlewareResult[T] = AsyncGenerator[None, T]
|
|
6
|
+
type Middleware[**P, T] = Callable[P, MiddlewareResult[T]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(eq=False, frozen=True, slots=True)
|
|
10
|
+
class MiddlewareGroup[**P, T]:
|
|
11
|
+
__middlewares: list[Middleware[P, T]] = field(
|
|
12
|
+
default_factory=list,
|
|
13
|
+
init=False,
|
|
14
|
+
repr=False,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def __stack(self) -> Iterator[Middleware[P, T]]:
|
|
19
|
+
return iter(self.__middlewares)
|
|
20
|
+
|
|
21
|
+
def add(self, *middlewares: Middleware[P, T]) -> Self:
|
|
22
|
+
self.__middlewares.extend(middlewares)
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
async def invoke(
|
|
26
|
+
self,
|
|
27
|
+
handler: Callable[P, Awaitable[T]],
|
|
28
|
+
/,
|
|
29
|
+
*args: P.args,
|
|
30
|
+
**kwargs: P.kwargs,
|
|
31
|
+
) -> T:
|
|
32
|
+
return await self.__apply_stack(handler, self.__stack)(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def __apply_middleware(
|
|
36
|
+
cls,
|
|
37
|
+
handler: Callable[P, Awaitable[T]],
|
|
38
|
+
middleware: Middleware[P, T],
|
|
39
|
+
) -> Callable[P, Awaitable[T]]:
|
|
40
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
41
|
+
generator: MiddlewareResult[T] = middleware(*args, **kwargs)
|
|
42
|
+
value: T = NotImplemented
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
await anext(generator)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
value = await handler(*args, **kwargs)
|
|
49
|
+
except BaseException as exc:
|
|
50
|
+
await generator.athrow(exc)
|
|
51
|
+
else:
|
|
52
|
+
await generator.asend(value)
|
|
53
|
+
|
|
54
|
+
except StopAsyncIteration:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
finally:
|
|
58
|
+
await generator.aclose()
|
|
59
|
+
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
return wrapper
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def __apply_stack(
|
|
66
|
+
cls,
|
|
67
|
+
handler: Callable[P, Awaitable[T]],
|
|
68
|
+
stack: Iterator[Middleware[P, T]],
|
|
69
|
+
) -> Callable[P, Awaitable[T]]:
|
|
70
|
+
for middleware in stack:
|
|
71
|
+
new_handler = cls.__apply_middleware(handler, middleware)
|
|
72
|
+
return cls.__apply_stack(new_handler, stack)
|
|
73
|
+
|
|
74
|
+
return handler
|
cq/_core/query.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import injection
|
|
5
|
+
|
|
6
|
+
from cq._core.bus import Bus, SimpleBus, SubscriberDecorator
|
|
7
|
+
from cq._core.dto import DTO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Query(DTO, ABC):
|
|
11
|
+
__slots__ = ()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type QueryBus[T] = Bus[Query, T]
|
|
15
|
+
query_handler: SubscriberDecorator[Query, Any] = SubscriberDecorator(QueryBus)
|
|
16
|
+
|
|
17
|
+
injection.set_constant(SimpleBus(), QueryBus, alias=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_query_bus[T]() -> QueryBus[T]:
|
|
21
|
+
return injection.find_instance(QueryBus)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-cq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight CQRS library.
|
|
5
|
+
Home-page: https://github.com/100nm/python-cq
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: cqrs
|
|
8
|
+
Author: remimd
|
|
9
|
+
Requires-Python: >=3.12,<4
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
24
|
+
Requires-Dist: python-injection
|
|
25
|
+
Project-URL: Repository, https://github.com/100nm/python-cq
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# python-cq
|
|
29
|
+
|
|
30
|
+
[](https://github.com/100nm/python-cq)
|
|
31
|
+
[](https://pypi.org/project/python-cq/)
|
|
32
|
+
[](https://github.com/astral-sh/ruff)
|
|
33
|
+
|
|
34
|
+
Lightweight library for separating Python code according to **Command and Query Responsibility Segregation** principles.
|
|
35
|
+
|
|
36
|
+
Dependency injection is handled by [python-injection](https://github.com/100nm/python-injection).
|
|
37
|
+
|
|
38
|
+
Easy to use with [FastAPI](https://github.com/fastapi/fastapi).
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
⚠️ _Requires Python 3.12 or higher_
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install python-cq
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Resources
|
|
49
|
+
|
|
50
|
+
* [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
|
|
51
|
+
* [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
|
|
52
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
cq/__init__.py,sha256=FFTaq-Kq_ywMqWwaucK4MSYPSVbyHizEEYrRwFj3iIU,604
|
|
2
|
+
cq/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
cq/_core/bus.py,sha256=1P0jHjK1q33GvvB6lo2-IEhK2RP0EZIIy4GyI9YcLVA,4537
|
|
4
|
+
cq/_core/command.py,sha256=dqN99iG4eAD5cXhi0wzWnuWVE9kPhoNE05S-QDGcqHc,476
|
|
5
|
+
cq/_core/dto.py,sha256=jrlTRvG9fXFyv0nb8PQY2WTVPldgGKxg-r2xL6FOT0c,206
|
|
6
|
+
cq/_core/event.py,sha256=9LocJ3QM1MZ4mqx8V1YJT-2lsNulUKix6Epab_u2VmM,424
|
|
7
|
+
cq/_core/middleware.py,sha256=aCTmTcDkTxhmCS2NLenVpMo5QLJmKrEyn6SI6yAuFk8,2117
|
|
8
|
+
cq/_core/query.py,sha256=rpYLviIFoy5uWVPCVsvGdfzHeyc21HSmt1e1KBFEz_A,456
|
|
9
|
+
python_cq-0.1.0.dist-info/METADATA,sha256=Lr4fXKlyW_ULiTq03uYxbDA0rtt59N0oh1pUWkYmu38,2039
|
|
10
|
+
python_cq-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
11
|
+
python_cq-0.1.0.dist-info/RECORD,,
|