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.
Files changed (53) hide show
  1. hexagonal/__init__.py +2 -0
  2. hexagonal/adapters/drivens/buses/base/__init__.py +15 -0
  3. hexagonal/adapters/drivens/buses/base/command_bus.py +69 -0
  4. hexagonal/adapters/drivens/buses/base/event_bus.py +160 -0
  5. hexagonal/adapters/drivens/buses/base/infrastructure.py +38 -0
  6. hexagonal/adapters/drivens/buses/base/message_bus.py +73 -0
  7. hexagonal/adapters/drivens/buses/base/query.py +82 -0
  8. hexagonal/adapters/drivens/buses/base/utils.py +1 -0
  9. hexagonal/adapters/drivens/buses/inmemory/__init__.py +12 -0
  10. hexagonal/adapters/drivens/buses/inmemory/command_bus.py +70 -0
  11. hexagonal/adapters/drivens/buses/inmemory/event_bus.py +69 -0
  12. hexagonal/adapters/drivens/buses/inmemory/infra.py +49 -0
  13. hexagonal/adapters/drivens/mappers.py +127 -0
  14. hexagonal/adapters/drivens/repository/base/__init__.py +13 -0
  15. hexagonal/adapters/drivens/repository/base/repository.py +85 -0
  16. hexagonal/adapters/drivens/repository/base/unit_of_work.py +75 -0
  17. hexagonal/adapters/drivens/repository/sqlite/__init__.py +18 -0
  18. hexagonal/adapters/drivens/repository/sqlite/datastore.py +197 -0
  19. hexagonal/adapters/drivens/repository/sqlite/env_vars.py +2 -0
  20. hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +20 -0
  21. hexagonal/adapters/drivens/repository/sqlite/outbox.py +405 -0
  22. hexagonal/adapters/drivens/repository/sqlite/repository.py +286 -0
  23. hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +25 -0
  24. hexagonal/adapters/drivers/__init__.py +5 -0
  25. hexagonal/adapters/drivers/app.py +38 -0
  26. hexagonal/application/__init__.py +29 -0
  27. hexagonal/application/api.py +61 -0
  28. hexagonal/application/app.py +76 -0
  29. hexagonal/application/bus_app.py +70 -0
  30. hexagonal/application/handlers.py +107 -0
  31. hexagonal/application/infrastructure.py +64 -0
  32. hexagonal/application/query.py +71 -0
  33. hexagonal/domain/__init__.py +77 -0
  34. hexagonal/domain/aggregate.py +159 -0
  35. hexagonal/domain/base.py +169 -0
  36. hexagonal/domain/exceptions.py +38 -0
  37. hexagonal/entrypoints/__init__.py +4 -0
  38. hexagonal/entrypoints/app.py +53 -0
  39. hexagonal/entrypoints/base.py +105 -0
  40. hexagonal/entrypoints/bus.py +68 -0
  41. hexagonal/entrypoints/sqlite.py +49 -0
  42. hexagonal/ports/__init__.py +0 -0
  43. hexagonal/ports/drivens/__init__.py +43 -0
  44. hexagonal/ports/drivens/application.py +35 -0
  45. hexagonal/ports/drivens/buses.py +148 -0
  46. hexagonal/ports/drivens/infrastructure.py +19 -0
  47. hexagonal/ports/drivens/repository.py +152 -0
  48. hexagonal/ports/drivers/__init__.py +3 -0
  49. hexagonal/ports/drivers/app.py +58 -0
  50. hexagonal/py.typed +0 -0
  51. python_hexagonal-0.1.0.dist-info/METADATA +15 -0
  52. python_hexagonal-0.1.0.dist-info/RECORD +53 -0
  53. python_hexagonal-0.1.0.dist-info/WHEEL +4 -0
hexagonal/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from python-hexagonal!"
@@ -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
+ )