python-hexagonal 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.
- hexagonal/__init__.py +2 -0
- hexagonal/adapters/drivens/buses/base/__init__.py +15 -0
- hexagonal/adapters/drivens/buses/base/command_bus.py +69 -0
- hexagonal/adapters/drivens/buses/base/event_bus.py +160 -0
- hexagonal/adapters/drivens/buses/base/infrastructure.py +38 -0
- hexagonal/adapters/drivens/buses/base/message_bus.py +73 -0
- hexagonal/adapters/drivens/buses/base/query.py +82 -0
- hexagonal/adapters/drivens/buses/base/utils.py +1 -0
- hexagonal/adapters/drivens/buses/inmemory/__init__.py +12 -0
- hexagonal/adapters/drivens/buses/inmemory/command_bus.py +70 -0
- hexagonal/adapters/drivens/buses/inmemory/event_bus.py +69 -0
- hexagonal/adapters/drivens/buses/inmemory/infra.py +49 -0
- hexagonal/adapters/drivens/mappers.py +127 -0
- hexagonal/adapters/drivens/repository/base/__init__.py +13 -0
- hexagonal/adapters/drivens/repository/base/repository.py +85 -0
- hexagonal/adapters/drivens/repository/base/unit_of_work.py +75 -0
- hexagonal/adapters/drivens/repository/sqlite/__init__.py +18 -0
- hexagonal/adapters/drivens/repository/sqlite/datastore.py +197 -0
- hexagonal/adapters/drivens/repository/sqlite/env_vars.py +2 -0
- hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +20 -0
- hexagonal/adapters/drivens/repository/sqlite/outbox.py +405 -0
- hexagonal/adapters/drivens/repository/sqlite/repository.py +286 -0
- hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +25 -0
- hexagonal/adapters/drivers/__init__.py +5 -0
- hexagonal/adapters/drivers/app.py +38 -0
- hexagonal/application/__init__.py +29 -0
- hexagonal/application/api.py +61 -0
- hexagonal/application/app.py +76 -0
- hexagonal/application/bus_app.py +70 -0
- hexagonal/application/handlers.py +107 -0
- hexagonal/application/infrastructure.py +64 -0
- hexagonal/application/query.py +71 -0
- hexagonal/domain/__init__.py +77 -0
- hexagonal/domain/aggregate.py +159 -0
- hexagonal/domain/base.py +169 -0
- hexagonal/domain/exceptions.py +38 -0
- hexagonal/entrypoints/__init__.py +4 -0
- hexagonal/entrypoints/app.py +53 -0
- hexagonal/entrypoints/base.py +105 -0
- hexagonal/entrypoints/bus.py +68 -0
- hexagonal/entrypoints/sqlite.py +49 -0
- hexagonal/ports/__init__.py +0 -0
- hexagonal/ports/drivens/__init__.py +43 -0
- hexagonal/ports/drivens/application.py +35 -0
- hexagonal/ports/drivens/buses.py +148 -0
- hexagonal/ports/drivens/infrastructure.py +19 -0
- hexagonal/ports/drivens/repository.py +152 -0
- hexagonal/ports/drivers/__init__.py +3 -0
- hexagonal/ports/drivers/app.py +58 -0
- hexagonal/py.typed +0 -0
- python_hexagonal-0.1.0.dist-info/METADATA +15 -0
- python_hexagonal-0.1.0.dist-info/RECORD +53 -0
- python_hexagonal-0.1.0.dist-info/WHEEL +4 -0
hexagonal/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .command_bus import BaseCommandBus
|
|
2
|
+
from .event_bus import BaseEventBus
|
|
3
|
+
from .infrastructure import BaseBusInfrastructure
|
|
4
|
+
from .message_bus import MessageBus
|
|
5
|
+
from .query import QueryBus
|
|
6
|
+
from .utils import EVENT_BUS_RAISE_ERROR
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"EVENT_BUS_RAISE_ERROR",
|
|
10
|
+
"BaseCommandBus",
|
|
11
|
+
"BaseEventBus",
|
|
12
|
+
"MessageBus",
|
|
13
|
+
"QueryBus",
|
|
14
|
+
"BaseBusInfrastructure",
|
|
15
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any, Mapping, Type
|
|
2
|
+
|
|
3
|
+
from eventsourcing.utils import get_topic
|
|
4
|
+
|
|
5
|
+
from hexagonal.domain import (
|
|
6
|
+
CloudMessage,
|
|
7
|
+
Command,
|
|
8
|
+
HandlerAlreadyRegistered,
|
|
9
|
+
HandlerNotRegistered,
|
|
10
|
+
TCommand,
|
|
11
|
+
)
|
|
12
|
+
from hexagonal.ports.drivens import ICommandBus, IMessageHandler, TManager
|
|
13
|
+
|
|
14
|
+
from .message_bus import MessageBus
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseCommandBus(ICommandBus[TManager], MessageBus[TManager]):
|
|
18
|
+
_handlers: dict[str, IMessageHandler[Any]]
|
|
19
|
+
|
|
20
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
21
|
+
self.__class__._handlers = {}
|
|
22
|
+
super().initialize(env)
|
|
23
|
+
|
|
24
|
+
def _get_name(self, command_type: Type[TCommand]) -> str:
|
|
25
|
+
return get_topic(command_type)
|
|
26
|
+
|
|
27
|
+
def _get_handlers(self, message: CloudMessage[TCommand]) -> list[str]:
|
|
28
|
+
return [self._get_name(message.payload.__class__)]
|
|
29
|
+
|
|
30
|
+
def _handle_message(self, message: CloudMessage[TCommand], handler: str) -> None:
|
|
31
|
+
f_handler = self._handlers.get(handler)
|
|
32
|
+
if f_handler:
|
|
33
|
+
f_handler.handle_message(message)
|
|
34
|
+
else:
|
|
35
|
+
raise HandlerNotRegistered(f"Command: {handler}")
|
|
36
|
+
|
|
37
|
+
def register_handler(
|
|
38
|
+
self, command_type: Type[TCommand], handler: IMessageHandler[TCommand]
|
|
39
|
+
):
|
|
40
|
+
self.verify()
|
|
41
|
+
name = self._get_name(command_type)
|
|
42
|
+
if name in self._handlers:
|
|
43
|
+
raise HandlerAlreadyRegistered(f"Command: {name}")
|
|
44
|
+
self._handlers[name] = handler
|
|
45
|
+
|
|
46
|
+
def unregister_handler(self, command_type: Type[TCommand]):
|
|
47
|
+
self.verify()
|
|
48
|
+
name = self._get_name(command_type)
|
|
49
|
+
if name in self._handlers:
|
|
50
|
+
del self._handlers[name]
|
|
51
|
+
else:
|
|
52
|
+
raise HandlerNotRegistered(f"Command: {name}")
|
|
53
|
+
|
|
54
|
+
def dispatch(
|
|
55
|
+
self, command: TCommand | CloudMessage[TCommand], *, to_outbox: bool = False
|
|
56
|
+
) -> None:
|
|
57
|
+
self.verify()
|
|
58
|
+
cmd = (
|
|
59
|
+
command
|
|
60
|
+
if not isinstance(command, Command)
|
|
61
|
+
else CloudMessage[command.__class__].new(command) # type: ignore
|
|
62
|
+
)
|
|
63
|
+
if to_outbox:
|
|
64
|
+
self.outbox_repository.save(cmd)
|
|
65
|
+
else:
|
|
66
|
+
self.process_command(cmd)
|
|
67
|
+
|
|
68
|
+
def process_command(self, command: CloudMessage[TCommand]) -> None:
|
|
69
|
+
self._process_messages(command)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Callable, Dict, List, Mapping, Type, overload
|
|
3
|
+
|
|
4
|
+
from eventsourcing.utils import TopicError, get_topic, strtobool
|
|
5
|
+
|
|
6
|
+
from hexagonal.domain import (
|
|
7
|
+
CloudMessage,
|
|
8
|
+
HandlerAlreadyRegistered,
|
|
9
|
+
HandlerNotRegistered,
|
|
10
|
+
TEvent,
|
|
11
|
+
TEvento,
|
|
12
|
+
)
|
|
13
|
+
from hexagonal.ports.drivens import IEventBus, IMessageHandler, TManager
|
|
14
|
+
|
|
15
|
+
from .message_bus import MessageBus
|
|
16
|
+
from .utils import EVENT_BUS_RAISE_ERROR
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HandlerError(Exception):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
evento: CloudMessage[TEvento],
|
|
25
|
+
handler: IMessageHandler[TEvent] | Callable[..., None],
|
|
26
|
+
error: Exception,
|
|
27
|
+
):
|
|
28
|
+
super().__init__(f"""
|
|
29
|
+
Error al Manejar Evento {evento.__class__.__name__}
|
|
30
|
+
handler: {
|
|
31
|
+
handler.__class__.__name__ # pyright: ignore[reportUnknownMemberType]
|
|
32
|
+
if isinstance(handler, IMessageHandler)
|
|
33
|
+
else handler.__name__
|
|
34
|
+
}
|
|
35
|
+
evento: {evento.type}
|
|
36
|
+
datos: {evento.model_dump_json(indent=2)}
|
|
37
|
+
error: {error}
|
|
38
|
+
stacktrace: {error.__traceback__}
|
|
39
|
+
""") # type: ignore # noqa: E501
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
43
|
+
handlers: Dict[str, Dict[str, IMessageHandler[Any]]]
|
|
44
|
+
wait_list: Dict[str, List[Callable[..., None]]]
|
|
45
|
+
|
|
46
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
47
|
+
self.handlers = {}
|
|
48
|
+
self.wait_list = {}
|
|
49
|
+
self.raise_error = strtobool(env.get(EVENT_BUS_RAISE_ERROR, "false"))
|
|
50
|
+
super().initialize(env)
|
|
51
|
+
|
|
52
|
+
def _get_key(self, obj: Type[TEvent] | IMessageHandler[TEvent]) -> str:
|
|
53
|
+
if not isinstance(obj, IMessageHandler):
|
|
54
|
+
return get_topic(obj)
|
|
55
|
+
try:
|
|
56
|
+
return get_topic(obj.__class__)
|
|
57
|
+
except TopicError as e:
|
|
58
|
+
raise HandlerAlreadyRegistered(
|
|
59
|
+
f"Handler: {obj.__class__}, error: {e}"
|
|
60
|
+
) from e
|
|
61
|
+
|
|
62
|
+
def subscribe(self, event_type: Type[TEvent], handler: IMessageHandler[TEvent]):
|
|
63
|
+
self.verify()
|
|
64
|
+
key_event = self._get_key(event_type)
|
|
65
|
+
handlers = self.handlers.get(key_event, {})
|
|
66
|
+
key_handler = self._get_key(handler)
|
|
67
|
+
if key_handler in handlers:
|
|
68
|
+
raise HandlerAlreadyRegistered(f"Event: {key_event}")
|
|
69
|
+
handlers[key_handler] = handler
|
|
70
|
+
self.handlers[key_event] = handlers
|
|
71
|
+
|
|
72
|
+
def unsubscribe(self, event_type: Type[TEvent], *handlers: IMessageHandler[TEvent]):
|
|
73
|
+
self.verify()
|
|
74
|
+
key_event = self._get_key(event_type)
|
|
75
|
+
if not handlers:
|
|
76
|
+
if key_event in self.handlers:
|
|
77
|
+
del self.handlers[key_event]
|
|
78
|
+
return
|
|
79
|
+
for handler in handlers:
|
|
80
|
+
key_handler = self._get_key(handler)
|
|
81
|
+
dict_handlers = self.handlers.get(key_event)
|
|
82
|
+
if not dict_handlers:
|
|
83
|
+
raise HandlerNotRegistered(f"Event: {key_event}")
|
|
84
|
+
if key_handler not in dict_handlers:
|
|
85
|
+
raise HandlerNotRegistered(f"Event: {key_event}")
|
|
86
|
+
del dict_handlers[key_handler]
|
|
87
|
+
|
|
88
|
+
if not list(self.handlers[key_event].values()):
|
|
89
|
+
del self.handlers[key_event]
|
|
90
|
+
|
|
91
|
+
def _wait_for(self, event_type: Type[TEvent], handler: Callable[[TEvent], None]):
|
|
92
|
+
name = self._get_key(event_type)
|
|
93
|
+
if name not in self.wait_list:
|
|
94
|
+
self.wait_list[name] = []
|
|
95
|
+
self.wait_list[name].append(handler)
|
|
96
|
+
|
|
97
|
+
def _handle_wait_list(self, event: TEvento):
|
|
98
|
+
event_type = type(event)
|
|
99
|
+
key = self._get_key(event_type)
|
|
100
|
+
wait_list = self.wait_list.get(key)
|
|
101
|
+
while wait_list:
|
|
102
|
+
if self.raise_error:
|
|
103
|
+
handler = wait_list.pop()
|
|
104
|
+
handler(event)
|
|
105
|
+
else:
|
|
106
|
+
try:
|
|
107
|
+
handler = wait_list.pop()
|
|
108
|
+
handler(event)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise HandlerError(event, handler, e) from e # type: ignore
|
|
111
|
+
|
|
112
|
+
@overload
|
|
113
|
+
def wait_for_publish(
|
|
114
|
+
self, event_type: Type[TEvent], handler: Callable[[TEvent], None]
|
|
115
|
+
) -> None: ...
|
|
116
|
+
|
|
117
|
+
@overload
|
|
118
|
+
def wait_for_publish(
|
|
119
|
+
self, event_type: Type[TEvent]
|
|
120
|
+
) -> Callable[[Callable[[TEvent], None]], None]: ...
|
|
121
|
+
|
|
122
|
+
def wait_for_publish(
|
|
123
|
+
self, event_type: Type[TEvent], handler: Callable[[TEvent], None] | None = None
|
|
124
|
+
) -> Callable[[Callable[[TEvent], None]], None] | None:
|
|
125
|
+
self.verify()
|
|
126
|
+
if handler:
|
|
127
|
+
return self._wait_for(event_type, handler)
|
|
128
|
+
|
|
129
|
+
def decorator(func: Callable[[TEvent], None]):
|
|
130
|
+
self._wait_for(event_type, func)
|
|
131
|
+
|
|
132
|
+
return decorator
|
|
133
|
+
|
|
134
|
+
def _get_handlers(self, message: CloudMessage[TEvento]) -> list[str]:
|
|
135
|
+
key_event = self._get_key(type(message.payload))
|
|
136
|
+
handlers = self.handlers.get(key_event, {})
|
|
137
|
+
return list(handlers.keys())
|
|
138
|
+
|
|
139
|
+
def _handle_message(self, message: CloudMessage[TEvento], handler: str) -> None:
|
|
140
|
+
handlers = self.handlers.get(self._get_key(type(message.payload)))
|
|
141
|
+
if not handlers or handler not in handlers:
|
|
142
|
+
raise HandlerNotRegistered(f"Event: {handler}")
|
|
143
|
+
try:
|
|
144
|
+
handlers[handler].handle_message(message)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
if self.raise_error:
|
|
147
|
+
logger.exception(
|
|
148
|
+
"Error al Manejar Evento %s, con handler %s", message, handler
|
|
149
|
+
)
|
|
150
|
+
raise
|
|
151
|
+
raise HandlerError(message, handlers[handler], e) from e
|
|
152
|
+
|
|
153
|
+
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
154
|
+
try:
|
|
155
|
+
self._handle_wait_list(message.payload)
|
|
156
|
+
except HandlerError as e:
|
|
157
|
+
logger.error(e)
|
|
158
|
+
|
|
159
|
+
def process_events(self, *events: CloudMessage[TEvent]) -> None:
|
|
160
|
+
return self._process_messages(*events)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from hexagonal.application import InfrastructureGroup
|
|
2
|
+
from hexagonal.ports.drivens import (
|
|
3
|
+
IBaseInfrastructure,
|
|
4
|
+
IBusInfrastructure,
|
|
5
|
+
ICommandBus,
|
|
6
|
+
IEventBus,
|
|
7
|
+
IQueryBus,
|
|
8
|
+
TManager,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseBusInfrastructure(IBusInfrastructure[TManager], InfrastructureGroup):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
command_bus: ICommandBus[TManager],
|
|
16
|
+
event_bus: IEventBus[TManager],
|
|
17
|
+
query_bus: IQueryBus[TManager],
|
|
18
|
+
*args: IBaseInfrastructure,
|
|
19
|
+
):
|
|
20
|
+
self._command_bus = command_bus
|
|
21
|
+
self._event_bus = event_bus
|
|
22
|
+
self._query_bus = query_bus
|
|
23
|
+
super().__init__(self._command_bus, self._event_bus, self._query_bus, *args)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def command_bus(self):
|
|
27
|
+
self.verify()
|
|
28
|
+
return self._command_bus
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def event_bus(self):
|
|
32
|
+
self.verify()
|
|
33
|
+
return self._event_bus
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def query_bus(self):
|
|
37
|
+
self.verify()
|
|
38
|
+
return self._query_bus
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from hexagonal.application import Infrastructure
|
|
5
|
+
from hexagonal.domain import CloudMessage
|
|
6
|
+
from hexagonal.ports.drivens import (
|
|
7
|
+
IBaseMessageBus,
|
|
8
|
+
IInboxRepository,
|
|
9
|
+
IOutboxRepository,
|
|
10
|
+
TManager,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageBus(IBaseMessageBus[TManager], Infrastructure):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
inbox_repository: IInboxRepository[TManager],
|
|
18
|
+
outbox_repository: IOutboxRepository[TManager],
|
|
19
|
+
):
|
|
20
|
+
self._inbox_repository = inbox_repository
|
|
21
|
+
self._outbox_repository = outbox_repository
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def inbox_repository(self) -> IInboxRepository[TManager]:
|
|
26
|
+
return self._inbox_repository
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def outbox_repository(self) -> IOutboxRepository[TManager]:
|
|
30
|
+
return self._outbox_repository
|
|
31
|
+
|
|
32
|
+
# publish
|
|
33
|
+
def publish_from_outbox(self, limit: int | None = None):
|
|
34
|
+
self.verify()
|
|
35
|
+
messages = self.outbox_repository.fetch_pending(limit=limit)
|
|
36
|
+
self._publish_messages(*messages)
|
|
37
|
+
|
|
38
|
+
def _publish_messages(self, *messages: CloudMessage[Any]) -> None:
|
|
39
|
+
for message in messages:
|
|
40
|
+
try:
|
|
41
|
+
self._publish_message(message)
|
|
42
|
+
self.outbox_repository.mark_as_published(message.message_id)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self.outbox_repository.mark_as_failed(message.message_id, error=str(e))
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def _publish_message(self, message: CloudMessage[Any]) -> None: ...
|
|
48
|
+
|
|
49
|
+
# consume
|
|
50
|
+
def _process_messages(self, *messages: CloudMessage[Any]) -> None:
|
|
51
|
+
handlers: list[tuple[CloudMessage[Any], str]] = []
|
|
52
|
+
for msg in messages:
|
|
53
|
+
handlers.extend((msg, handler) for handler in self._get_handlers(msg))
|
|
54
|
+
for msg, handler in handlers:
|
|
55
|
+
self._process_message(msg, handler)
|
|
56
|
+
|
|
57
|
+
def _process_message(self, message: CloudMessage[Any], handler: str) -> None:
|
|
58
|
+
duplicated = self.inbox_repository.register_message(message, handler)
|
|
59
|
+
if not duplicated:
|
|
60
|
+
try:
|
|
61
|
+
self._handle_message(message, handler)
|
|
62
|
+
self.inbox_repository.mark_as_processed(message.message_id, handler)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self.inbox_repository.mark_as_failed(
|
|
65
|
+
message.message_id, handler, error=str(e)
|
|
66
|
+
)
|
|
67
|
+
raise
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def _get_handlers(self, message: CloudMessage[Any]) -> list[str]: ...
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def _handle_message(self, message: CloudMessage[Any], handler: str) -> None: ...
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Any, Literal, Mapping, Type, overload
|
|
2
|
+
|
|
3
|
+
from eventsourcing.utils import get_topic
|
|
4
|
+
|
|
5
|
+
from hexagonal.application import Infrastructure
|
|
6
|
+
from hexagonal.domain import (
|
|
7
|
+
HandlerAlreadyRegistered,
|
|
8
|
+
HandlerNotRegistered,
|
|
9
|
+
Query,
|
|
10
|
+
QueryResult,
|
|
11
|
+
QueryResults,
|
|
12
|
+
TQuery,
|
|
13
|
+
TView,
|
|
14
|
+
)
|
|
15
|
+
from hexagonal.ports.drivens import IQueryBus, IQueryHandler, TManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
19
|
+
handlers: dict[str, IQueryHandler[TManager, Any, Any]]
|
|
20
|
+
|
|
21
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
22
|
+
self.handlers = {}
|
|
23
|
+
super().initialize(env)
|
|
24
|
+
|
|
25
|
+
def _get_name(self, query_type: Type[Query[TView]]) -> str:
|
|
26
|
+
return get_topic(query_type)
|
|
27
|
+
|
|
28
|
+
def _get_handler(
|
|
29
|
+
self, query: Query[TView]
|
|
30
|
+
) -> IQueryHandler[TManager, Query[TView], TView] | None:
|
|
31
|
+
name = self._get_name(query.__class__)
|
|
32
|
+
return self.handlers.get(name)
|
|
33
|
+
|
|
34
|
+
def register_handler(
|
|
35
|
+
self,
|
|
36
|
+
query_type: Type[TQuery],
|
|
37
|
+
handler: IQueryHandler[TManager, TQuery, TView],
|
|
38
|
+
):
|
|
39
|
+
self.verify()
|
|
40
|
+
name = self._get_name(query_type)
|
|
41
|
+
if name in self.handlers:
|
|
42
|
+
raise HandlerAlreadyRegistered(f"Query: {name}")
|
|
43
|
+
self.handlers[name] = handler
|
|
44
|
+
|
|
45
|
+
def unregister_handler(self, query_type: Type[TQuery]):
|
|
46
|
+
self.verify()
|
|
47
|
+
name = self._get_name(query_type)
|
|
48
|
+
if name in self.handlers:
|
|
49
|
+
del self.handlers[name]
|
|
50
|
+
else:
|
|
51
|
+
raise HandlerNotRegistered(f"Query: {name}")
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def get(self, query: Query[TView], *, one: Literal[True]) -> QueryResult[TView]: ...
|
|
55
|
+
|
|
56
|
+
@overload
|
|
57
|
+
def get(
|
|
58
|
+
self,
|
|
59
|
+
query: Query[TView],
|
|
60
|
+
*,
|
|
61
|
+
one: Literal[False] = False,
|
|
62
|
+
) -> QueryResults[TView]: ...
|
|
63
|
+
|
|
64
|
+
def get(
|
|
65
|
+
self,
|
|
66
|
+
query: Query[TView],
|
|
67
|
+
*,
|
|
68
|
+
one: bool = False,
|
|
69
|
+
) -> QueryResult[TView] | QueryResults[TView]:
|
|
70
|
+
self.verify()
|
|
71
|
+
name = self._get_name(query.__class__)
|
|
72
|
+
handler = self._get_handler(query)
|
|
73
|
+
if not handler:
|
|
74
|
+
raise HandlerNotRegistered(f"Query: {name}")
|
|
75
|
+
results = handler.get(query)
|
|
76
|
+
if not one:
|
|
77
|
+
return results
|
|
78
|
+
if len(results) == 0:
|
|
79
|
+
raise ValueError("No results found")
|
|
80
|
+
if len(results) > 1:
|
|
81
|
+
raise ValueError("More than one result found")
|
|
82
|
+
return QueryResult[TView](item=results.items[0])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
EVENT_BUS_RAISE_ERROR = "EVENT_BUS_RAISE_ERROR"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .command_bus import InMemoryCommandBus, InMemoryQueueCommandBus
|
|
2
|
+
from .event_bus import InMemoryEventBus, InMemoryQueueEventBus
|
|
3
|
+
from .infra import InMemoryBusInfrastructure, InMemoryQueueBusInfrastructure
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"InMemoryEventBus",
|
|
7
|
+
"InMemoryCommandBus",
|
|
8
|
+
"InMemoryQueueCommandBus",
|
|
9
|
+
"InMemoryBusInfrastructure",
|
|
10
|
+
"InMemoryQueueBusInfrastructure",
|
|
11
|
+
"InMemoryQueueEventBus",
|
|
12
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pyright: reportMissingParameterType=none, reportGeneralTypeIssues=none
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from queue import Empty, Queue
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
from hexagonal.adapters.drivens.buses.base import BaseCommandBus
|
|
9
|
+
from hexagonal.domain import CloudMessage, TCommand
|
|
10
|
+
from hexagonal.ports.drivens import TManager
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InMemoryCommandBus(BaseCommandBus[TManager]):
|
|
16
|
+
def _publish_message(self, message: CloudMessage[TCommand]) -> None:
|
|
17
|
+
return self._process_messages(message)
|
|
18
|
+
|
|
19
|
+
def consume(self, limit: int | None = None):
|
|
20
|
+
return # No-op for non-queued bus
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InMemoryQueueCommandBus(BaseCommandBus[TManager]):
|
|
24
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
25
|
+
self.queue: Queue[CloudMessage[Any]] = Queue()
|
|
26
|
+
self._stop = threading.Event()
|
|
27
|
+
self._worker = threading.Thread(target=self._worker_loop, daemon=True)
|
|
28
|
+
|
|
29
|
+
super().initialize(env)
|
|
30
|
+
|
|
31
|
+
def shutdown(self) -> None:
|
|
32
|
+
# Si tienes lifecycle, llama esto al apagar la app
|
|
33
|
+
self._stop.set()
|
|
34
|
+
self._worker.join(timeout=5)
|
|
35
|
+
|
|
36
|
+
def _publish_message(self, message: CloudMessage[TCommand]) -> None:
|
|
37
|
+
self.verify()
|
|
38
|
+
self.queue.put(
|
|
39
|
+
message
|
|
40
|
+
) # bloquea si hubiera maxsize; puedes usar put_nowait si quieres
|
|
41
|
+
logger.debug(
|
|
42
|
+
"Enqueued command: %s | %s | %s | %s",
|
|
43
|
+
message.message_id,
|
|
44
|
+
message.causation_id,
|
|
45
|
+
message.correlation_id,
|
|
46
|
+
message.type,
|
|
47
|
+
)
|
|
48
|
+
# No llamamos consume() aquí. El worker ya está drenando.
|
|
49
|
+
|
|
50
|
+
def _worker_loop(self) -> None:
|
|
51
|
+
while not self._stop.is_set():
|
|
52
|
+
try:
|
|
53
|
+
message = self.queue.get(timeout=0.2)
|
|
54
|
+
except Empty:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
logger.debug(
|
|
59
|
+
"Processing command: %s | %s | %s | %s",
|
|
60
|
+
message.message_id,
|
|
61
|
+
message.causation_id,
|
|
62
|
+
message.correlation_id,
|
|
63
|
+
message.type,
|
|
64
|
+
)
|
|
65
|
+
self.dispatch(message)
|
|
66
|
+
finally:
|
|
67
|
+
self.queue.task_done()
|
|
68
|
+
|
|
69
|
+
def consume(self, limit: int | None = None):
|
|
70
|
+
self._worker.start()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pyright: reportMissingParameterType=none, reportGeneralTypeIssues=none, reportUnknownMemberType=none
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from queue import Empty, Queue
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
from hexagonal.adapters.drivens.buses.base import BaseEventBus
|
|
9
|
+
from hexagonal.domain import CloudMessage, TEvent, TEvento
|
|
10
|
+
from hexagonal.ports.drivens import TManager
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InMemoryEventBus(BaseEventBus[TManager]):
|
|
16
|
+
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
17
|
+
super()._publish_message(message)
|
|
18
|
+
self._process_messages(message)
|
|
19
|
+
|
|
20
|
+
def publish(self, *events: CloudMessage[TEvent]) -> None:
|
|
21
|
+
return self._publish_messages(*events)
|
|
22
|
+
|
|
23
|
+
def consume(self, limit: int | None = None):
|
|
24
|
+
pass # No-op for non-queued bus
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
28
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
29
|
+
self.queue: Queue[CloudMessage[TEvento]] = Queue() # o Queue(maxsize=...)
|
|
30
|
+
self._stop = threading.Event()
|
|
31
|
+
self._worker = threading.Thread(target=self._worker_loop, daemon=True)
|
|
32
|
+
|
|
33
|
+
super().initialize(env)
|
|
34
|
+
|
|
35
|
+
def shutdown(self) -> None:
|
|
36
|
+
# Llamar en el lifecycle de tu app
|
|
37
|
+
self._stop.set()
|
|
38
|
+
self._worker.join(timeout=5)
|
|
39
|
+
|
|
40
|
+
def publish(self, *events: CloudMessage[TEvent]) -> None:
|
|
41
|
+
return self._publish_messages(*events)
|
|
42
|
+
|
|
43
|
+
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
44
|
+
super()._publish_message(message)
|
|
45
|
+
self.verify()
|
|
46
|
+
self.queue.put(message)
|
|
47
|
+
# No llamamos consume() aquí: el worker se encarga.
|
|
48
|
+
|
|
49
|
+
def _worker_loop(self) -> None:
|
|
50
|
+
while not self._stop.is_set():
|
|
51
|
+
try:
|
|
52
|
+
event = self.queue.get(timeout=0.2)
|
|
53
|
+
except Empty:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
logger.debug(
|
|
58
|
+
"Processing event: %s | %s | %s | %s",
|
|
59
|
+
event.message_id,
|
|
60
|
+
event.causation_id,
|
|
61
|
+
event.correlation_id,
|
|
62
|
+
event.type,
|
|
63
|
+
)
|
|
64
|
+
self._process_messages(event)
|
|
65
|
+
finally:
|
|
66
|
+
self.queue.task_done()
|
|
67
|
+
|
|
68
|
+
def consume(self, limit: int | None = None):
|
|
69
|
+
self._worker.start()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# pyright: reportMissingParameterType=none, reportGeneralTypeIssues=none
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from hexagonal.adapters.drivens.buses.base import BaseBusInfrastructure, QueryBus
|
|
6
|
+
from hexagonal.ports.drivens import (
|
|
7
|
+
IInboxRepository,
|
|
8
|
+
IOutboxRepository,
|
|
9
|
+
TManager,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .command_bus import InMemoryCommandBus, InMemoryQueueCommandBus
|
|
13
|
+
from .event_bus import InMemoryEventBus, InMemoryQueueEventBus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InMemoryBusInfrastructure(BaseBusInfrastructure[TManager]):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
inbox: IInboxRepository[TManager],
|
|
20
|
+
outbox: IOutboxRepository[TManager],
|
|
21
|
+
*args: Any,
|
|
22
|
+
**kwargs: Any,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(
|
|
25
|
+
InMemoryCommandBus(inbox, outbox),
|
|
26
|
+
InMemoryEventBus(inbox, outbox),
|
|
27
|
+
QueryBus[TManager](),
|
|
28
|
+
inbox,
|
|
29
|
+
outbox,
|
|
30
|
+
*args,
|
|
31
|
+
**kwargs,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InMemoryQueueBusInfrastructure(BaseBusInfrastructure[TManager]):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
inbox: IInboxRepository[TManager],
|
|
39
|
+
outbox: IOutboxRepository[TManager],
|
|
40
|
+
*args: Any,
|
|
41
|
+
**kwargs: Any,
|
|
42
|
+
):
|
|
43
|
+
super().__init__(
|
|
44
|
+
InMemoryQueueCommandBus(inbox, outbox),
|
|
45
|
+
InMemoryQueueEventBus(inbox, outbox),
|
|
46
|
+
QueryBus(),
|
|
47
|
+
inbox,
|
|
48
|
+
outbox,
|
|
49
|
+
)
|