python-hexagonal 0.1.1__tar.gz → 0.2.0__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/PKG-INFO +90 -0
- python_hexagonal-0.2.0/README.md +76 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/pyproject.toml +7 -1
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/event_bus.py +14 -2
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/query.py +15 -5
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/event_bus.py +5 -2
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/mappers.py +10 -1
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/__init__.py +4 -2
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/repository.py +35 -5
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/__init__.py +33 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/datastore.py +288 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/env_vars.py +22 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/infrastructure.py +41 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/models.py +160 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/outbox.py +456 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/repository.py +442 -0
- python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/unit_of_work.py +60 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/__init__.py +24 -3
- python_hexagonal-0.2.0/src/hexagonal/application/api.py +210 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/handlers.py +40 -11
- python_hexagonal-0.2.0/src/hexagonal/application/query.py +85 -0
- python_hexagonal-0.2.0/src/hexagonal/application/topics.py +45 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/__init__.py +17 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/aggregate.py +32 -10
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/base.py +10 -2
- python_hexagonal-0.2.0/src/hexagonal/domain/queries.py +17 -0
- python_hexagonal-0.2.0/src/hexagonal/entrypoints/sqlalchemy.py +146 -0
- python_hexagonal-0.2.0/src/hexagonal/integrations/__init__.py +1 -0
- python_hexagonal-0.2.0/src/hexagonal/integrations/sqlalchemy.py +49 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/__init__.py +4 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/buses.py +14 -1
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/repository.py +27 -5
- python_hexagonal-0.1.1/PKG-INFO +0 -15
- python_hexagonal-0.1.1/README.md +0 -2
- python_hexagonal-0.1.1/src/hexagonal/application/api.py +0 -61
- python_hexagonal-0.1.1/src/hexagonal/application/query.py +0 -71
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/command_bus.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/infrastructure.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/message_bus.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/utils.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/command_bus.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/infra.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/unit_of_work.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/datastore.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/env_vars.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/outbox.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/repository.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivers/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivers/app.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/app.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/bus_app.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/infrastructure.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/exceptions.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/app.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/base.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/bus.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/sqlite.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/application.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/infrastructure.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivers/__init__.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivers/app.py +0 -0
- {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/py.typed +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-hexagonal
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Framework to build hexagonal architecture applications in Python.
|
|
5
|
+
Author: jose-matos-9281
|
|
6
|
+
Author-email: jose-matos-9281 <58991817+jose-matos-9281@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: eventsourcing>=9.4.6
|
|
8
|
+
Requires-Dist: orjson>=3.11.5
|
|
9
|
+
Requires-Dist: pydantic>=2.12.5
|
|
10
|
+
Requires-Dist: sqlalchemy>=2.0.45
|
|
11
|
+
Requires-Dist: uuid6>=2025.0.1
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# python-hexagonal
|
|
16
|
+
|
|
17
|
+
`python-hexagonal` is a library for building hexagonal applications in Python.
|
|
18
|
+
The real usage story lives in `src/example` and the proof lives in
|
|
19
|
+
`src/tests/use_cases`, so this documentation starts there instead of pretending a
|
|
20
|
+
two-line snippet is enough.
|
|
21
|
+
|
|
22
|
+
## Start here
|
|
23
|
+
|
|
24
|
+
- Install the package: `pip install python-hexagonal`
|
|
25
|
+
- Follow the example-backed guide: [`docs/getting-started/first-app.md`](docs/getting-started/first-app.md)
|
|
26
|
+
- Understand the architecture roles:
|
|
27
|
+
[`docs/explanation/architecture-from-example.md`](docs/explanation/architecture-from-example.md)
|
|
28
|
+
- Bootstrap the adapter-specific SQLAlchemy path:
|
|
29
|
+
[`docs/how-to/bootstrap-sqlalchemy-app.md`](docs/how-to/bootstrap-sqlalchemy-app.md)
|
|
30
|
+
- Learn the canonical testing workflow:
|
|
31
|
+
[`docs/how-to/test-use-cases.md`](docs/how-to/test-use-cases.md)
|
|
32
|
+
- Check the guardrails before copying imports:
|
|
33
|
+
[`docs/reference/supported-surface.md`](docs/reference/supported-surface.md)
|
|
34
|
+
- Review the evidence map if you want to verify a claim against code or tests:
|
|
35
|
+
[`docs/reference/evidence-map.yaml`](docs/reference/evidence-map.yaml)
|
|
36
|
+
|
|
37
|
+
## What you learn from the first path
|
|
38
|
+
|
|
39
|
+
After the first-app guide you should understand:
|
|
40
|
+
|
|
41
|
+
- where the domain model lives and why it stays isolated
|
|
42
|
+
- how application APIs wrap command, query, and event buses
|
|
43
|
+
- how ports describe the infrastructure your app needs
|
|
44
|
+
- how entrypoints assemble an app from environment and infrastructure
|
|
45
|
+
- how the use-case tests prove the workflow end to end
|
|
46
|
+
|
|
47
|
+
## The blueprint we actually trust
|
|
48
|
+
|
|
49
|
+
These files are the baseline for the supported adoption story:
|
|
50
|
+
|
|
51
|
+
- `src/example/contacto/domain/contacto.py` - aggregate behavior, value-object
|
|
52
|
+
strategy, and query entry points
|
|
53
|
+
- `src/example/contacto/application/app.py` - application composition through
|
|
54
|
+
`BusAppGroup`
|
|
55
|
+
- `src/example/contacto/ports/drivens.py` - infrastructure contracts and
|
|
56
|
+
repository boundaries
|
|
57
|
+
- `src/example/app/application/api.py` - top-level API wrapper consumers call
|
|
58
|
+
- `src/example/app/entrypoints/main.py` - environment-driven bootstrap via
|
|
59
|
+
`EntrypointGroup`
|
|
60
|
+
- `src/example/app/entrypoints/db/sqlalchemy.py` - SQLAlchemy-specific
|
|
61
|
+
infrastructure assembly
|
|
62
|
+
- `src/tests/use_cases/base.py` - canonical test bootstrap with migrations,
|
|
63
|
+
entrypoint creation, and topic registration
|
|
64
|
+
|
|
65
|
+
## Supported path vs internals
|
|
66
|
+
|
|
67
|
+
The documented path is intentionally narrow.
|
|
68
|
+
|
|
69
|
+
- Start from `hexagonal.domain`, `hexagonal.application`,
|
|
70
|
+
`hexagonal.ports.drivens`, `hexagonal.ports.drivers`, and
|
|
71
|
+
`hexagonal.entrypoints`
|
|
72
|
+
- Reach for `hexagonal.integrations.sqlalchemy` when you need the reusable
|
|
73
|
+
SQLAlchemy repository and unit-of-work utilities
|
|
74
|
+
- Treat `hexagonal.entrypoints.sqlalchemy` as adapter-specific convenience, not
|
|
75
|
+
the whole framework story
|
|
76
|
+
- Do not treat `hexagonal.__init__` as the public integration surface; right now
|
|
77
|
+
it only exposes `hello()`
|
|
78
|
+
- Do not cargo-cult example names like `exampleAPI`, `exampleEntrypoint`, or
|
|
79
|
+
`Exampletate`; copy the roles, not the labels
|
|
80
|
+
- Do not build new code against `hexagonal.adapters.*`; that path is kept for
|
|
81
|
+
compatibility, while the supported SQLAlchemy extension surface now lives under
|
|
82
|
+
`hexagonal.integrations.sqlalchemy`
|
|
83
|
+
|
|
84
|
+
## Companion skill
|
|
85
|
+
|
|
86
|
+
This repo also ships an installable companion skill at
|
|
87
|
+
`skills/python-hexagonal-usage/SKILL.md`.
|
|
88
|
+
It is there to inspect a user repository, map it to the same architecture, and
|
|
89
|
+
point people back to the written docs. It does NOT replace the docs and it does
|
|
90
|
+
NOT get to invent new supported APIs.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# python-hexagonal
|
|
2
|
+
|
|
3
|
+
`python-hexagonal` is a library for building hexagonal applications in Python.
|
|
4
|
+
The real usage story lives in `src/example` and the proof lives in
|
|
5
|
+
`src/tests/use_cases`, so this documentation starts there instead of pretending a
|
|
6
|
+
two-line snippet is enough.
|
|
7
|
+
|
|
8
|
+
## Start here
|
|
9
|
+
|
|
10
|
+
- Install the package: `pip install python-hexagonal`
|
|
11
|
+
- Follow the example-backed guide: [`docs/getting-started/first-app.md`](docs/getting-started/first-app.md)
|
|
12
|
+
- Understand the architecture roles:
|
|
13
|
+
[`docs/explanation/architecture-from-example.md`](docs/explanation/architecture-from-example.md)
|
|
14
|
+
- Bootstrap the adapter-specific SQLAlchemy path:
|
|
15
|
+
[`docs/how-to/bootstrap-sqlalchemy-app.md`](docs/how-to/bootstrap-sqlalchemy-app.md)
|
|
16
|
+
- Learn the canonical testing workflow:
|
|
17
|
+
[`docs/how-to/test-use-cases.md`](docs/how-to/test-use-cases.md)
|
|
18
|
+
- Check the guardrails before copying imports:
|
|
19
|
+
[`docs/reference/supported-surface.md`](docs/reference/supported-surface.md)
|
|
20
|
+
- Review the evidence map if you want to verify a claim against code or tests:
|
|
21
|
+
[`docs/reference/evidence-map.yaml`](docs/reference/evidence-map.yaml)
|
|
22
|
+
|
|
23
|
+
## What you learn from the first path
|
|
24
|
+
|
|
25
|
+
After the first-app guide you should understand:
|
|
26
|
+
|
|
27
|
+
- where the domain model lives and why it stays isolated
|
|
28
|
+
- how application APIs wrap command, query, and event buses
|
|
29
|
+
- how ports describe the infrastructure your app needs
|
|
30
|
+
- how entrypoints assemble an app from environment and infrastructure
|
|
31
|
+
- how the use-case tests prove the workflow end to end
|
|
32
|
+
|
|
33
|
+
## The blueprint we actually trust
|
|
34
|
+
|
|
35
|
+
These files are the baseline for the supported adoption story:
|
|
36
|
+
|
|
37
|
+
- `src/example/contacto/domain/contacto.py` - aggregate behavior, value-object
|
|
38
|
+
strategy, and query entry points
|
|
39
|
+
- `src/example/contacto/application/app.py` - application composition through
|
|
40
|
+
`BusAppGroup`
|
|
41
|
+
- `src/example/contacto/ports/drivens.py` - infrastructure contracts and
|
|
42
|
+
repository boundaries
|
|
43
|
+
- `src/example/app/application/api.py` - top-level API wrapper consumers call
|
|
44
|
+
- `src/example/app/entrypoints/main.py` - environment-driven bootstrap via
|
|
45
|
+
`EntrypointGroup`
|
|
46
|
+
- `src/example/app/entrypoints/db/sqlalchemy.py` - SQLAlchemy-specific
|
|
47
|
+
infrastructure assembly
|
|
48
|
+
- `src/tests/use_cases/base.py` - canonical test bootstrap with migrations,
|
|
49
|
+
entrypoint creation, and topic registration
|
|
50
|
+
|
|
51
|
+
## Supported path vs internals
|
|
52
|
+
|
|
53
|
+
The documented path is intentionally narrow.
|
|
54
|
+
|
|
55
|
+
- Start from `hexagonal.domain`, `hexagonal.application`,
|
|
56
|
+
`hexagonal.ports.drivens`, `hexagonal.ports.drivers`, and
|
|
57
|
+
`hexagonal.entrypoints`
|
|
58
|
+
- Reach for `hexagonal.integrations.sqlalchemy` when you need the reusable
|
|
59
|
+
SQLAlchemy repository and unit-of-work utilities
|
|
60
|
+
- Treat `hexagonal.entrypoints.sqlalchemy` as adapter-specific convenience, not
|
|
61
|
+
the whole framework story
|
|
62
|
+
- Do not treat `hexagonal.__init__` as the public integration surface; right now
|
|
63
|
+
it only exposes `hello()`
|
|
64
|
+
- Do not cargo-cult example names like `exampleAPI`, `exampleEntrypoint`, or
|
|
65
|
+
`Exampletate`; copy the roles, not the labels
|
|
66
|
+
- Do not build new code against `hexagonal.adapters.*`; that path is kept for
|
|
67
|
+
compatibility, while the supported SQLAlchemy extension surface now lives under
|
|
68
|
+
`hexagonal.integrations.sqlalchemy`
|
|
69
|
+
|
|
70
|
+
## Companion skill
|
|
71
|
+
|
|
72
|
+
This repo also ships an installable companion skill at
|
|
73
|
+
`skills/python-hexagonal-usage/SKILL.md`.
|
|
74
|
+
It is there to inspect a user repository, map it to the same architecture, and
|
|
75
|
+
point people back to the written docs. It does NOT replace the docs and it does
|
|
76
|
+
NOT get to invent new supported APIs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-hexagonal"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Framework to build hexagonal architecture applications in Python."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
|
11
11
|
"eventsourcing>=9.4.6",
|
|
12
12
|
"orjson>=3.11.5",
|
|
13
13
|
"pydantic>=2.12.5",
|
|
14
|
+
"sqlalchemy>=2.0.45",
|
|
14
15
|
"uuid6>=2025.0.1",
|
|
15
16
|
]
|
|
16
17
|
|
|
@@ -27,5 +28,10 @@ fixable = ["I", "E", "F", "B"]
|
|
|
27
28
|
|
|
28
29
|
[dependency-groups]
|
|
29
30
|
dev = [
|
|
31
|
+
"alembic>=1.18.4",
|
|
30
32
|
"pytest>=9.0.2",
|
|
33
|
+
"python-dotenv>=1.2.2",
|
|
34
|
+
]
|
|
35
|
+
sqlalchemy = [
|
|
36
|
+
"sqlalchemy>=2.0.45",
|
|
31
37
|
]
|
|
@@ -28,9 +28,9 @@ class HandlerError(Exception):
|
|
|
28
28
|
super().__init__(f"""
|
|
29
29
|
Error al Manejar Evento {evento.__class__.__name__}
|
|
30
30
|
handler: {
|
|
31
|
-
handler.__class__.__name__
|
|
31
|
+
handler.__class__.__name__ # pyright: ignore[reportUnknownMemberType]
|
|
32
32
|
if isinstance(handler, IMessageHandler)
|
|
33
|
-
else handler.__name__
|
|
33
|
+
else handler.__name__
|
|
34
34
|
}
|
|
35
35
|
evento: {evento.type}
|
|
36
36
|
datos: {evento.model_dump_json(indent=2)}
|
|
@@ -93,11 +93,23 @@ class BaseEventBus(IEventBus[TManager], MessageBus[TManager]):
|
|
|
93
93
|
if name not in self.wait_list:
|
|
94
94
|
self.wait_list[name] = []
|
|
95
95
|
self.wait_list[name].append(handler)
|
|
96
|
+
logger.debug(
|
|
97
|
+
" [DEBUG _wait_for] Added handler to wait_list[%s], now has %s handlers",
|
|
98
|
+
name,
|
|
99
|
+
len(self.wait_list[name]),
|
|
100
|
+
)
|
|
96
101
|
|
|
97
102
|
def _handle_wait_list(self, event: TEvento):
|
|
98
103
|
event_type = type(event)
|
|
99
104
|
key = self._get_key(event_type)
|
|
105
|
+
logger.debug(" [DEBUG _handle_wait_list] Publishing event type=%s", key)
|
|
100
106
|
wait_list = self.wait_list.get(key)
|
|
107
|
+
if wait_list:
|
|
108
|
+
logger.debug(
|
|
109
|
+
" [DEBUG _handle_wait_list] Found %s handlers", len(wait_list)
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
logger.debug(" [DEBUG _handle_wait_list] No handlers registered!")
|
|
101
113
|
while wait_list:
|
|
102
114
|
if self.raise_error:
|
|
103
115
|
handler = wait_list.pop()
|
{python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/query.py
RENAMED
|
@@ -7,6 +7,8 @@ from hexagonal.domain import (
|
|
|
7
7
|
HandlerAlreadyRegistered,
|
|
8
8
|
HandlerNotRegistered,
|
|
9
9
|
Query,
|
|
10
|
+
QueryBase,
|
|
11
|
+
QueryOne,
|
|
10
12
|
QueryResult,
|
|
11
13
|
QueryResults,
|
|
12
14
|
TQuery,
|
|
@@ -22,12 +24,12 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
22
24
|
self.handlers = {}
|
|
23
25
|
super().initialize(env)
|
|
24
26
|
|
|
25
|
-
def _get_name(self, query_type: Type[
|
|
27
|
+
def _get_name(self, query_type: Type[QueryBase[TView]]) -> str:
|
|
26
28
|
return get_topic(query_type)
|
|
27
29
|
|
|
28
30
|
def _get_handler(
|
|
29
|
-
self, query:
|
|
30
|
-
) -> IQueryHandler[TManager,
|
|
31
|
+
self, query: QueryBase[TView]
|
|
32
|
+
) -> IQueryHandler[TManager, QueryBase[TView], TView] | None:
|
|
31
33
|
name = self._get_name(query.__class__)
|
|
32
34
|
return self.handlers.get(name)
|
|
33
35
|
|
|
@@ -50,6 +52,14 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
50
52
|
else:
|
|
51
53
|
raise HandlerNotRegistered(f"Query: {name}")
|
|
52
54
|
|
|
55
|
+
@overload
|
|
56
|
+
def get(
|
|
57
|
+
self,
|
|
58
|
+
query: QueryOne[TView],
|
|
59
|
+
*,
|
|
60
|
+
one: bool = False,
|
|
61
|
+
) -> QueryResult[TView]: ...
|
|
62
|
+
|
|
53
63
|
@overload
|
|
54
64
|
def get(self, query: Query[TView], *, one: Literal[True]) -> QueryResult[TView]: ...
|
|
55
65
|
|
|
@@ -63,7 +73,7 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
63
73
|
|
|
64
74
|
def get(
|
|
65
75
|
self,
|
|
66
|
-
query: Query[TView],
|
|
76
|
+
query: Query[TView] | QueryOne[TView],
|
|
67
77
|
*,
|
|
68
78
|
one: bool = False,
|
|
69
79
|
) -> QueryResult[TView] | QueryResults[TView]:
|
|
@@ -73,7 +83,7 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
73
83
|
if not handler:
|
|
74
84
|
raise HandlerNotRegistered(f"Query: {name}")
|
|
75
85
|
results = handler.get(query)
|
|
76
|
-
if not one:
|
|
86
|
+
if not (one or isinstance(query, QueryOne)):
|
|
77
87
|
return results
|
|
78
88
|
if len(results) == 0:
|
|
79
89
|
raise ValueError("No results found")
|
|
@@ -5,6 +5,8 @@ import threading
|
|
|
5
5
|
from queue import Empty, Queue
|
|
6
6
|
from typing import Mapping
|
|
7
7
|
|
|
8
|
+
from eventsourcing.utils import strtobool
|
|
9
|
+
|
|
8
10
|
from hexagonal.adapters.drivens.buses.base import BaseEventBus
|
|
9
11
|
from hexagonal.domain import CloudMessage, TEvent, TEvento
|
|
10
12
|
from hexagonal.ports.drivens import TManager
|
|
@@ -28,7 +30,8 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
28
30
|
def initialize(self, env: Mapping[str, str]) -> None:
|
|
29
31
|
self.queue: Queue[CloudMessage[TEvento]] = Queue() # o Queue(maxsize=...)
|
|
30
32
|
self._stop = threading.Event()
|
|
31
|
-
|
|
33
|
+
daemon = strtobool(env.get("EVENT_BUS_WORKER_DAEMON", "true"))
|
|
34
|
+
self._worker = threading.Thread(target=self._worker_loop, daemon=daemon)
|
|
32
35
|
|
|
33
36
|
super().initialize(env)
|
|
34
37
|
|
|
@@ -41,8 +44,8 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
41
44
|
return self._publish_messages(*events)
|
|
42
45
|
|
|
43
46
|
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
44
|
-
super()._publish_message(message)
|
|
45
47
|
self.verify()
|
|
48
|
+
super()._publish_message(message)
|
|
46
49
|
self.queue.put(message)
|
|
47
50
|
# No llamamos consume() aquí: el worker se encarga.
|
|
48
51
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from decimal import Decimal
|
|
2
3
|
from typing import Any, Hashable, cast
|
|
3
4
|
from uuid import UUID
|
|
4
5
|
|
|
@@ -119,9 +120,17 @@ class MessageMapper(Mapper[UUID]):
|
|
|
119
120
|
return cls.model_validate(event_state)
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
def default_orjson_value_serializer(obj: Any) -> Any:
|
|
124
|
+
if isinstance(obj, (UUID, Decimal)):
|
|
125
|
+
return str(obj)
|
|
126
|
+
if isinstance(obj, Inmutable):
|
|
127
|
+
return obj.model_dump(mode="json")
|
|
128
|
+
raise TypeError
|
|
129
|
+
|
|
130
|
+
|
|
122
131
|
class OrjsonTranscoder(Transcoder):
|
|
123
132
|
def encode(self, obj: Any) -> bytes:
|
|
124
|
-
return orjson.dumps(obj)
|
|
133
|
+
return orjson.dumps(obj, default=default_orjson_value_serializer)
|
|
125
134
|
|
|
126
135
|
def decode(self, data: bytes) -> Any:
|
|
127
136
|
return orjson.loads(data)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from .repository import (
|
|
2
2
|
BaseAggregateRepositoryAdapter,
|
|
3
|
+
BaseEntityRepositoryAdapter,
|
|
3
4
|
BaseRepositoryAdapter,
|
|
4
|
-
|
|
5
|
+
BaseSearchRepositoryAdapter,
|
|
5
6
|
)
|
|
6
7
|
from .unit_of_work import BaseUnitOfWork
|
|
7
8
|
|
|
@@ -9,5 +10,6 @@ __all__ = [
|
|
|
9
10
|
"BaseRepositoryAdapter",
|
|
10
11
|
"BaseAggregateRepositoryAdapter",
|
|
11
12
|
"BaseUnitOfWork",
|
|
12
|
-
"
|
|
13
|
+
"BaseEntityRepositoryAdapter",
|
|
14
|
+
"BaseSearchRepositoryAdapter",
|
|
13
15
|
]
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# pyright: reportMissingTypeStubs=false, reportUnknownArgumentType=false, reportMissingParameterType=none, reportGeneralTypeIssues=none
|
|
2
2
|
|
|
3
3
|
from typing import (
|
|
4
|
-
Any,
|
|
5
4
|
ClassVar,
|
|
6
5
|
Dict,
|
|
7
6
|
Mapping,
|
|
8
7
|
Type,
|
|
9
|
-
TypeVar,
|
|
10
8
|
get_args,
|
|
11
9
|
get_origin,
|
|
12
10
|
)
|
|
@@ -16,16 +14,16 @@ from eventsourcing.persistence import Mapper
|
|
|
16
14
|
from eventsourcing.utils import Environment
|
|
17
15
|
|
|
18
16
|
from hexagonal.application import Infrastructure
|
|
19
|
-
from hexagonal.domain import
|
|
17
|
+
from hexagonal.domain import TAggregate, TEntity, TIdEntity, TQuery, TView
|
|
20
18
|
from hexagonal.ports.drivens import (
|
|
21
19
|
IAggregateRepository,
|
|
22
20
|
IBaseRepository,
|
|
21
|
+
IEntityRepository,
|
|
22
|
+
ISearchRepository,
|
|
23
23
|
IUnitOfWork,
|
|
24
24
|
TManager,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
TAggregate = TypeVar("TAggregate", bound=AggregateRoot[Any, Any])
|
|
28
|
-
|
|
29
27
|
|
|
30
28
|
class BaseRepositoryAdapter(IBaseRepository[TManager], Infrastructure):
|
|
31
29
|
ENV: ClassVar[Dict[str, str]] = {}
|
|
@@ -83,3 +81,35 @@ class BaseAggregateRepositoryAdapter(
|
|
|
83
81
|
@property
|
|
84
82
|
def aggregate_name(self) -> str:
|
|
85
83
|
return self._type_of_aggregate.__name__
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BaseEntityRepositoryAdapter(
|
|
87
|
+
BaseRepositoryAdapter[TManager],
|
|
88
|
+
IEntityRepository[TManager, TEntity, TIdEntity],
|
|
89
|
+
):
|
|
90
|
+
_type_of_entity: Type[TEntity]
|
|
91
|
+
|
|
92
|
+
def __init_subclass__(cls) -> None:
|
|
93
|
+
super().__init_subclass__()
|
|
94
|
+
# Inspect generic base to find the concrete type argument
|
|
95
|
+
for base in getattr(cls, "__orig_bases__", []):
|
|
96
|
+
origin = get_origin(base)
|
|
97
|
+
if origin and issubclass(origin, BaseEntityRepositoryAdapter):
|
|
98
|
+
args = get_args(base)
|
|
99
|
+
if args:
|
|
100
|
+
cls._type_of_entity = args[0]
|
|
101
|
+
cls.NAME = cls._type_of_entity.__name__.upper()
|
|
102
|
+
|
|
103
|
+
def __init__(self, mapper: Mapper[UUID], connection_manager: TManager):
|
|
104
|
+
super().__init__(connection_manager)
|
|
105
|
+
self._mapper = mapper
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def entity_name(self) -> str:
|
|
109
|
+
return self._type_of_entity.__name__
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class BaseSearchRepositoryAdapter(
|
|
113
|
+
BaseRepositoryAdapter[TManager],
|
|
114
|
+
ISearchRepository[TManager, TQuery, TView],
|
|
115
|
+
): ...
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""SQLAlchemy adapters for the repository pattern.
|
|
2
|
+
|
|
3
|
+
This module provides SQLAlchemy-based implementations of the repository,
|
|
4
|
+
outbox, inbox, and unit of work patterns. Supports multiple database
|
|
5
|
+
backends (PostgreSQL, MySQL, SQLite) through SQLAlchemy's abstraction layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .datastore import SQLAlchemyConnectionContextManager, SQLAlchemyDatastore
|
|
9
|
+
from .infrastructure import SQLAlchemyInfrastructure
|
|
10
|
+
from .outbox import (
|
|
11
|
+
SQLAlchemyInboxRepository,
|
|
12
|
+
SQLAlchemyOutboxRepository,
|
|
13
|
+
SQLAlchemyPairInboxOutbox,
|
|
14
|
+
)
|
|
15
|
+
from .repository import (
|
|
16
|
+
SQLAlchemyEntityRepositoryAdapter,
|
|
17
|
+
SQLAlchemyRepositoryAdapter,
|
|
18
|
+
SQLAlchemySearchRepositoryAdapter,
|
|
19
|
+
)
|
|
20
|
+
from .unit_of_work import SQLAlchemyUnitOfWork
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SQLAlchemyConnectionContextManager",
|
|
24
|
+
"SQLAlchemyDatastore",
|
|
25
|
+
"SQLAlchemyRepositoryAdapter",
|
|
26
|
+
"SQLAlchemyUnitOfWork",
|
|
27
|
+
"SQLAlchemyOutboxRepository",
|
|
28
|
+
"SQLAlchemyInboxRepository",
|
|
29
|
+
"SQLAlchemyInfrastructure",
|
|
30
|
+
"SQLAlchemyPairInboxOutbox",
|
|
31
|
+
"SQLAlchemyEntityRepositoryAdapter",
|
|
32
|
+
"SQLAlchemySearchRepositoryAdapter",
|
|
33
|
+
]
|