python-hexagonal 0.2.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/PKG-INFO +5 -1
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/README.md +4 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/pyproject.toml +17 -1
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/command_bus.py +8 -11
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/event_bus.py +49 -22
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/infrastructure.py +4 -4
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/message_bus.py +53 -8
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/query.py +9 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/inmemory/command_bus.py +2 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/inmemory/event_bus.py +5 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/mappers.py +2 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/base/repository.py +5 -8
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/base/unit_of_work.py +25 -9
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/__init__.py +2 -1
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/datastore.py +9 -6
- python_hexagonal-0.3.1/src/hexagonal/adapters/drivens/repository/sqlalchemy/infrastructure.py +102 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/outbox.py +2 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/repository.py +1 -1
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/datastore.py +14 -8
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/outbox.py +2 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/repository.py +1 -1
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivers/app.py +19 -10
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/__init__.py +9 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/api.py +16 -78
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/app.py +18 -15
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/bus_app.py +4 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/handlers.py +74 -7
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/infrastructure.py +3 -3
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/query.py +22 -6
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/application/topics.py +4 -4
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/domain/__init__.py +3 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/domain/aggregate.py +8 -8
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/domain/base.py +7 -5
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/domain/queries.py +7 -2
- python_hexagonal-0.3.1/src/hexagonal/domain/responses.py +58 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/entrypoints/app.py +13 -6
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/entrypoints/base.py +2 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/entrypoints/bus.py +15 -3
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/entrypoints/sqlalchemy.py +7 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/entrypoints/sqlite.py +9 -2
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/integrations/sqlalchemy.py +2 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivens/__init__.py +18 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivens/buses.py +33 -9
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivens/infrastructure.py +1 -1
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivens/repository.py +32 -9
- python_hexagonal-0.3.1/src/hexagonal/ports/drivens/scoped.py +36 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivers/app.py +3 -3
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/infrastructure.py +0 -41
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/utils.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/inmemory/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/inmemory/infra.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/base/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/env_vars.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/models.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlalchemy/unit_of_work.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/env_vars.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivers/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/domain/exceptions.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/entrypoints/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/integrations/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivens/application.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/ports/drivers/__init__.py +0 -0
- {python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-hexagonal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Framework to build hexagonal architecture applications in Python.
|
|
5
5
|
Author: jose-matos-9281
|
|
6
6
|
Author-email: jose-matos-9281 <58991817+jose-matos-9281@users.noreply.github.com>
|
|
@@ -27,6 +27,8 @@ two-line snippet is enough.
|
|
|
27
27
|
[`docs/explanation/architecture-from-example.md`](docs/explanation/architecture-from-example.md)
|
|
28
28
|
- Bootstrap the adapter-specific SQLAlchemy path:
|
|
29
29
|
[`docs/how-to/bootstrap-sqlalchemy-app.md`](docs/how-to/bootstrap-sqlalchemy-app.md)
|
|
30
|
+
- Migrate existing apps to the scoped execution model:
|
|
31
|
+
[`docs/how-to/migrate-to-0.3.0-scoped-execution.md`](docs/how-to/migrate-to-0.3.0-scoped-execution.md)
|
|
30
32
|
- Learn the canonical testing workflow:
|
|
31
33
|
[`docs/how-to/test-use-cases.md`](docs/how-to/test-use-cases.md)
|
|
32
34
|
- Check the guardrails before copying imports:
|
|
@@ -71,6 +73,8 @@ The documented path is intentionally narrow.
|
|
|
71
73
|
`hexagonal.entrypoints`
|
|
72
74
|
- Reach for `hexagonal.integrations.sqlalchemy` when you need the reusable
|
|
73
75
|
SQLAlchemy repository and unit-of-work utilities
|
|
76
|
+
- Treat scoped execution as the default mental model: shared datastore and
|
|
77
|
+
mapper, fresh write/read scope per operation
|
|
74
78
|
- Treat `hexagonal.entrypoints.sqlalchemy` as adapter-specific convenience, not
|
|
75
79
|
the whole framework story
|
|
76
80
|
- Do not treat `hexagonal.__init__` as the public integration surface; right now
|
|
@@ -13,6 +13,8 @@ two-line snippet is enough.
|
|
|
13
13
|
[`docs/explanation/architecture-from-example.md`](docs/explanation/architecture-from-example.md)
|
|
14
14
|
- Bootstrap the adapter-specific SQLAlchemy path:
|
|
15
15
|
[`docs/how-to/bootstrap-sqlalchemy-app.md`](docs/how-to/bootstrap-sqlalchemy-app.md)
|
|
16
|
+
- Migrate existing apps to the scoped execution model:
|
|
17
|
+
[`docs/how-to/migrate-to-0.3.0-scoped-execution.md`](docs/how-to/migrate-to-0.3.0-scoped-execution.md)
|
|
16
18
|
- Learn the canonical testing workflow:
|
|
17
19
|
[`docs/how-to/test-use-cases.md`](docs/how-to/test-use-cases.md)
|
|
18
20
|
- Check the guardrails before copying imports:
|
|
@@ -57,6 +59,8 @@ The documented path is intentionally narrow.
|
|
|
57
59
|
`hexagonal.entrypoints`
|
|
58
60
|
- Reach for `hexagonal.integrations.sqlalchemy` when you need the reusable
|
|
59
61
|
SQLAlchemy repository and unit-of-work utilities
|
|
62
|
+
- Treat scoped execution as the default mental model: shared datastore and
|
|
63
|
+
mapper, fresh write/read scope per operation
|
|
60
64
|
- Treat `hexagonal.entrypoints.sqlalchemy` as adapter-specific convenience, not
|
|
61
65
|
the whole framework story
|
|
62
66
|
- Do not treat `hexagonal.__init__` as the public integration surface; right now
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-hexagonal"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Framework to build hexagonal architecture applications in Python."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -26,6 +26,22 @@ module-name = "hexagonal"
|
|
|
26
26
|
select = ["I", "E", "F", "B"]
|
|
27
27
|
fixable = ["I", "E", "F", "B"]
|
|
28
28
|
|
|
29
|
+
[tool.mypy]
|
|
30
|
+
mypy_path = ["src"]
|
|
31
|
+
python_version = "3.12"
|
|
32
|
+
strict = true
|
|
33
|
+
namespace_packages = true
|
|
34
|
+
explicit_package_bases = true
|
|
35
|
+
show_error_codes = true
|
|
36
|
+
|
|
37
|
+
[[tool.mypy.overrides]]
|
|
38
|
+
module = ["alembic", "alembic.*"]
|
|
39
|
+
ignore_missing_imports = true
|
|
40
|
+
|
|
41
|
+
[[tool.mypy.overrides]]
|
|
42
|
+
module = ["tests.*"]
|
|
43
|
+
disable_error_code = ["attr-defined", "no-untyped-call", "no-untyped-def", "var-annotated"]
|
|
44
|
+
|
|
29
45
|
[dependency-groups]
|
|
30
46
|
dev = [
|
|
31
47
|
"alembic>=1.18.4",
|
|
@@ -4,7 +4,6 @@ from eventsourcing.utils import get_topic
|
|
|
4
4
|
|
|
5
5
|
from hexagonal.domain import (
|
|
6
6
|
CloudMessage,
|
|
7
|
-
Command,
|
|
8
7
|
HandlerAlreadyRegistered,
|
|
9
8
|
HandlerNotRegistered,
|
|
10
9
|
TCommand,
|
|
@@ -36,14 +35,14 @@ class BaseCommandBus(ICommandBus[TManager], MessageBus[TManager]):
|
|
|
36
35
|
|
|
37
36
|
def register_handler(
|
|
38
37
|
self, command_type: Type[TCommand], handler: IMessageHandler[TCommand]
|
|
39
|
-
):
|
|
38
|
+
) -> None:
|
|
40
39
|
self.verify()
|
|
41
40
|
name = self._get_name(command_type)
|
|
42
41
|
if name in self._handlers:
|
|
43
42
|
raise HandlerAlreadyRegistered(f"Command: {name}")
|
|
44
43
|
self._handlers[name] = handler
|
|
45
44
|
|
|
46
|
-
def unregister_handler(self, command_type: Type[TCommand]):
|
|
45
|
+
def unregister_handler(self, command_type: Type[TCommand]) -> None:
|
|
47
46
|
self.verify()
|
|
48
47
|
name = self._get_name(command_type)
|
|
49
48
|
if name in self._handlers:
|
|
@@ -52,18 +51,16 @@ class BaseCommandBus(ICommandBus[TManager], MessageBus[TManager]):
|
|
|
52
51
|
raise HandlerNotRegistered(f"Command: {name}")
|
|
53
52
|
|
|
54
53
|
def dispatch(
|
|
55
|
-
self,
|
|
54
|
+
self,
|
|
55
|
+
command: CloudMessage[TCommand],
|
|
56
|
+
*,
|
|
57
|
+
to_outbox: bool = False,
|
|
56
58
|
) -> None:
|
|
57
59
|
self.verify()
|
|
58
|
-
cmd = (
|
|
59
|
-
command
|
|
60
|
-
if not isinstance(command, Command)
|
|
61
|
-
else CloudMessage[command.__class__].new(command) # type: ignore
|
|
62
|
-
)
|
|
63
60
|
if to_outbox:
|
|
64
|
-
self.
|
|
61
|
+
self.save_to_outbox(command)
|
|
65
62
|
else:
|
|
66
|
-
self.process_command(
|
|
63
|
+
self.process_command(command)
|
|
67
64
|
|
|
68
65
|
def process_command(self, command: CloudMessage[TCommand]) -> None:
|
|
69
66
|
self._process_messages(command)
|
|
@@ -21,22 +21,36 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
class HandlerError(Exception):
|
|
22
22
|
def __init__(
|
|
23
23
|
self,
|
|
24
|
-
evento: CloudMessage[TEvento],
|
|
24
|
+
evento: CloudMessage[TEvento] | TEvento,
|
|
25
25
|
handler: IMessageHandler[TEvent] | Callable[..., None],
|
|
26
26
|
error: Exception,
|
|
27
|
-
):
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
handler: {
|
|
31
|
-
handler.__class__.__name__ # pyright: ignore[reportUnknownMemberType]
|
|
27
|
+
) -> None:
|
|
28
|
+
handler_name = (
|
|
29
|
+
handler.__class__.__name__
|
|
32
30
|
if isinstance(handler, IMessageHandler)
|
|
33
31
|
else handler.__name__
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
)
|
|
33
|
+
event_name = (
|
|
34
|
+
evento.__class__.__name__
|
|
35
|
+
if not isinstance(evento, CloudMessage)
|
|
36
|
+
else evento.payload.__class__.__name__
|
|
37
|
+
)
|
|
38
|
+
event_topic = (
|
|
39
|
+
evento.TOPIC if not isinstance(evento, CloudMessage) else evento.type
|
|
40
|
+
)
|
|
41
|
+
event_dump = (
|
|
42
|
+
evento.model_dump_json(indent=2)
|
|
43
|
+
if not isinstance(evento, CloudMessage)
|
|
44
|
+
else evento.model_dump_json(indent=2)
|
|
45
|
+
)
|
|
46
|
+
super().__init__(f"""
|
|
47
|
+
Error al Manejar Evento {event_name}
|
|
48
|
+
handler: {handler_name}
|
|
49
|
+
evento: {event_topic}
|
|
50
|
+
datos: {event_dump}
|
|
37
51
|
error: {error}
|
|
38
52
|
stacktrace: {error.__traceback__}
|
|
39
|
-
""")
|
|
53
|
+
""")
|
|
40
54
|
|
|
41
55
|
|
|
42
56
|
class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
@@ -52,6 +66,9 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
52
66
|
def _get_key(self, obj: Type[TEvent] | IMessageHandler[TEvent]) -> str:
|
|
53
67
|
if not isinstance(obj, IMessageHandler):
|
|
54
68
|
return get_topic(obj)
|
|
69
|
+
handler_key = getattr(obj, "handler_key", None)
|
|
70
|
+
if isinstance(handler_key, str):
|
|
71
|
+
return handler_key
|
|
55
72
|
try:
|
|
56
73
|
return get_topic(obj.__class__)
|
|
57
74
|
except TopicError as e:
|
|
@@ -59,7 +76,9 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
59
76
|
f"Handler: {obj.__class__}, error: {e}"
|
|
60
77
|
) from e
|
|
61
78
|
|
|
62
|
-
def subscribe(
|
|
79
|
+
def subscribe(
|
|
80
|
+
self, event_type: Type[TEvent], handler: IMessageHandler[TEvent]
|
|
81
|
+
) -> None:
|
|
63
82
|
self.verify()
|
|
64
83
|
key_event = self._get_key(event_type)
|
|
65
84
|
handlers = self.handlers.get(key_event, {})
|
|
@@ -69,7 +88,9 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
69
88
|
handlers[key_handler] = handler
|
|
70
89
|
self.handlers[key_event] = handlers
|
|
71
90
|
|
|
72
|
-
def unsubscribe(
|
|
91
|
+
def unsubscribe(
|
|
92
|
+
self, event_type: Type[TEvent], *handlers: IMessageHandler[TEvent]
|
|
93
|
+
) -> None:
|
|
73
94
|
self.verify()
|
|
74
95
|
key_event = self._get_key(event_type)
|
|
75
96
|
if not handlers:
|
|
@@ -88,7 +109,9 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
88
109
|
if not list(self.handlers[key_event].values()):
|
|
89
110
|
del self.handlers[key_event]
|
|
90
111
|
|
|
91
|
-
def _wait_for(
|
|
112
|
+
def _wait_for(
|
|
113
|
+
self, event_type: Type[TEvent], handler: Callable[[TEvent], None]
|
|
114
|
+
) -> None:
|
|
92
115
|
name = self._get_key(event_type)
|
|
93
116
|
if name not in self.wait_list:
|
|
94
117
|
self.wait_list[name] = []
|
|
@@ -99,7 +122,7 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
99
122
|
len(self.wait_list[name]),
|
|
100
123
|
)
|
|
101
124
|
|
|
102
|
-
def _handle_wait_list(self, event: TEvento):
|
|
125
|
+
def _handle_wait_list(self, event: TEvento) -> None:
|
|
103
126
|
event_type = type(event)
|
|
104
127
|
key = self._get_key(event_type)
|
|
105
128
|
logger.debug(" [DEBUG _handle_wait_list] Publishing event type=%s", key)
|
|
@@ -112,14 +135,17 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
112
135
|
logger.debug(" [DEBUG _handle_wait_list] No handlers registered!")
|
|
113
136
|
while wait_list:
|
|
114
137
|
if self.raise_error:
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
immediate_handler = wait_list.pop()
|
|
139
|
+
immediate_handler(event)
|
|
117
140
|
else:
|
|
141
|
+
queued_handler: Callable[[TEvento], None] | None = None
|
|
118
142
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
queued_handler = wait_list.pop()
|
|
144
|
+
queued_handler(event)
|
|
121
145
|
except Exception as e:
|
|
122
|
-
|
|
146
|
+
if queued_handler is None:
|
|
147
|
+
raise
|
|
148
|
+
raise HandlerError(event, queued_handler, e) from e
|
|
123
149
|
|
|
124
150
|
@overload
|
|
125
151
|
def wait_for_publish(
|
|
@@ -136,9 +162,10 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
136
162
|
) -> Callable[[Callable[[TEvent], None]], None] | None:
|
|
137
163
|
self.verify()
|
|
138
164
|
if handler:
|
|
139
|
-
|
|
165
|
+
self._wait_for(event_type, handler)
|
|
166
|
+
return None
|
|
140
167
|
|
|
141
|
-
def decorator(func: Callable[[TEvent], None]):
|
|
168
|
+
def decorator(func: Callable[[TEvent], None]) -> None:
|
|
142
169
|
self._wait_for(event_type, func)
|
|
143
170
|
|
|
144
171
|
return decorator
|
|
@@ -169,4 +196,4 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
169
196
|
logger.error(e)
|
|
170
197
|
|
|
171
198
|
def process_events(self, *events: CloudMessage[TEvent]) -> None:
|
|
172
|
-
|
|
199
|
+
self._process_messages(*events)
|
|
@@ -16,23 +16,23 @@ class BaseBusInfrastructure(IBusInfrastructure[TManager], InfrastructureGroup):
|
|
|
16
16
|
event_bus: IEventBus[TManager],
|
|
17
17
|
query_bus: IQueryBus[TManager],
|
|
18
18
|
*args: IBaseInfrastructure,
|
|
19
|
-
):
|
|
19
|
+
) -> None:
|
|
20
20
|
self._command_bus = command_bus
|
|
21
21
|
self._event_bus = event_bus
|
|
22
22
|
self._query_bus = query_bus
|
|
23
23
|
super().__init__(self._command_bus, self._event_bus, self._query_bus, *args)
|
|
24
24
|
|
|
25
25
|
@property
|
|
26
|
-
def command_bus(self):
|
|
26
|
+
def command_bus(self) -> ICommandBus[TManager]:
|
|
27
27
|
self.verify()
|
|
28
28
|
return self._command_bus
|
|
29
29
|
|
|
30
30
|
@property
|
|
31
|
-
def event_bus(self):
|
|
31
|
+
def event_bus(self) -> IEventBus[TManager]:
|
|
32
32
|
self.verify()
|
|
33
33
|
return self._event_bus
|
|
34
34
|
|
|
35
35
|
@property
|
|
36
|
-
def query_bus(self):
|
|
36
|
+
def query_bus(self) -> IQueryBus[TManager]:
|
|
37
37
|
self.verify()
|
|
38
38
|
return self._query_bus
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, Callable
|
|
3
3
|
|
|
4
4
|
from hexagonal.application import Infrastructure
|
|
5
5
|
from hexagonal.domain import CloudMessage
|
|
@@ -9,6 +9,7 @@ from hexagonal.ports.drivens import (
|
|
|
9
9
|
IOutboxRepository,
|
|
10
10
|
TManager,
|
|
11
11
|
)
|
|
12
|
+
from hexagonal.ports.drivens.scoped import IWriteScopeRunner, TWriteScope
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class MessageBus(IBaseMessageBus[TManager], Infrastructure):
|
|
@@ -16,9 +17,13 @@ class MessageBus(IBaseMessageBus[TManager], Infrastructure):
|
|
|
16
17
|
self,
|
|
17
18
|
inbox_repository: IInboxRepository[TManager],
|
|
18
19
|
outbox_repository: IOutboxRepository[TManager],
|
|
19
|
-
):
|
|
20
|
+
) -> None:
|
|
20
21
|
self._inbox_repository = inbox_repository
|
|
21
22
|
self._outbox_repository = outbox_repository
|
|
23
|
+
self._write_scope_runner: IWriteScopeRunner[Any] | None = None
|
|
24
|
+
self._outbox_repository_getter: (
|
|
25
|
+
Callable[[Any], IOutboxRepository[TManager]] | None
|
|
26
|
+
) = None
|
|
22
27
|
super().__init__()
|
|
23
28
|
|
|
24
29
|
@property
|
|
@@ -29,19 +34,59 @@ class MessageBus(IBaseMessageBus[TManager], Infrastructure):
|
|
|
29
34
|
def outbox_repository(self) -> IOutboxRepository[TManager]:
|
|
30
35
|
return self._outbox_repository
|
|
31
36
|
|
|
37
|
+
def configure_scope_runtime(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
write_scope_runner: IWriteScopeRunner[TWriteScope] | None = None,
|
|
41
|
+
outbox_repository_getter: Callable[[TWriteScope], IOutboxRepository[TManager]]
|
|
42
|
+
| None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._write_scope_runner = write_scope_runner
|
|
45
|
+
self._outbox_repository_getter = outbox_repository_getter
|
|
46
|
+
|
|
47
|
+
def save_to_outbox(self, *messages: CloudMessage[Any]) -> None:
|
|
48
|
+
self.verify()
|
|
49
|
+
self._run_with_outbox_repository(
|
|
50
|
+
lambda outbox_repository: outbox_repository.save(*messages)
|
|
51
|
+
)
|
|
52
|
+
|
|
32
53
|
# publish
|
|
33
|
-
def publish_from_outbox(self, limit: int | None = None):
|
|
54
|
+
def publish_from_outbox(self, limit: int | None = None) -> None:
|
|
34
55
|
self.verify()
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
self._run_with_outbox_repository(
|
|
57
|
+
lambda outbox_repository: self._publish_messages(
|
|
58
|
+
outbox_repository,
|
|
59
|
+
*outbox_repository.fetch_pending(limit=limit),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _run_with_outbox_repository(
|
|
64
|
+
self,
|
|
65
|
+
work: Callable[[IOutboxRepository[TManager]], Any],
|
|
66
|
+
) -> Any:
|
|
67
|
+
if self._write_scope_runner is None or self._outbox_repository_getter is None:
|
|
68
|
+
return work(self.outbox_repository)
|
|
69
|
+
|
|
70
|
+
outbox_repository_getter = self._outbox_repository_getter
|
|
71
|
+
scope = self._write_scope_runner.current_write_scope
|
|
72
|
+
if scope is not None:
|
|
73
|
+
return work(outbox_repository_getter(scope))
|
|
74
|
+
|
|
75
|
+
return self._write_scope_runner.run_in_write_scope(
|
|
76
|
+
lambda write_scope: work(outbox_repository_getter(write_scope))
|
|
77
|
+
)
|
|
37
78
|
|
|
38
|
-
def _publish_messages(
|
|
79
|
+
def _publish_messages(
|
|
80
|
+
self,
|
|
81
|
+
outbox_repository: IOutboxRepository[TManager],
|
|
82
|
+
*messages: CloudMessage[Any],
|
|
83
|
+
) -> None:
|
|
39
84
|
for message in messages:
|
|
40
85
|
try:
|
|
41
86
|
self._publish_message(message)
|
|
42
|
-
|
|
87
|
+
outbox_repository.mark_as_published(message.message_id)
|
|
43
88
|
except Exception as e:
|
|
44
|
-
|
|
89
|
+
outbox_repository.mark_as_failed(message.message_id, error=str(e))
|
|
45
90
|
|
|
46
91
|
@abstractmethod
|
|
47
92
|
def _publish_message(self, message: CloudMessage[Any]) -> None: ...
|
{python_hexagonal-0.2.0 → python_hexagonal-0.3.1}/src/hexagonal/adapters/drivens/buses/base/query.py
RENAMED
|
@@ -15,6 +15,7 @@ from hexagonal.domain import (
|
|
|
15
15
|
TView,
|
|
16
16
|
)
|
|
17
17
|
from hexagonal.ports.drivens import IQueryBus, IQueryHandler, TManager
|
|
18
|
+
from hexagonal.ports.drivens.scoped import IReadScopeRunner
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
@@ -22,8 +23,14 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
22
23
|
|
|
23
24
|
def initialize(self, env: Mapping[str, str]) -> None:
|
|
24
25
|
self.handlers = {}
|
|
26
|
+
self._read_scope_runner: IReadScopeRunner[Any] | None = None
|
|
25
27
|
super().initialize(env)
|
|
26
28
|
|
|
29
|
+
def configure_read_scope_runtime(
|
|
30
|
+
self, read_scope_runner: IReadScopeRunner[Any] | None = None
|
|
31
|
+
) -> None:
|
|
32
|
+
self._read_scope_runner = read_scope_runner
|
|
33
|
+
|
|
27
34
|
def _get_name(self, query_type: Type[QueryBase[TView]]) -> str:
|
|
28
35
|
return get_topic(query_type)
|
|
29
36
|
|
|
@@ -37,14 +44,14 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
37
44
|
self,
|
|
38
45
|
query_type: Type[TQuery],
|
|
39
46
|
handler: IQueryHandler[TManager, TQuery, TView],
|
|
40
|
-
):
|
|
47
|
+
) -> None:
|
|
41
48
|
self.verify()
|
|
42
49
|
name = self._get_name(query_type)
|
|
43
50
|
if name in self.handlers:
|
|
44
51
|
raise HandlerAlreadyRegistered(f"Query: {name}")
|
|
45
52
|
self.handlers[name] = handler
|
|
46
53
|
|
|
47
|
-
def unregister_handler(self, query_type: Type[TQuery]):
|
|
54
|
+
def unregister_handler(self, query_type: Type[TQuery]) -> None:
|
|
48
55
|
self.verify()
|
|
49
56
|
name = self._get_name(query_type)
|
|
50
57
|
if name in self.handlers:
|
|
@@ -16,7 +16,7 @@ class InMemoryCommandBus(BaseCommandBus[TManager]):
|
|
|
16
16
|
def _publish_message(self, message: CloudMessage[TCommand]) -> None:
|
|
17
17
|
return self._process_messages(message)
|
|
18
18
|
|
|
19
|
-
def consume(self, limit: int | None = None):
|
|
19
|
+
def consume(self, limit: int | None = None) -> None:
|
|
20
20
|
return # No-op for non-queued bus
|
|
21
21
|
|
|
22
22
|
|
|
@@ -66,5 +66,5 @@ class InMemoryQueueCommandBus(BaseCommandBus[TManager]):
|
|
|
66
66
|
finally:
|
|
67
67
|
self.queue.task_done()
|
|
68
68
|
|
|
69
|
-
def consume(self, limit: int | None = None):
|
|
69
|
+
def consume(self, limit: int | None = None) -> None:
|
|
70
70
|
self._worker.start()
|
|
@@ -15,6 +15,9 @@ logger = logging.getLogger(__name__)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class InMemoryEventBus(BaseEventBus[TManager]):
|
|
18
|
+
def shutdown(self) -> None:
|
|
19
|
+
return
|
|
20
|
+
|
|
18
21
|
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
19
22
|
super()._publish_message(message)
|
|
20
23
|
self._process_messages(message)
|
|
@@ -22,7 +25,7 @@ class InMemoryEventBus(BaseEventBus[TManager]):
|
|
|
22
25
|
def publish(self, *events: CloudMessage[TEvent]) -> None:
|
|
23
26
|
return self._publish_messages(*events)
|
|
24
27
|
|
|
25
|
-
def consume(self, limit: int | None = None):
|
|
28
|
+
def consume(self, limit: int | None = None) -> None:
|
|
26
29
|
pass # No-op for non-queued bus
|
|
27
30
|
|
|
28
31
|
|
|
@@ -68,5 +71,5 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
68
71
|
finally:
|
|
69
72
|
self.queue.task_done()
|
|
70
73
|
|
|
71
|
-
def consume(self, limit: int | None = None):
|
|
74
|
+
def consume(self, limit: int | None = None) -> None:
|
|
72
75
|
self._worker.start()
|
|
@@ -91,8 +91,8 @@ class MessageMapper(Mapper[UUID]):
|
|
|
91
91
|
getattr(cls, f"upcast_v{from_version}_v{from_version + 1}")(event_state)
|
|
92
92
|
from_version += 1
|
|
93
93
|
if issubclass(cls, AggregateSnapshot):
|
|
94
|
-
return cls.model_validate(event_state)
|
|
95
|
-
domain_event = object.__new__(cls)
|
|
94
|
+
return cast(DomainEventProtocol[UUID], cls.model_validate(event_state))
|
|
95
|
+
domain_event = cast(DomainEventProtocol[UUID], object.__new__(cls))
|
|
96
96
|
domain_event.__dict__.update(event_state)
|
|
97
97
|
return domain_event
|
|
98
98
|
|
|
@@ -38,22 +38,19 @@ class BaseRepositoryAdapter(IBaseRepository[TManager], Infrastructure):
|
|
|
38
38
|
name = self.NAME or self.__class__.__name__.upper()
|
|
39
39
|
env2.update(env)
|
|
40
40
|
self.env = Environment(name, env2)
|
|
41
|
-
self.
|
|
42
|
-
self._manager_at_uow: TManager | None = None
|
|
41
|
+
self._scope_connection_manager: TManager | None = None
|
|
43
42
|
super().initialize(self.env)
|
|
44
43
|
|
|
45
44
|
def attach_to_unit_of_work(self, uow: IUnitOfWork[TManager]) -> None:
|
|
46
|
-
self.
|
|
47
|
-
self._manager_at_uow = uow.connection_manager
|
|
45
|
+
self._scope_connection_manager = uow.connection_manager
|
|
48
46
|
|
|
49
47
|
def detach_from_unit_of_work(self) -> None:
|
|
50
|
-
self.
|
|
51
|
-
self._manager_at_uow = None
|
|
48
|
+
self._scope_connection_manager = None
|
|
52
49
|
|
|
53
50
|
@property
|
|
54
51
|
def connection_manager(self) -> TManager:
|
|
55
|
-
if self.
|
|
56
|
-
return self.
|
|
52
|
+
if self._scope_connection_manager is not None:
|
|
53
|
+
return self._scope_connection_manager
|
|
57
54
|
return self._connection_manager
|
|
58
55
|
|
|
59
56
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from types import TracebackType
|
|
2
|
+
from typing import Any, Mapping, cast
|
|
2
3
|
|
|
3
4
|
from eventsourcing.utils import get_topic
|
|
4
5
|
|
|
@@ -11,7 +12,7 @@ class BaseUnitOfWork(IUnitOfWork[TManager], InfrastructureGroup):
|
|
|
11
12
|
self,
|
|
12
13
|
*repositories: IBaseRepository[TManager],
|
|
13
14
|
connection_manager: TManager,
|
|
14
|
-
):
|
|
15
|
+
) -> None:
|
|
15
16
|
self._repositories = {get_topic(repo.__class__): repo for repo in repositories}
|
|
16
17
|
self._initialized = False
|
|
17
18
|
self._manager = connection_manager
|
|
@@ -20,20 +21,26 @@ class BaseUnitOfWork(IUnitOfWork[TManager], InfrastructureGroup):
|
|
|
20
21
|
def initialize(self, env: Mapping[str, str]) -> None:
|
|
21
22
|
self._initialized = True
|
|
22
23
|
self._active = False
|
|
24
|
+
self._attached_repositories: list[IBaseRepository[TManager]] = []
|
|
23
25
|
return super().initialize(env)
|
|
24
26
|
|
|
25
27
|
@property
|
|
26
28
|
def initialized(self) -> bool:
|
|
27
29
|
return self._initialized and super().initialized
|
|
28
30
|
|
|
29
|
-
def attach_repo(self, repo: IBaseRepository[TManager]):
|
|
31
|
+
def attach_repo(self, repo: IBaseRepository[TManager]) -> None:
|
|
30
32
|
topic = get_topic(repo.__class__)
|
|
31
33
|
if topic in self._repositories:
|
|
32
34
|
return
|
|
33
35
|
|
|
34
36
|
self._repositories[topic] = repo
|
|
35
|
-
if
|
|
37
|
+
if (
|
|
38
|
+
self.initialized
|
|
39
|
+
and self._active
|
|
40
|
+
and repo.connection_manager is not self._manager
|
|
41
|
+
):
|
|
36
42
|
repo.attach_to_unit_of_work(self)
|
|
43
|
+
self._attached_repositories.append(repo)
|
|
37
44
|
|
|
38
45
|
def detach_repo(self, repo: IBaseRepository[TManager]) -> None:
|
|
39
46
|
topic = get_topic(repo.__class__)
|
|
@@ -45,21 +52,30 @@ class BaseUnitOfWork(IUnitOfWork[TManager], InfrastructureGroup):
|
|
|
45
52
|
del self._repositories[topic]
|
|
46
53
|
|
|
47
54
|
@property
|
|
48
|
-
def connection_manager(self):
|
|
55
|
+
def connection_manager(self) -> TManager:
|
|
49
56
|
return self._manager
|
|
50
57
|
|
|
51
|
-
def __enter__(self):
|
|
58
|
+
def __enter__(self) -> "BaseUnitOfWork[TManager]":
|
|
52
59
|
self.verify()
|
|
53
60
|
if self._active:
|
|
54
61
|
return self
|
|
55
62
|
# get connection from manager, the connection is entered yet
|
|
56
63
|
self._ctx = self._manager.start_connection()
|
|
64
|
+
self._attached_repositories = []
|
|
57
65
|
for repo in self._repositories.values():
|
|
66
|
+
if repo.connection_manager is self._manager:
|
|
67
|
+
continue
|
|
58
68
|
repo.attach_to_unit_of_work(self)
|
|
69
|
+
self._attached_repositories.append(repo)
|
|
59
70
|
self._active = True
|
|
60
71
|
return self
|
|
61
72
|
|
|
62
|
-
def __exit__(
|
|
73
|
+
def __exit__(
|
|
74
|
+
self,
|
|
75
|
+
exc_type: type[BaseException] | None,
|
|
76
|
+
exc_val: BaseException | None,
|
|
77
|
+
exc_tb: TracebackType | None,
|
|
78
|
+
) -> None:
|
|
63
79
|
self._active = False
|
|
64
80
|
try:
|
|
65
81
|
if exc_type is None:
|
|
@@ -70,6 +86,6 @@ class BaseUnitOfWork(IUnitOfWork[TManager], InfrastructureGroup):
|
|
|
70
86
|
# Log or handle commit/rollback errors
|
|
71
87
|
raise RuntimeError(f"Failed to finalize transaction: {e}") from e
|
|
72
88
|
finally:
|
|
73
|
-
for repo in self.
|
|
89
|
+
for repo in self._attached_repositories:
|
|
74
90
|
repo.detach_from_unit_of_work()
|
|
75
|
-
self._ctx.__exit__(exc_type, exc_val, exc_tb)
|
|
91
|
+
cast(Any, self._ctx).__exit__(exc_type, exc_val, exc_tb)
|
|
@@ -6,7 +6,7 @@ backends (PostgreSQL, MySQL, SQLite) through SQLAlchemy's abstraction layer.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .datastore import SQLAlchemyConnectionContextManager, SQLAlchemyDatastore
|
|
9
|
-
from .infrastructure import SQLAlchemyInfrastructure
|
|
9
|
+
from .infrastructure import SQLAlchemyInfrastructure, SQLAlchemyScopeRunner
|
|
10
10
|
from .outbox import (
|
|
11
11
|
SQLAlchemyInboxRepository,
|
|
12
12
|
SQLAlchemyOutboxRepository,
|
|
@@ -27,6 +27,7 @@ __all__ = [
|
|
|
27
27
|
"SQLAlchemyOutboxRepository",
|
|
28
28
|
"SQLAlchemyInboxRepository",
|
|
29
29
|
"SQLAlchemyInfrastructure",
|
|
30
|
+
"SQLAlchemyScopeRunner",
|
|
30
31
|
"SQLAlchemyPairInboxOutbox",
|
|
31
32
|
"SQLAlchemyEntityRepositoryAdapter",
|
|
32
33
|
"SQLAlchemySearchRepositoryAdapter",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""SQLAlchemy datastore and connection context manager."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator, Mapping
|
|
4
|
-
from contextlib import contextmanager
|
|
4
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
5
5
|
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
from eventsourcing.utils import strtobool
|
|
@@ -38,7 +38,7 @@ class SQLAlchemyDatastore:
|
|
|
38
38
|
pool_recycle: int = 3600,
|
|
39
39
|
pool_pre_ping: bool = True,
|
|
40
40
|
max_overflow: int = 10,
|
|
41
|
-
):
|
|
41
|
+
) -> None:
|
|
42
42
|
"""Initialize SQLAlchemy datastore.
|
|
43
43
|
|
|
44
44
|
Args:
|
|
@@ -142,7 +142,7 @@ class SQLAlchemyConnectionContextManager(IConnectionManager):
|
|
|
142
142
|
Maintains a current connection reference for transaction coordination.
|
|
143
143
|
"""
|
|
144
144
|
|
|
145
|
-
def __init__(self, datastore: Optional[SQLAlchemyDatastore] = None):
|
|
145
|
+
def __init__(self, datastore: Optional[SQLAlchemyDatastore] = None) -> None:
|
|
146
146
|
"""Initialize connection context manager.
|
|
147
147
|
|
|
148
148
|
Args:
|
|
@@ -151,7 +151,7 @@ class SQLAlchemyConnectionContextManager(IConnectionManager):
|
|
|
151
151
|
"""
|
|
152
152
|
self._datastore = datastore
|
|
153
153
|
self._current_connection: Optional[Connection] = None
|
|
154
|
-
self._connection_ctx: Optional[
|
|
154
|
+
self._connection_ctx: Optional[AbstractContextManager[Connection]] = None
|
|
155
155
|
self._initialized = datastore is not None
|
|
156
156
|
|
|
157
157
|
@property
|
|
@@ -213,7 +213,10 @@ class SQLAlchemyConnectionContextManager(IConnectionManager):
|
|
|
213
213
|
)
|
|
214
214
|
self._initialized = True
|
|
215
215
|
|
|
216
|
-
def
|
|
216
|
+
def create_scoped_manager(self) -> "SQLAlchemyConnectionContextManager":
|
|
217
|
+
return SQLAlchemyConnectionContextManager(self.datastore)
|
|
218
|
+
|
|
219
|
+
def start_connection(self) -> Any:
|
|
217
220
|
"""Start a new connection context.
|
|
218
221
|
|
|
219
222
|
Returns:
|
|
@@ -280,7 +283,7 @@ class SQLAlchemyConnectionContextManager(IConnectionManager):
|
|
|
280
283
|
self._current_connection = None
|
|
281
284
|
conn = self.current_connection
|
|
282
285
|
yield conn
|
|
283
|
-
if commit:
|
|
286
|
+
if commit and conn.in_transaction():
|
|
284
287
|
conn.commit()
|
|
285
288
|
except Exception:
|
|
286
289
|
# Reset connection on error
|