python-hexagonal 0.1.2__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.2 → python_hexagonal-0.2.0}/pyproject.toml +3 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/query.py +15 -5
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/event_bus.py +5 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/mappers.py +2 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/__init__.py +4 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/repository.py +35 -5
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/__init__.py +7 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/models.py +9 -5
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/outbox.py +1 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/repository.py +123 -5
- {python_hexagonal-0.1.2 → 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.2 → 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.2 → python_hexagonal-0.2.0}/src/hexagonal/domain/__init__.py +17 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/domain/aggregate.py +26 -10
- {python_hexagonal-0.1.2 → 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.1.2 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/sqlalchemy.py +6 -8
- 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.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/__init__.py +4 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/buses.py +14 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/repository.py +27 -5
- python_hexagonal-0.1.2/PKG-INFO +0 -16
- python_hexagonal-0.1.2/README.md +0 -2
- python_hexagonal-0.1.2/src/hexagonal/application/api.py +0 -61
- python_hexagonal-0.1.2/src/hexagonal/application/query.py +0 -71
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/command_bus.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/event_bus.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/infrastructure.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/message_bus.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/utils.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/command_bus.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/infra.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/unit_of_work.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/datastore.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/env_vars.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/infrastructure.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/unit_of_work.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/datastore.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/env_vars.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/outbox.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/repository.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivers/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivers/app.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/application/app.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/application/bus_app.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/application/infrastructure.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/domain/exceptions.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/app.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/base.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/bus.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/sqlite.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/application.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/infrastructure.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivers/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivers/app.py +0 -0
- {python_hexagonal-0.1.2 → 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 = [
|
|
@@ -28,7 +28,9 @@ fixable = ["I", "E", "F", "B"]
|
|
|
28
28
|
|
|
29
29
|
[dependency-groups]
|
|
30
30
|
dev = [
|
|
31
|
+
"alembic>=1.18.4",
|
|
31
32
|
"pytest>=9.0.2",
|
|
33
|
+
"python-dotenv>=1.2.2",
|
|
32
34
|
]
|
|
33
35
|
sqlalchemy = [
|
|
34
36
|
"sqlalchemy>=2.0.45",
|
{python_hexagonal-0.1.2 → 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
|
|
|
@@ -123,6 +123,8 @@ class MessageMapper(Mapper[UUID]):
|
|
|
123
123
|
def default_orjson_value_serializer(obj: Any) -> Any:
|
|
124
124
|
if isinstance(obj, (UUID, Decimal)):
|
|
125
125
|
return str(obj)
|
|
126
|
+
if isinstance(obj, Inmutable):
|
|
127
|
+
return obj.model_dump(mode="json")
|
|
126
128
|
raise TypeError
|
|
127
129
|
|
|
128
130
|
|
|
@@ -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
|
+
): ...
|
|
@@ -12,7 +12,11 @@ from .outbox import (
|
|
|
12
12
|
SQLAlchemyOutboxRepository,
|
|
13
13
|
SQLAlchemyPairInboxOutbox,
|
|
14
14
|
)
|
|
15
|
-
from .repository import
|
|
15
|
+
from .repository import (
|
|
16
|
+
SQLAlchemyEntityRepositoryAdapter,
|
|
17
|
+
SQLAlchemyRepositoryAdapter,
|
|
18
|
+
SQLAlchemySearchRepositoryAdapter,
|
|
19
|
+
)
|
|
16
20
|
from .unit_of_work import SQLAlchemyUnitOfWork
|
|
17
21
|
|
|
18
22
|
__all__ = [
|
|
@@ -24,4 +28,6 @@ __all__ = [
|
|
|
24
28
|
"SQLAlchemyInboxRepository",
|
|
25
29
|
"SQLAlchemyInfrastructure",
|
|
26
30
|
"SQLAlchemyPairInboxOutbox",
|
|
31
|
+
"SQLAlchemyEntityRepositoryAdapter",
|
|
32
|
+
"SQLAlchemySearchRepositoryAdapter",
|
|
27
33
|
]
|
|
@@ -19,7 +19,7 @@ metadata = MetaData()
|
|
|
19
19
|
|
|
20
20
|
def _get_table_key(table_name: str, schema: str | None = None) -> str:
|
|
21
21
|
"""Get the key used by SQLAlchemy metadata to store the table."""
|
|
22
|
-
if schema:
|
|
22
|
+
if schema and schema != "":
|
|
23
23
|
return f"{schema}.{table_name}"
|
|
24
24
|
return table_name
|
|
25
25
|
|
|
@@ -34,6 +34,7 @@ def create_aggregates_table(table_name: str, schema: str | None = None) -> Table
|
|
|
34
34
|
Returns:
|
|
35
35
|
SQLAlchemy Table object for aggregate snapshots
|
|
36
36
|
"""
|
|
37
|
+
schema = schema if schema and schema != "" else None
|
|
37
38
|
full_table_name = f"aggregates_{table_name}"
|
|
38
39
|
table_key = _get_table_key(full_table_name, schema)
|
|
39
40
|
|
|
@@ -45,7 +46,7 @@ def create_aggregates_table(table_name: str, schema: str | None = None) -> Table
|
|
|
45
46
|
full_table_name,
|
|
46
47
|
metadata,
|
|
47
48
|
Column("originator_id", String(36), nullable=False),
|
|
48
|
-
Column("aggregate_name",
|
|
49
|
+
Column("aggregate_name", String(255), nullable=False),
|
|
49
50
|
Column("originator_version", Integer, nullable=False),
|
|
50
51
|
Column("topic", Text, nullable=False),
|
|
51
52
|
Column("state", LargeBinary, nullable=False),
|
|
@@ -66,6 +67,7 @@ def create_events_table(table_name: str, schema: str | None = None) -> Table:
|
|
|
66
67
|
Returns:
|
|
67
68
|
SQLAlchemy Table object for aggregate events history
|
|
68
69
|
"""
|
|
70
|
+
schema = schema if schema and schema != "" else None
|
|
69
71
|
full_table_name = f"aggregates_{table_name}_events"
|
|
70
72
|
table_key = _get_table_key(full_table_name, schema)
|
|
71
73
|
|
|
@@ -77,7 +79,7 @@ def create_events_table(table_name: str, schema: str | None = None) -> Table:
|
|
|
77
79
|
full_table_name,
|
|
78
80
|
metadata,
|
|
79
81
|
Column("originator_id", String(36), nullable=False),
|
|
80
|
-
Column("aggregate_name",
|
|
82
|
+
Column("aggregate_name", String(255), nullable=False),
|
|
81
83
|
Column("originator_version", Integer, nullable=False),
|
|
82
84
|
Column("topic", Text, nullable=False),
|
|
83
85
|
Column("state", LargeBinary, nullable=False),
|
|
@@ -97,6 +99,7 @@ def create_outbox_table(table_name: str = "outbox", schema: str | None = None) -
|
|
|
97
99
|
Returns:
|
|
98
100
|
SQLAlchemy Table object for outbox messages
|
|
99
101
|
"""
|
|
102
|
+
schema = schema if schema and schema != "" else None
|
|
100
103
|
table_key = _get_table_key(table_name, schema)
|
|
101
104
|
|
|
102
105
|
# Return existing table if already defined
|
|
@@ -107,7 +110,7 @@ def create_outbox_table(table_name: str = "outbox", schema: str | None = None) -
|
|
|
107
110
|
table_name,
|
|
108
111
|
metadata,
|
|
109
112
|
Column("message_id", String(36), primary_key=True),
|
|
110
|
-
Column("topic",
|
|
113
|
+
Column("topic", String(255), nullable=False),
|
|
111
114
|
Column("message", LargeBinary, nullable=False),
|
|
112
115
|
Column("published_at", DateTime(timezone=True), nullable=True),
|
|
113
116
|
Column("failed_at", DateTime(timezone=True), nullable=True),
|
|
@@ -134,6 +137,7 @@ def create_inbox_table(table_name: str = "inbox", schema: str | None = None) ->
|
|
|
134
137
|
Returns:
|
|
135
138
|
SQLAlchemy Table object for inbox messages
|
|
136
139
|
"""
|
|
140
|
+
schema = schema if schema and schema != "" else None
|
|
137
141
|
table_key = _get_table_key(table_name, schema)
|
|
138
142
|
|
|
139
143
|
# Return existing table if already defined
|
|
@@ -144,7 +148,7 @@ def create_inbox_table(table_name: str = "inbox", schema: str | None = None) ->
|
|
|
144
148
|
table_name,
|
|
145
149
|
metadata,
|
|
146
150
|
Column("message_id", String(36), nullable=False),
|
|
147
|
-
Column("handler",
|
|
151
|
+
Column("handler", String(255), nullable=False),
|
|
148
152
|
Column("received_at", DateTime(timezone=True), nullable=False),
|
|
149
153
|
Column("processed_at", DateTime(timezone=True), nullable=True),
|
|
150
154
|
Column("error", Text, nullable=True),
|
|
@@ -313,7 +313,7 @@ class SQLAlchemyInboxRepository(
|
|
|
313
313
|
|
|
314
314
|
def _cursor(self):
|
|
315
315
|
"""Get the current connection for executing statements."""
|
|
316
|
-
return self.
|
|
316
|
+
return self.connection_manager.cursor()
|
|
317
317
|
|
|
318
318
|
def create_tables(self) -> None:
|
|
319
319
|
"""Create the inbox table if it doesn't exist."""
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from typing import Any, ClassVar, Dict, Mapping, Sequence, Tuple, cast
|
|
7
8
|
from uuid import UUID
|
|
8
9
|
|
|
9
10
|
from eventsourcing.domain import CanMutateAggregate
|
|
@@ -11,24 +12,104 @@ from eventsourcing.persistence import StoredEvent
|
|
|
11
12
|
from eventsourcing.utils import strtobool
|
|
12
13
|
from sqlalchemy import Connection, delete, insert, select, update
|
|
13
14
|
|
|
14
|
-
from hexagonal.adapters.drivens.repository.base import
|
|
15
|
+
from hexagonal.adapters.drivens.repository.base import (
|
|
16
|
+
BaseAggregateRepositoryAdapter,
|
|
17
|
+
BaseEntityRepositoryAdapter,
|
|
18
|
+
BaseSearchRepositoryAdapter,
|
|
19
|
+
)
|
|
15
20
|
from hexagonal.domain import (
|
|
16
21
|
AggregateNotFound,
|
|
17
|
-
AggregateRoot,
|
|
18
22
|
AggregateSnapshot,
|
|
19
23
|
AggregateVersionMismatch,
|
|
20
24
|
SnapshotState,
|
|
25
|
+
TAggregate,
|
|
26
|
+
TEntity,
|
|
21
27
|
TIdEntity,
|
|
28
|
+
TQuery,
|
|
29
|
+
TView,
|
|
22
30
|
)
|
|
23
31
|
|
|
24
32
|
from .datastore import SQLAlchemyConnectionContextManager
|
|
25
33
|
from .models import create_aggregates_table, create_events_table
|
|
26
34
|
|
|
27
|
-
TAggregate = TypeVar("TAggregate", bound=AggregateRoot[Any, Any])
|
|
28
|
-
|
|
29
35
|
logger = logging.getLogger(__name__)
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
class SQLAlchemyEntityRepositoryAdapter(
|
|
39
|
+
BaseEntityRepositoryAdapter[SQLAlchemyConnectionContextManager, TEntity, TIdEntity]
|
|
40
|
+
):
|
|
41
|
+
"""SQLAlchemy repository adapter for entities.
|
|
42
|
+
|
|
43
|
+
This adapter implements the IEntityRepository interface using SQLAlchemy
|
|
44
|
+
as the backing store. It handles the persistence and retrieval of entities.
|
|
45
|
+
|
|
46
|
+
Supports multiple database backends (PostgreSQL, MySQL, SQLite) through
|
|
47
|
+
SQLAlchemy's abstraction layer.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
mapper: Mapper for converting between domain and persistence models
|
|
51
|
+
connection_manager: SQLAlchemy connection context manager
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
55
|
+
"""Initialize the repository from environment variables."""
|
|
56
|
+
super().initialize(env)
|
|
57
|
+
self._schema_name: str | None = self.env.get("SCHEMA_NAME")
|
|
58
|
+
|
|
59
|
+
def _get(self, conn: Connection, id: TIdEntity) -> TEntity | None:
|
|
60
|
+
"""Fetch entity from database."""
|
|
61
|
+
# Implement entity retrieval logic using SQLAlchemy
|
|
62
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def _insert(self, conn: Connection, entity: TEntity) -> None:
|
|
66
|
+
"""Save an entity to the database."""
|
|
67
|
+
# Implement entity persistence logic using SQLAlchemy
|
|
68
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def _update(self, conn: Connection, entity: TEntity) -> None:
|
|
72
|
+
"""Update an existing entity in the database."""
|
|
73
|
+
# Implement entity update logic using SQLAlchemy
|
|
74
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def _delete(self, conn: Connection, id: TIdEntity) -> None:
|
|
78
|
+
"""Delete an entity from the database."""
|
|
79
|
+
# Implement entity deletion logic using SQLAlchemy
|
|
80
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def get(self, id: TIdEntity) -> TEntity:
|
|
84
|
+
"""Get an entity by its ID."""
|
|
85
|
+
self.verify()
|
|
86
|
+
with self.connection_manager.cursor() as conn:
|
|
87
|
+
entity = self._get(conn, id)
|
|
88
|
+
if entity is None:
|
|
89
|
+
raise AggregateNotFound(f"Entity with id {id} not found")
|
|
90
|
+
return entity
|
|
91
|
+
|
|
92
|
+
def save(self, entity: TEntity):
|
|
93
|
+
"""Save an entity to the repository."""
|
|
94
|
+
self.verify()
|
|
95
|
+
with self.connection_manager.cursor() as conn:
|
|
96
|
+
existing = self._get(conn, entity.id)
|
|
97
|
+
if existing is None:
|
|
98
|
+
self._insert(conn, entity)
|
|
99
|
+
else:
|
|
100
|
+
self._update(conn, entity)
|
|
101
|
+
|
|
102
|
+
def delete(self, id: TIdEntity) -> TEntity:
|
|
103
|
+
"""Delete an entity from the repository."""
|
|
104
|
+
self.verify()
|
|
105
|
+
with self.connection_manager.cursor() as conn:
|
|
106
|
+
entity = self._get(conn, id)
|
|
107
|
+
if entity is None:
|
|
108
|
+
raise AggregateNotFound(f"Entity with id {id} not found")
|
|
109
|
+
self._delete(conn, id)
|
|
110
|
+
return entity
|
|
111
|
+
|
|
112
|
+
|
|
32
113
|
class SQLAlchemyRepositoryAdapter(
|
|
33
114
|
BaseAggregateRepositoryAdapter[
|
|
34
115
|
SQLAlchemyConnectionContextManager, TAggregate, TIdEntity
|
|
@@ -322,3 +403,40 @@ class SQLAlchemyRepositoryAdapter(
|
|
|
322
403
|
self._save_event_history(conn, events)
|
|
323
404
|
self._delete(conn, id)
|
|
324
405
|
return agg
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class SQLAlchemySearchRepositoryAdapter(
|
|
409
|
+
BaseSearchRepositoryAdapter[SQLAlchemyConnectionContextManager, TQuery, TView]
|
|
410
|
+
):
|
|
411
|
+
"""SQLAlchemy repository adapter for search queries.
|
|
412
|
+
|
|
413
|
+
This adapter implements the ISearchRepository interface using SQLAlchemy
|
|
414
|
+
as the backing store. It handles executing search queries and returning
|
|
415
|
+
results in a view format.
|
|
416
|
+
|
|
417
|
+
Supports multiple database backends (PostgreSQL, MySQL, SQLite) through
|
|
418
|
+
SQLAlchemy's abstraction layer.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
mapper: Mapper for converting between domain and persistence models
|
|
422
|
+
connection_manager: SQLAlchemy connection context manager
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
@abstractmethod
|
|
426
|
+
def _search(self, conn: Connection, query: TQuery) -> Sequence[TView]: ...
|
|
427
|
+
|
|
428
|
+
def search(self, query: TQuery) -> Sequence[TView]:
|
|
429
|
+
"""Execute a search query against the repository.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
query: The search query to execute
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
A sequence of view objects matching the query criteria
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
RuntimeError: If not attached to a unit of work
|
|
439
|
+
"""
|
|
440
|
+
self.verify()
|
|
441
|
+
with self.connection_manager.cursor() as conn:
|
|
442
|
+
return self._search(conn, query)
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
from .api import BaseAPI, GetEvent
|
|
1
|
+
from .api import BaseAPI, GetEvent, TBaseApp
|
|
2
2
|
from .app import Application
|
|
3
3
|
from .bus_app import BusAppGroup, ComposableBusApp
|
|
4
|
-
from .handlers import
|
|
4
|
+
from .handlers import (
|
|
5
|
+
CommandHandler,
|
|
6
|
+
CommandHandlerBase,
|
|
7
|
+
EventHandler,
|
|
8
|
+
EventHandlerBase,
|
|
9
|
+
MessageHandler,
|
|
10
|
+
QueryHandler,
|
|
11
|
+
)
|
|
5
12
|
from .infrastructure import (
|
|
6
13
|
ComposableInfrastructure,
|
|
7
14
|
Infrastructure,
|
|
8
15
|
InfrastructureGroup,
|
|
9
16
|
)
|
|
10
|
-
from .query import
|
|
17
|
+
from .query import (
|
|
18
|
+
AggregateView,
|
|
19
|
+
GetAggregateByIdHandler,
|
|
20
|
+
GetById,
|
|
21
|
+
GetByIdHandler,
|
|
22
|
+
GetEntityByIdHandler,
|
|
23
|
+
SearchAggregateRepository,
|
|
24
|
+
)
|
|
25
|
+
from .topics import RegisterTopics
|
|
11
26
|
|
|
12
27
|
__all__ = [
|
|
28
|
+
"TBaseApp",
|
|
13
29
|
"BaseAPI",
|
|
14
30
|
"GetEvent",
|
|
15
31
|
"Application",
|
|
@@ -26,4 +42,9 @@ __all__ = [
|
|
|
26
42
|
"SearchAggregateRepository",
|
|
27
43
|
"AggregateView",
|
|
28
44
|
"GetByIdHandler",
|
|
45
|
+
"RegisterTopics",
|
|
46
|
+
"GetAggregateByIdHandler",
|
|
47
|
+
"GetEntityByIdHandler",
|
|
48
|
+
"CommandHandlerBase",
|
|
49
|
+
"EventHandlerBase",
|
|
29
50
|
]
|