nlbone 0.6.19__py3-none-any.whl → 0.6.20__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.
- nlbone/adapters/db/postgres/query_builder.py +1 -1
- nlbone/adapters/repositories/outbox_repo.py +6 -8
- nlbone/config/settings.py +5 -0
- nlbone/core/application/bus.py +169 -0
- nlbone/core/application/di.py +99 -0
- nlbone/core/application/registry.py +123 -0
- nlbone/core/ports/repository.py +115 -0
- nlbone/interfaces/cli/crypto.py +22 -0
- nlbone/interfaces/cli/main.py +2 -0
- nlbone/interfaces/jobs/dispatch_outbox.py +1 -0
- nlbone/utils/crypto.py +29 -0
- nlbone/utils/normalize_mobile.py +33 -0
- {nlbone-0.6.19.dist-info → nlbone-0.6.20.dist-info}/METADATA +2 -1
- {nlbone-0.6.19.dist-info → nlbone-0.6.20.dist-info}/RECORD +17 -11
- nlbone/core/application/command_bus.py +0 -25
- {nlbone-0.6.19.dist-info → nlbone-0.6.20.dist-info}/WHEEL +0 -0
- {nlbone-0.6.19.dist-info → nlbone-0.6.20.dist-info}/entry_points.txt +0 -0
- {nlbone-0.6.19.dist-info → nlbone-0.6.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -340,7 +340,7 @@ def get_paginated_response(
|
|
|
340
340
|
with_count: bool = True,
|
|
341
341
|
output_cls: Optional[Type] = None,
|
|
342
342
|
eager_options: Optional[Sequence[LoaderOption]] = None,
|
|
343
|
-
query
|
|
343
|
+
query=None,
|
|
344
344
|
) -> dict:
|
|
345
345
|
if not query:
|
|
346
346
|
query = session.query(entity)
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class OutboxRecord(dict):
|
|
4
5
|
pass
|
|
5
6
|
|
|
7
|
+
|
|
6
8
|
class OutboxRepository:
|
|
7
9
|
def __init__(self, engine):
|
|
8
10
|
self._engine = engine
|
|
9
11
|
|
|
10
|
-
def add(self, msg) -> None:
|
|
11
|
-
...
|
|
12
|
+
def add(self, msg) -> None: ...
|
|
12
13
|
|
|
13
|
-
def fetch_pending(self, limit: int = 100) -> List[OutboxRecord]:
|
|
14
|
-
...
|
|
14
|
+
def fetch_pending(self, limit: int = 100) -> List[OutboxRecord]: ...
|
|
15
15
|
|
|
16
|
-
def mark_sent(self, msg_id: int) -> None:
|
|
17
|
-
...
|
|
16
|
+
def mark_sent(self, msg_id: int) -> None: ...
|
|
18
17
|
|
|
19
|
-
def schedule_retry(self, msg_id: int, retries: int, backoff_base: int = 2) -> None:
|
|
20
|
-
...
|
|
18
|
+
def schedule_retry(self, msg_id: int, retries: int, backoff_base: int = 2) -> None: ...
|
nlbone/config/settings.py
CHANGED
|
@@ -96,6 +96,11 @@ class Settings(BaseSettings):
|
|
|
96
96
|
# ---------------------------
|
|
97
97
|
PRICING_SERVICE_URL: AnyHttpUrl = Field(default="https://pricing.numberland.ir/v1")
|
|
98
98
|
|
|
99
|
+
# ---------------------------
|
|
100
|
+
# Crypto
|
|
101
|
+
# ---------------------------
|
|
102
|
+
FERNET_KEY: str = Field(default="")
|
|
103
|
+
|
|
99
104
|
model_config = SettingsConfigDict(
|
|
100
105
|
env_prefix="",
|
|
101
106
|
env_file=None,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Optional, List, Callable, Coroutine, Any, Protocol
|
|
3
|
+
|
|
4
|
+
from nlbone.core.application.registry import HandlerRegistry, AsyncHandlerRegistry
|
|
5
|
+
from nlbone.core.domain.base import Command, DomainEvent
|
|
6
|
+
from nlbone.core.ports import UnitOfWork, AsyncUnitOfWork
|
|
7
|
+
from nlbone.core.ports.event_bus import OutboxPublisher, Message, AsyncOutboxPublisher
|
|
8
|
+
from nlbone.interfaces.api.middleware.access_log import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SyncMiddleware(Protocol):
|
|
12
|
+
def __call__(self, message: Message, next_: Callable[[Message], None]) -> None: # noqa: D401
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncMiddleware(Protocol):
|
|
17
|
+
async def __call__(self, message: Message, next_: Callable[[Message], Coroutine[Any, Any, None]]) -> None: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncMessageBus:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
uow: UnitOfWork,
|
|
24
|
+
registry: HandlerRegistry,
|
|
25
|
+
middlewares: Optional[List[SyncMiddleware]] = None,
|
|
26
|
+
outbox: Optional[OutboxPublisher] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.uow = uow
|
|
29
|
+
self.registry = registry
|
|
30
|
+
self.middlewares = middlewares or []
|
|
31
|
+
self.outbox = outbox
|
|
32
|
+
self._queue: List[Message] = []
|
|
33
|
+
|
|
34
|
+
def handle(self, message: Message) -> None:
|
|
35
|
+
self._queue.append(message)
|
|
36
|
+
while self._queue:
|
|
37
|
+
msg = self._queue.pop(0)
|
|
38
|
+
self._dispatch_with_pipeline(msg)
|
|
39
|
+
|
|
40
|
+
# pipeline that wraps dispatch
|
|
41
|
+
def _dispatch_with_pipeline(self, message: Message) -> None:
|
|
42
|
+
def terminal(m: Message) -> None:
|
|
43
|
+
self._dispatch(m)
|
|
44
|
+
|
|
45
|
+
# build chain right-to-left
|
|
46
|
+
next_callable = terminal
|
|
47
|
+
for mw in reversed(self.middlewares):
|
|
48
|
+
current_mw = mw
|
|
49
|
+
|
|
50
|
+
def make_next(nxt: Callable[[Message], None]): # closure helper
|
|
51
|
+
def _mw_call(m: Message) -> None:
|
|
52
|
+
return current_mw(m, nxt)
|
|
53
|
+
|
|
54
|
+
return _mw_call
|
|
55
|
+
|
|
56
|
+
next_callable = make_next(next_callable)
|
|
57
|
+
next_callable(message)
|
|
58
|
+
|
|
59
|
+
def _dispatch(self, message: Message) -> None:
|
|
60
|
+
if isinstance(message, DomainEvent):
|
|
61
|
+
self._handle_event(message)
|
|
62
|
+
elif isinstance(message, Command):
|
|
63
|
+
self._handle_command(message)
|
|
64
|
+
else:
|
|
65
|
+
raise TypeError(f"Unknown message type: {type(message)!r}")
|
|
66
|
+
|
|
67
|
+
def _handle_event(self, event: DomainEvent) -> None:
|
|
68
|
+
handlers = self.registry.for_event(type(event))
|
|
69
|
+
for handler in handlers:
|
|
70
|
+
try:
|
|
71
|
+
logger.debug("handling event %s with %s", event, handler)
|
|
72
|
+
produced = handler(event)
|
|
73
|
+
if produced:
|
|
74
|
+
self._queue.extend(produced)
|
|
75
|
+
self._queue.extend(self.uow.collect_new_events())
|
|
76
|
+
except Exception: # noqa: BLE001
|
|
77
|
+
logger.exception("Exception handling event %s\n%s", event, traceback.format_exc())
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
def _handle_command(self, command: Command) -> None:
|
|
81
|
+
handler = self.registry.for_command(type(command))
|
|
82
|
+
try:
|
|
83
|
+
logger.debug("handling command %s with %s", command, handler)
|
|
84
|
+
produced = handler(command)
|
|
85
|
+
if produced:
|
|
86
|
+
self._queue.extend(produced)
|
|
87
|
+
# commit (and gather/emit domain events)
|
|
88
|
+
self._queue.extend(self.uow.collect_new_events())
|
|
89
|
+
if self.outbox:
|
|
90
|
+
self.outbox.publish(self._queue) # best-effort; in real systems use a DB-backed outbox
|
|
91
|
+
except Exception: # noqa: BLE001
|
|
92
|
+
logger.exception("Exception handling command %s\n%s", command, traceback.format_exc())
|
|
93
|
+
raise
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ==========================
|
|
97
|
+
# MessageBus (async)
|
|
98
|
+
# ==========================
|
|
99
|
+
class AsyncMessageBus:
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
uow: AsyncUnitOfWork,
|
|
103
|
+
registry: AsyncHandlerRegistry,
|
|
104
|
+
middlewares: Optional[List[AsyncMiddleware]] = None,
|
|
105
|
+
outbox: Optional[AsyncOutboxPublisher] = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
self.uow = uow
|
|
108
|
+
self.registry = registry
|
|
109
|
+
self.middlewares = middlewares or []
|
|
110
|
+
self.outbox = outbox
|
|
111
|
+
self._queue: List[Message] = []
|
|
112
|
+
|
|
113
|
+
async def handle(self, message: Message) -> None:
|
|
114
|
+
self._queue.append(message)
|
|
115
|
+
while self._queue:
|
|
116
|
+
msg = self._queue.pop(0)
|
|
117
|
+
await self._dispatch_with_pipeline(msg)
|
|
118
|
+
|
|
119
|
+
async def _dispatch_with_pipeline(self, message: Message) -> None:
|
|
120
|
+
async def terminal(m: Message) -> None:
|
|
121
|
+
await self._dispatch(m)
|
|
122
|
+
|
|
123
|
+
next_callable = terminal
|
|
124
|
+
for mw in reversed(self.middlewares):
|
|
125
|
+
current_mw = mw
|
|
126
|
+
|
|
127
|
+
def make_next(nxt: Callable[[Message], Coroutine[Any, Any, None]]):
|
|
128
|
+
async def _mw_call(m: Message) -> None:
|
|
129
|
+
return await current_mw(m, nxt)
|
|
130
|
+
|
|
131
|
+
return _mw_call
|
|
132
|
+
|
|
133
|
+
next_callable = make_next(next_callable)
|
|
134
|
+
await next_callable(message)
|
|
135
|
+
|
|
136
|
+
async def _dispatch(self, message: Message) -> None:
|
|
137
|
+
if isinstance(message, DomainEvent):
|
|
138
|
+
await self._handle_event(message)
|
|
139
|
+
elif isinstance(message, Command):
|
|
140
|
+
await self._handle_command(message)
|
|
141
|
+
else:
|
|
142
|
+
raise TypeError(f"Unknown message type: {type(message)!r}")
|
|
143
|
+
|
|
144
|
+
async def _handle_event(self, event: DomainEvent) -> None:
|
|
145
|
+
handlers = self.registry.for_event(type(event))
|
|
146
|
+
for handler in handlers:
|
|
147
|
+
try:
|
|
148
|
+
logger.debug("handling event %s with %s", event, handler)
|
|
149
|
+
produced = await handler(event)
|
|
150
|
+
if produced:
|
|
151
|
+
self._queue.extend(produced)
|
|
152
|
+
self._queue.extend(self.uow.collect_new_events())
|
|
153
|
+
except Exception: # noqa: BLE001
|
|
154
|
+
logger.exception("Exception handling event %s\n%s", event, traceback.format_exc())
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
async def _handle_command(self, command: Command) -> None:
|
|
158
|
+
handler = self.registry.for_command(type(command))
|
|
159
|
+
try:
|
|
160
|
+
logger.debug("handling command %s with %s", command, handler)
|
|
161
|
+
produced = await handler(command)
|
|
162
|
+
if produced:
|
|
163
|
+
self._queue.extend(produced)
|
|
164
|
+
self._queue.extend(self.uow.collect_new_events())
|
|
165
|
+
if self.outbox:
|
|
166
|
+
await self.outbox.publish(self._queue)
|
|
167
|
+
except Exception: # noqa: BLE001
|
|
168
|
+
logger.exception("Exception handling command %s\n%s", command, traceback.format_exc())
|
|
169
|
+
raise
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, Callable, Dict, Optional, Type, get_type_hints, get_origin, get_args
|
|
4
|
+
|
|
5
|
+
class TypeContainer:
|
|
6
|
+
"""Tiny type-based DI: register_instance(T, obj) / register_factory(T, () -> obj)."""
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._instances: Dict[Type[Any], Any] = {}
|
|
9
|
+
self._factories: Dict[Type[Any], Callable[[], Any]] = {}
|
|
10
|
+
|
|
11
|
+
def register_instance(self, t: Type[Any], instance: Any) -> None:
|
|
12
|
+
self._instances[t] = instance
|
|
13
|
+
|
|
14
|
+
def register_factory(self, t: Type[Any], factory: Callable[[], Any]) -> None:
|
|
15
|
+
self._factories[t] = factory
|
|
16
|
+
|
|
17
|
+
def _providers(self):
|
|
18
|
+
for t, v in self._instances.items():
|
|
19
|
+
yield t, (lambda v=v: v)
|
|
20
|
+
for t, f in self._factories.items():
|
|
21
|
+
yield t, f
|
|
22
|
+
|
|
23
|
+
def _unwrap(self, ann: Any) -> tuple[list[Type[Any]], bool]:
|
|
24
|
+
if ann is inspect._empty:
|
|
25
|
+
return [], True
|
|
26
|
+
origin = get_origin(ann)
|
|
27
|
+
args = list(get_args(ann))
|
|
28
|
+
allow_none = False
|
|
29
|
+
if origin in (Optional, getattr(__import__("typing"), "Union")):
|
|
30
|
+
if type(None) in args:
|
|
31
|
+
allow_none = True
|
|
32
|
+
args = [a for a in args if a is not type(None)]
|
|
33
|
+
return [a for a in args if isinstance(a, type)], allow_none
|
|
34
|
+
if isinstance(ann, type):
|
|
35
|
+
return [ann], False
|
|
36
|
+
return [], True
|
|
37
|
+
|
|
38
|
+
def resolve(self, ann: Any) -> Any:
|
|
39
|
+
types, allow_none = self._unwrap(ann)
|
|
40
|
+
if not types:
|
|
41
|
+
if allow_none:
|
|
42
|
+
return None
|
|
43
|
+
raise LookupError(f"Cannot resolve {ann!r}")
|
|
44
|
+
for T in types:
|
|
45
|
+
# exact
|
|
46
|
+
for pt, make in self._providers():
|
|
47
|
+
if pt is T:
|
|
48
|
+
return make()
|
|
49
|
+
# supertype/provider match
|
|
50
|
+
best = None
|
|
51
|
+
for pt, make in self._providers():
|
|
52
|
+
try:
|
|
53
|
+
if issubclass(T, pt):
|
|
54
|
+
dist = _mro_distance(T, pt)
|
|
55
|
+
if best is None or dist < best[0]:
|
|
56
|
+
best = (dist, make)
|
|
57
|
+
except TypeError:
|
|
58
|
+
pass
|
|
59
|
+
if best:
|
|
60
|
+
return best[1]()
|
|
61
|
+
if allow_none:
|
|
62
|
+
return None
|
|
63
|
+
raise LookupError(f"No provider for {types}")
|
|
64
|
+
|
|
65
|
+
def _mro_distance(sub: Type[Any], sup: Type[Any]) -> int:
|
|
66
|
+
try:
|
|
67
|
+
return sub.mro().index(sup)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return 10**6
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def bind_callable(fn: Callable[..., Any], c: TypeContainer) -> Callable[..., Any]:
|
|
73
|
+
sig = inspect.signature(fn)
|
|
74
|
+
hints = get_type_hints(fn)
|
|
75
|
+
def wrapper(message: Any):
|
|
76
|
+
kwargs = {}
|
|
77
|
+
for name, param in list(sig.parameters.items())[1:]:
|
|
78
|
+
ann = hints.get(name, param.annotation)
|
|
79
|
+
kwargs[name] = c.resolve(ann)
|
|
80
|
+
return fn(message, **kwargs)
|
|
81
|
+
return wrapper
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def bind_handler(handler: Any, c: TypeContainer) -> Callable[..., Any]:
|
|
85
|
+
if inspect.isclass(handler):
|
|
86
|
+
init = handler.__init__ # type: ignore[attr-defined]
|
|
87
|
+
sig = inspect.signature(init)
|
|
88
|
+
hints = get_type_hints(init)
|
|
89
|
+
kwargs = {}
|
|
90
|
+
for name, param in list(sig.parameters.items())[1:]:
|
|
91
|
+
ann = hints.get(name, param.annotation)
|
|
92
|
+
kwargs[name] = c.resolve(ann)
|
|
93
|
+
instance = handler(**kwargs) # type: ignore
|
|
94
|
+
if not callable(instance):
|
|
95
|
+
raise TypeError(f"{handler!r} must implement __call__")
|
|
96
|
+
return instance
|
|
97
|
+
elif callable(handler):
|
|
98
|
+
return bind_callable(handler, c)
|
|
99
|
+
raise TypeError(f"Unsupported handler: {handler!r}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from typing import Dict, Type, List, Callable, TypeVar, Optional, Iterable, Coroutine, Any
|
|
2
|
+
|
|
3
|
+
from nlbone.core.application.di import TypeContainer, bind_handler
|
|
4
|
+
from nlbone.core.domain.base import DomainEvent, Command
|
|
5
|
+
from nlbone.core.ports.event_bus import Message
|
|
6
|
+
|
|
7
|
+
TMsg = TypeVar("TMsg", bound=Message)
|
|
8
|
+
SyncHandler = Callable[[TMsg], Optional[Iterable[Message]]]
|
|
9
|
+
AsyncHandler = Callable[[TMsg], Coroutine[Any, Any, Optional[Iterable[Message]]]]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HandlerRegistry:
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self._event_handlers: Dict[Type[DomainEvent], List[SyncHandler[Any]]] = {}
|
|
15
|
+
self._command_handlers: Dict[Type[Command], SyncHandler[Any]] = {}
|
|
16
|
+
|
|
17
|
+
def register_event(self, event_type: Type[DomainEvent], handler: SyncHandler[Any]) -> None:
|
|
18
|
+
self._event_handlers.setdefault(event_type, []).append(handler)
|
|
19
|
+
|
|
20
|
+
def register_command(self, cmd_type: Type[Command], handler: SyncHandler[Any]) -> None:
|
|
21
|
+
if cmd_type in self._command_handlers:
|
|
22
|
+
raise ValueError(f"Command handler already registered for {cmd_type!r}")
|
|
23
|
+
self._command_handlers[cmd_type] = handler
|
|
24
|
+
|
|
25
|
+
def for_event(self, event_type: Type[DomainEvent]) -> List[SyncHandler[Any]]:
|
|
26
|
+
return self._event_handlers.get(event_type, [])
|
|
27
|
+
|
|
28
|
+
def for_command(self, cmd_type: Type[Command]) -> SyncHandler[Any]:
|
|
29
|
+
try:
|
|
30
|
+
return self._command_handlers[cmd_type]
|
|
31
|
+
except KeyError as e:
|
|
32
|
+
raise KeyError(f"No handler for command {cmd_type.__name__}") from e
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncHandlerRegistry:
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._event_handlers: Dict[Type[DomainEvent], List[AsyncHandler[Any]]] = {}
|
|
38
|
+
self._command_handlers: Dict[Type[Command], AsyncHandler[Any]] = {}
|
|
39
|
+
|
|
40
|
+
def register_event(self, event_type: Type[DomainEvent], handler: AsyncHandler[Any]) -> None:
|
|
41
|
+
self._event_handlers.setdefault(event_type, []).append(handler)
|
|
42
|
+
|
|
43
|
+
def register_command(self, cmd_type: Type[Command], handler: AsyncHandler[Any]) -> None:
|
|
44
|
+
if cmd_type in self._command_handlers:
|
|
45
|
+
raise ValueError(f"Command handler already registered for {cmd_type!r}")
|
|
46
|
+
self._command_handlers[cmd_type] = handler
|
|
47
|
+
|
|
48
|
+
def for_event(self, event_type: Type[DomainEvent]) -> List[AsyncHandler[Any]]:
|
|
49
|
+
return self._event_handlers.get(event_type, [])
|
|
50
|
+
|
|
51
|
+
def for_command(self, cmd_type: Type[Command]) -> AsyncHandler[Any]:
|
|
52
|
+
try:
|
|
53
|
+
return self._command_handlers[cmd_type]
|
|
54
|
+
except KeyError as e:
|
|
55
|
+
raise KeyError(f"No handler for command {cmd_type.__name__}") from e
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def handles_event(event_type: Type[DomainEvent], registry: HandlerRegistry):
|
|
59
|
+
def deco(fn: SyncHandler[Any]) -> SyncHandler[Any]:
|
|
60
|
+
registry.register_event(event_type, fn)
|
|
61
|
+
return fn
|
|
62
|
+
|
|
63
|
+
return deco
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def handles_command(cmd_type: Type[Command], registry: HandlerRegistry):
|
|
67
|
+
def deco(fn: SyncHandler[Any]) -> SyncHandler[Any]:
|
|
68
|
+
registry.register_command(cmd_type, fn)
|
|
69
|
+
return fn
|
|
70
|
+
|
|
71
|
+
return deco
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def handles_event_async(event_type: Type[DomainEvent], registry: AsyncHandlerRegistry):
|
|
75
|
+
def deco(fn: AsyncHandler[Any]) -> AsyncHandler[Any]:
|
|
76
|
+
registry.register_event(event_type, fn)
|
|
77
|
+
return fn
|
|
78
|
+
|
|
79
|
+
return deco
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def handles_command_async(cmd_type: Type[Command], registry: AsyncHandlerRegistry):
|
|
83
|
+
def deco(fn: AsyncHandler[Any]) -> AsyncHandler[Any]:
|
|
84
|
+
registry.register_command(cmd_type, fn)
|
|
85
|
+
return fn
|
|
86
|
+
|
|
87
|
+
return deco
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def make_sync_decorators(registry: HandlerRegistry, container: TypeContainer):
|
|
91
|
+
def handles_command(cmd_type: Type[Any]):
|
|
92
|
+
def deco(h: Any):
|
|
93
|
+
registry.register_command(cmd_type, bind_handler(h, container))
|
|
94
|
+
return h
|
|
95
|
+
|
|
96
|
+
return deco
|
|
97
|
+
|
|
98
|
+
def handles_event(evt_type: Type[Any]):
|
|
99
|
+
def deco(h: Any):
|
|
100
|
+
registry.register_event(evt_type, bind_handler(h, container))
|
|
101
|
+
return h
|
|
102
|
+
|
|
103
|
+
return deco
|
|
104
|
+
|
|
105
|
+
return handles_command, handles_event
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def make_async_decorators(registry: AsyncHandlerRegistry, container: TypeContainer):
|
|
109
|
+
def handles_command_async(cmd_type: Type[Any]):
|
|
110
|
+
def deco(h: Any):
|
|
111
|
+
registry.register_command(cmd_type, bind_handler(h, container))
|
|
112
|
+
return h
|
|
113
|
+
|
|
114
|
+
return deco
|
|
115
|
+
|
|
116
|
+
def handles_event_async(evt_type: Type[Any]):
|
|
117
|
+
def deco(h: Any):
|
|
118
|
+
registry.register_event(evt_type, bind_handler(h, container))
|
|
119
|
+
return h
|
|
120
|
+
|
|
121
|
+
return deco
|
|
122
|
+
|
|
123
|
+
return handles_command_async, handles_event_async
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import (
|
|
3
|
+
TypeVar,
|
|
4
|
+
Generic,
|
|
5
|
+
Optional,
|
|
6
|
+
Iterable,
|
|
7
|
+
Callable,
|
|
8
|
+
List,
|
|
9
|
+
ClassVar,
|
|
10
|
+
Type,
|
|
11
|
+
Any,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from nlbone.interfaces.api.exceptions import NotFoundException
|
|
15
|
+
|
|
16
|
+
ID = TypeVar("ID")
|
|
17
|
+
T = TypeVar("T", bound="HasId")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Repository(Generic[T, ID], ABC):
|
|
21
|
+
model: ClassVar[Type[Any]]
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get(self, id: ID) -> Optional[T]: ...
|
|
25
|
+
|
|
26
|
+
def get_or_raise(self, id: ID) -> T:
|
|
27
|
+
...
|
|
28
|
+
entity = self.get(id)
|
|
29
|
+
if entity is None:
|
|
30
|
+
raise NotFoundException(f"Entity with id={id!r} not found")
|
|
31
|
+
return entity
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def list(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
offset: int = 0,
|
|
38
|
+
limit: Optional[int] = None,
|
|
39
|
+
where: Optional[Callable[[T], bool]] = None,
|
|
40
|
+
order_by: Optional[Callable[[T], object]] = None,
|
|
41
|
+
reverse: bool = False,
|
|
42
|
+
) -> List[T]: ...
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def count(self, *, where: Optional[Callable[[T], bool]] = None) -> int: ...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def exists(self, id: ID) -> bool: ...
|
|
49
|
+
|
|
50
|
+
# --- Write ---
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def add(self, entity: T) -> T: ...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def add_many(self, entities: Iterable[T]) -> List[T]: ...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def update(self, entity: T) -> T: ...
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def delete(self, id: ID) -> bool: ...
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def clear(self) -> None: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# -----------------------------
|
|
68
|
+
# Async Repository (Abstract)
|
|
69
|
+
# -----------------------------
|
|
70
|
+
class AsyncRepository(Generic[T, ID], ABC):
|
|
71
|
+
@property
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def model(self) -> type[T]: ...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
async def get(self, id: ID) -> Optional[T]: ...
|
|
77
|
+
|
|
78
|
+
async def get_or_raise(self, id: ID) -> T:
|
|
79
|
+
entity = await self.get(id)
|
|
80
|
+
if entity is None:
|
|
81
|
+
raise NotFoundException(f"Entity with id={id!r} not found")
|
|
82
|
+
return entity
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
async def list(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
offset: int = 0,
|
|
89
|
+
limit: Optional[int] = None,
|
|
90
|
+
where: Optional[Callable[[T], bool]] = None,
|
|
91
|
+
order_by: Optional[Callable[[T], object]] = None,
|
|
92
|
+
reverse: bool = False,
|
|
93
|
+
) -> List[T]: ...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
async def count(self, *, where: Optional[Callable[[T], bool]] = None) -> int: ...
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def exists(self, id: ID) -> bool: ...
|
|
100
|
+
|
|
101
|
+
# --- Write ---
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def add(self, entity: T) -> T: ...
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def add_many(self, entities: Iterable[T]) -> List[T]: ...
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def update(self, entity: T) -> T: ...
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def delete(self, id: ID) -> bool: ...
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def clear(self) -> None: ...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from nlbone.utils.crypto import decrypt_text, encrypt_text
|
|
4
|
+
|
|
5
|
+
crypto_command = typer.Typer(help="Encryption / Decryption utilities")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@crypto_command.command("encrypt")
|
|
9
|
+
def encrypt_cmd(value: str):
|
|
10
|
+
"""Encrypt a plain text string."""
|
|
11
|
+
encrypted = encrypt_text(value)
|
|
12
|
+
typer.secho(f"🔐 Encrypted:\n{encrypted}", fg=typer.colors.GREEN)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@crypto_command.command("decrypt")
|
|
16
|
+
def decrypt_cmd(value: str):
|
|
17
|
+
"""Decrypt an encrypted token string."""
|
|
18
|
+
try:
|
|
19
|
+
decrypted = decrypt_text(value)
|
|
20
|
+
typer.secho(f"🔓 Decrypted:\n{decrypted}", fg=typer.colors.CYAN)
|
|
21
|
+
except Exception as e:
|
|
22
|
+
typer.secho(f"❌ Failed to decrypt: {e}", fg=typer.colors.RED)
|
nlbone/interfaces/cli/main.py
CHANGED
|
@@ -4,11 +4,13 @@ import typer
|
|
|
4
4
|
|
|
5
5
|
from nlbone.adapters.db import init_sync_engine
|
|
6
6
|
from nlbone.config.settings import get_settings
|
|
7
|
+
from nlbone.interfaces.cli.crypto import crypto_command
|
|
7
8
|
from nlbone.interfaces.cli.init_db import init_db_command
|
|
8
9
|
|
|
9
10
|
app = typer.Typer(help="NLBone CLI")
|
|
10
11
|
|
|
11
12
|
app.add_typer(init_db_command, name="db")
|
|
13
|
+
app.add_typer(crypto_command, name="crypto")
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@app.callback()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from nlbone.adapters.messaging.internal_router import internal_router
|
|
2
2
|
from nlbone.core.ports.event_bus import IntegrationPublisher
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
def run_dispatch_outbox(outbox_repo, publisher: IntegrationPublisher):
|
|
5
6
|
batch = outbox_repo.fetch_pending(limit=200)
|
|
6
7
|
for rec in batch:
|
nlbone/utils/crypto.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
from cryptography.fernet import Fernet
|
|
5
|
+
|
|
6
|
+
from nlbone.config.settings import get_settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_fernet_key() -> Fernet:
|
|
10
|
+
settings = get_settings()
|
|
11
|
+
fernet_key = settings.FERNET_KEY
|
|
12
|
+
|
|
13
|
+
if not fernet_key or not fernet_key.strip():
|
|
14
|
+
raise Exception("❌ FERNET_KEY is required in .env")
|
|
15
|
+
|
|
16
|
+
digest = hashlib.sha256(fernet_key.encode()).digest()
|
|
17
|
+
|
|
18
|
+
return base64.urlsafe_b64encode(digest)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
fernet = Fernet(_get_fernet_key())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def encrypt_text(plaintext: str) -> str:
|
|
25
|
+
return fernet.encrypt(plaintext.encode()).decode()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def decrypt_text(token: str) -> str:
|
|
29
|
+
return fernet.decrypt(token.encode()).decode()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def normalize_mobile(mobile, strip_zero=True, add_country_code=True):
|
|
6
|
+
if not mobile:
|
|
7
|
+
return ""
|
|
8
|
+
|
|
9
|
+
mobile = re.sub(r"\D", "", str(mobile))
|
|
10
|
+
|
|
11
|
+
if mobile.startswith("0098"):
|
|
12
|
+
mobile = mobile[4:]
|
|
13
|
+
elif mobile.startswith("98") and len(mobile) > 10:
|
|
14
|
+
mobile = mobile[2:]
|
|
15
|
+
|
|
16
|
+
if strip_zero and mobile.startswith("0"):
|
|
17
|
+
mobile = mobile[1:]
|
|
18
|
+
|
|
19
|
+
if add_country_code:
|
|
20
|
+
if not mobile.startswith("98"):
|
|
21
|
+
mobile = f"98{mobile}"
|
|
22
|
+
|
|
23
|
+
return mobile
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remove_duplicates(items: List[str]) -> List[str]:
|
|
27
|
+
seen = set()
|
|
28
|
+
out = []
|
|
29
|
+
for x in items:
|
|
30
|
+
if x and x not in seen:
|
|
31
|
+
seen.add(x)
|
|
32
|
+
out.append(x)
|
|
33
|
+
return out
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nlbone
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.20
|
|
4
4
|
Summary: Backbone package for interfaces and infrastructure in Python projects
|
|
5
5
|
Author-email: Amir Hosein Kahkbazzadeh <a.khakbazzadeh@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,6 +8,7 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Requires-Dist: anyio>=4.0
|
|
10
10
|
Requires-Dist: cachetools>=6.2.0
|
|
11
|
+
Requires-Dist: cryptography~=45.0.4
|
|
11
12
|
Requires-Dist: dependency-injector>=4.48.1
|
|
12
13
|
Requires-Dist: elasticsearch==8.14.0
|
|
13
14
|
Requires-Dist: fastapi>=0.116
|
|
@@ -15,7 +15,7 @@ nlbone/adapters/db/postgres/__init__.py,sha256=6JYJH0xZs3aR-zuyMpRhsdzFugmqz8npr
|
|
|
15
15
|
nlbone/adapters/db/postgres/audit.py,sha256=IuWkPitr70UyQ6-GkAedckp8U-Z4cTgzFbdt_bQv1VQ,4800
|
|
16
16
|
nlbone/adapters/db/postgres/base.py,sha256=kha9xmklzhuQAK8QEkNBn-mAHq8dUKbOM-3abaBpWmQ,71
|
|
17
17
|
nlbone/adapters/db/postgres/engine.py,sha256=UCegauVB1gvo42ThytYnn5VIcQBwR-5xhcXYFApRFNk,3448
|
|
18
|
-
nlbone/adapters/db/postgres/query_builder.py,sha256=
|
|
18
|
+
nlbone/adapters/db/postgres/query_builder.py,sha256=Qv_2oZ5OxZwtN3Ts-jaAX_8sLBzb1mpGBhlNF7aR6Wk,12543
|
|
19
19
|
nlbone/adapters/db/postgres/repository.py,sha256=J_DBE73JhHPYCk90c5-O7lQtZbxDgqjjN9OcWy4Omvs,1660
|
|
20
20
|
nlbone/adapters/db/postgres/schema.py,sha256=NlE7Rr8uXypsw4oWkdZhZwcIBHQEPIpoHLxcUo98i6s,1039
|
|
21
21
|
nlbone/adapters/db/postgres/uow.py,sha256=nRxNpY-WoWHpym-XeZ8VHm0MYvtB9wuopOeNdV_ebk8,2088
|
|
@@ -33,15 +33,17 @@ nlbone/adapters/messaging/redis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
33
33
|
nlbone/adapters/percolation/__init__.py,sha256=0h1Bw7FzxgkDIHxeoyQXSfegrhP6VbpYV4QC8njYdRE,38
|
|
34
34
|
nlbone/adapters/percolation/connection.py,sha256=1iJISSwMEh4r_6nJI7mYf_v64Q0eeU1eSI0wLIfOK14,415
|
|
35
35
|
nlbone/adapters/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
nlbone/adapters/repositories/outbox_repo.py,sha256=
|
|
36
|
+
nlbone/adapters/repositories/outbox_repo.py,sha256=AgZ5BIL6UXAsXO7BlrId6XKGdsznDWsz1Jg9wK2Klww,404
|
|
37
37
|
nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
38
|
nlbone/config/logging.py,sha256=Ot6Ctf7EQZlW8YNB-uBdleqI6wixn5fH0Eo6QRgNkQk,4358
|
|
39
|
-
nlbone/config/settings.py,sha256=
|
|
39
|
+
nlbone/config/settings.py,sha256=jmpMeQ6IG_BXv6onwYIvYEauGIlHbEYaf8k6k_Owc9k,4073
|
|
40
40
|
nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
41
|
nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
42
|
nlbone/core/application/base_worker.py,sha256=5brIToSd-vi6tw0ukhHnUZGZhOLq1SQ-NRRy-kp6D24,1193
|
|
43
|
-
nlbone/core/application/
|
|
43
|
+
nlbone/core/application/bus.py,sha256=i-pcIb28lwtkoDwitmXEXxXaf6FSH_jllid0O9QdhX0,6420
|
|
44
|
+
nlbone/core/application/di.py,sha256=95BG1LadZvptXI-kMpwqXpQqkXobVfkzirfBTBirRLs,3603
|
|
44
45
|
nlbone/core/application/events.py,sha256=eQGLE0aZHuWJsy9J-qRse4CMXOtweH9-2rQ7AIPRMEQ,614
|
|
46
|
+
nlbone/core/application/registry.py,sha256=Kvxozb_Z99q4WqP6xRtKoESeQyV9gJFm2olSGNAar08,4474
|
|
45
47
|
nlbone/core/application/services.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
48
|
nlbone/core/application/use_case.py,sha256=3GMQZ3CFK5cbLoBNBgohPft6GBq2j9_wr8iKRq_osQA,247
|
|
47
49
|
nlbone/core/application/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -56,6 +58,7 @@ nlbone/core/ports/event_bus.py,sha256=_Om1GOOT-F325oV6_LJXtLdx4vu5i7KrpTDD3qPJXU
|
|
|
56
58
|
nlbone/core/ports/files.py,sha256=7Ov2ITYRpPwwDTZGCeNVISg8e3A9l08jbOgpTImgfK8,1863
|
|
57
59
|
nlbone/core/ports/messaging.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
60
|
nlbone/core/ports/repo.py,sha256=zOw8CTMAu5DKKy2wZpT3_6JWWjaJCDt7q4dOiJYrCOQ,651
|
|
61
|
+
nlbone/core/ports/repository.py,sha256=2IfyGOERhWQGYkVk3pHswtUQ7_bGxfXZCTu3o29OhWU,2752
|
|
59
62
|
nlbone/core/ports/uow.py,sha256=SmBdRf0NvSdIjQ3Le1QGz8kNGBk7jgNHtNguvXRwmgs,557
|
|
60
63
|
nlbone/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
64
|
nlbone/interfaces/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -84,21 +87,24 @@ nlbone/interfaces/api/schema/__init__.py,sha256=LAqgynfupeqOQ6u0I5ucrcYnojRMZUg9
|
|
|
84
87
|
nlbone/interfaces/api/schema/adaptive_schema.py,sha256=bdWBNpP2NfOJ_in4btXn0lrZOK70x-OqfmZ-NpIJdoQ,3347
|
|
85
88
|
nlbone/interfaces/api/schema/base_response_model.py,sha256=lkBs7k0IcQiSQdJ3KvqDQPr_zwqKNbwaQjcwAE_chnU,599
|
|
86
89
|
nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
+
nlbone/interfaces/cli/crypto.py,sha256=lh2uUbSYKT6XxAt9uP1-VksopqAgdxiSKoKgXwXB0aE,692
|
|
87
91
|
nlbone/interfaces/cli/init_db.py,sha256=C67n2MsJ1vzkJxC8zfUYOxFdd6mEB_vT9agxN6jWoG8,790
|
|
88
|
-
nlbone/interfaces/cli/main.py,sha256=
|
|
92
|
+
nlbone/interfaces/cli/main.py,sha256=0SxjiJlEB0xd8gobxSCg8mzsDx_ZIzUvRn9DWYi8Vw8,787
|
|
89
93
|
nlbone/interfaces/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
nlbone/interfaces/jobs/dispatch_outbox.py,sha256=
|
|
94
|
+
nlbone/interfaces/jobs/dispatch_outbox.py,sha256=C6KWo17SXSETIrlYcGrNzh0SfI_gzfBcj0xp5mr8iXo,805
|
|
91
95
|
nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
96
|
nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
93
97
|
nlbone/utils/cache.py,sha256=hVfkR62o5vllDrE_nY4At10wK0It6qpZ45K1xoj10cQ,5931
|
|
94
98
|
nlbone/utils/cache_keys.py,sha256=Y2YSellHTbUOcoaNbl1jaD4r485VU_e4KXsfBWhYTBo,1075
|
|
95
99
|
nlbone/utils/cache_registry.py,sha256=w28sEfUQZAhzCCqVH5TflWQY3nyDXyEcFWt8hkuHRHw,823
|
|
96
100
|
nlbone/utils/context.py,sha256=MmclJ24BG2uvSTg1IK7J-Da9BhVFDQ5ag4Ggs2FF1_w,1600
|
|
101
|
+
nlbone/utils/crypto.py,sha256=UOxjjyc1B868_R_k2js83bYRIBoo7c0txD8gXLgavDY,661
|
|
97
102
|
nlbone/utils/http.py,sha256=UXUoXgQdTRNT08ho8zl-C5ekfDsD8uf-JiMQ323ooqw,872
|
|
103
|
+
nlbone/utils/normalize_mobile.py,sha256=sGH4tV9gX-6eVKozviNWJhm1DN1J28Nj-ERldCYkS_E,732
|
|
98
104
|
nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
|
|
99
105
|
nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
|
|
100
|
-
nlbone-0.6.
|
|
101
|
-
nlbone-0.6.
|
|
102
|
-
nlbone-0.6.
|
|
103
|
-
nlbone-0.6.
|
|
104
|
-
nlbone-0.6.
|
|
106
|
+
nlbone-0.6.20.dist-info/METADATA,sha256=y5qyXUYRp1IImMd3Y9X4HxrwcFzR6QsBHMu-Q150tho,2264
|
|
107
|
+
nlbone-0.6.20.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
108
|
+
nlbone-0.6.20.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
|
|
109
|
+
nlbone-0.6.20.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
110
|
+
nlbone-0.6.20.dist-info/RECORD,,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Protocol, Type, Any, Dict
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@dataclass(frozen=True)
|
|
6
|
-
class Command:
|
|
7
|
-
pass
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CommandHandler(Protocol):
|
|
11
|
-
def __call__(self, command: Command) -> Any: ...
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class CommandBus:
|
|
15
|
-
def __init__(self) -> None:
|
|
16
|
-
self._handlers: Dict[Type[Command], CommandHandler] = {}
|
|
17
|
-
|
|
18
|
-
def register(self, cmd_type: Type[Command], handler: CommandHandler) -> None:
|
|
19
|
-
self._handlers[cmd_type] = handler
|
|
20
|
-
|
|
21
|
-
def dispatch(self, command: Command) -> Any:
|
|
22
|
-
handler = self._handlers.get(type(command))
|
|
23
|
-
if handler is None:
|
|
24
|
-
raise LookupError(f"No handler registered for {type(command).__name__}")
|
|
25
|
-
return handler(command)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|