python-hexagonal 0.1.2__tar.gz → 0.3.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.3.0/PKG-INFO +94 -0
- python_hexagonal-0.3.0/README.md +80 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/pyproject.toml +19 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/command_bus.py +8 -11
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/event_bus.py +49 -22
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/infrastructure.py +4 -4
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/message_bus.py +34 -3
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/query.py +24 -7
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/inmemory/command_bus.py +2 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/inmemory/event_bus.py +10 -4
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/mappers.py +4 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/base/__init__.py +4 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/base/repository.py +40 -13
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/base/unit_of_work.py +25 -9
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/__init__.py +9 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/datastore.py +9 -6
- python_hexagonal-0.3.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/infrastructure.py +102 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/models.py +9 -5
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/outbox.py +3 -3
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/repository.py +123 -5
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/datastore.py +14 -8
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/outbox.py +2 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/repository.py +1 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivers/app.py +19 -10
- python_hexagonal-0.3.0/src/hexagonal/application/__init__.py +59 -0
- python_hexagonal-0.3.0/src/hexagonal/application/api.py +148 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/application/app.py +18 -15
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/application/bus_app.py +4 -0
- python_hexagonal-0.3.0/src/hexagonal/application/handlers.py +203 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/application/infrastructure.py +3 -3
- python_hexagonal-0.3.0/src/hexagonal/application/query.py +101 -0
- python_hexagonal-0.3.0/src/hexagonal/application/topics.py +45 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/domain/__init__.py +20 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/domain/aggregate.py +34 -18
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/domain/base.py +17 -7
- python_hexagonal-0.3.0/src/hexagonal/domain/queries.py +22 -0
- python_hexagonal-0.3.0/src/hexagonal/domain/responses.py +58 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/entrypoints/app.py +13 -6
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/entrypoints/base.py +2 -2
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/entrypoints/bus.py +15 -3
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/entrypoints/sqlalchemy.py +13 -10
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/entrypoints/sqlite.py +9 -2
- python_hexagonal-0.3.0/src/hexagonal/integrations/__init__.py +1 -0
- python_hexagonal-0.3.0/src/hexagonal/integrations/sqlalchemy.py +51 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivens/__init__.py +22 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivens/buses.py +46 -9
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivens/infrastructure.py +1 -1
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivens/repository.py +56 -11
- python_hexagonal-0.3.0/src/hexagonal/ports/drivens/scoped.py +36 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivers/app.py +3 -3
- 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/adapters/drivens/repository/sqlalchemy/infrastructure.py +0 -41
- python_hexagonal-0.1.2/src/hexagonal/application/__init__.py +0 -29
- python_hexagonal-0.1.2/src/hexagonal/application/api.py +0 -61
- python_hexagonal-0.1.2/src/hexagonal/application/handlers.py +0 -107
- python_hexagonal-0.1.2/src/hexagonal/application/query.py +0 -71
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/utils.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/inmemory/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/inmemory/infra.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/env_vars.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlalchemy/unit_of_work.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/env_vars.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivers/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/domain/exceptions.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/entrypoints/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivens/application.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/ports/drivers/__init__.py +0 -0
- {python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/py.typed +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-hexagonal
|
|
3
|
+
Version: 0.3.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
|
+
- 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)
|
|
32
|
+
- Learn the canonical testing workflow:
|
|
33
|
+
[`docs/how-to/test-use-cases.md`](docs/how-to/test-use-cases.md)
|
|
34
|
+
- Check the guardrails before copying imports:
|
|
35
|
+
[`docs/reference/supported-surface.md`](docs/reference/supported-surface.md)
|
|
36
|
+
- Review the evidence map if you want to verify a claim against code or tests:
|
|
37
|
+
[`docs/reference/evidence-map.yaml`](docs/reference/evidence-map.yaml)
|
|
38
|
+
|
|
39
|
+
## What you learn from the first path
|
|
40
|
+
|
|
41
|
+
After the first-app guide you should understand:
|
|
42
|
+
|
|
43
|
+
- where the domain model lives and why it stays isolated
|
|
44
|
+
- how application APIs wrap command, query, and event buses
|
|
45
|
+
- how ports describe the infrastructure your app needs
|
|
46
|
+
- how entrypoints assemble an app from environment and infrastructure
|
|
47
|
+
- how the use-case tests prove the workflow end to end
|
|
48
|
+
|
|
49
|
+
## The blueprint we actually trust
|
|
50
|
+
|
|
51
|
+
These files are the baseline for the supported adoption story:
|
|
52
|
+
|
|
53
|
+
- `src/example/contacto/domain/contacto.py` - aggregate behavior, value-object
|
|
54
|
+
strategy, and query entry points
|
|
55
|
+
- `src/example/contacto/application/app.py` - application composition through
|
|
56
|
+
`BusAppGroup`
|
|
57
|
+
- `src/example/contacto/ports/drivens.py` - infrastructure contracts and
|
|
58
|
+
repository boundaries
|
|
59
|
+
- `src/example/app/application/api.py` - top-level API wrapper consumers call
|
|
60
|
+
- `src/example/app/entrypoints/main.py` - environment-driven bootstrap via
|
|
61
|
+
`EntrypointGroup`
|
|
62
|
+
- `src/example/app/entrypoints/db/sqlalchemy.py` - SQLAlchemy-specific
|
|
63
|
+
infrastructure assembly
|
|
64
|
+
- `src/tests/use_cases/base.py` - canonical test bootstrap with migrations,
|
|
65
|
+
entrypoint creation, and topic registration
|
|
66
|
+
|
|
67
|
+
## Supported path vs internals
|
|
68
|
+
|
|
69
|
+
The documented path is intentionally narrow.
|
|
70
|
+
|
|
71
|
+
- Start from `hexagonal.domain`, `hexagonal.application`,
|
|
72
|
+
`hexagonal.ports.drivens`, `hexagonal.ports.drivers`, and
|
|
73
|
+
`hexagonal.entrypoints`
|
|
74
|
+
- Reach for `hexagonal.integrations.sqlalchemy` when you need the reusable
|
|
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
|
|
78
|
+
- Treat `hexagonal.entrypoints.sqlalchemy` as adapter-specific convenience, not
|
|
79
|
+
the whole framework story
|
|
80
|
+
- Do not treat `hexagonal.__init__` as the public integration surface; right now
|
|
81
|
+
it only exposes `hello()`
|
|
82
|
+
- Do not cargo-cult example names like `exampleAPI`, `exampleEntrypoint`, or
|
|
83
|
+
`Exampletate`; copy the roles, not the labels
|
|
84
|
+
- Do not build new code against `hexagonal.adapters.*`; that path is kept for
|
|
85
|
+
compatibility, while the supported SQLAlchemy extension surface now lives under
|
|
86
|
+
`hexagonal.integrations.sqlalchemy`
|
|
87
|
+
|
|
88
|
+
## Companion skill
|
|
89
|
+
|
|
90
|
+
This repo also ships an installable companion skill at
|
|
91
|
+
`skills/python-hexagonal-usage/SKILL.md`.
|
|
92
|
+
It is there to inspect a user repository, map it to the same architecture, and
|
|
93
|
+
point people back to the written docs. It does NOT replace the docs and it does
|
|
94
|
+
NOT get to invent new supported APIs.
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
- 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)
|
|
18
|
+
- Learn the canonical testing workflow:
|
|
19
|
+
[`docs/how-to/test-use-cases.md`](docs/how-to/test-use-cases.md)
|
|
20
|
+
- Check the guardrails before copying imports:
|
|
21
|
+
[`docs/reference/supported-surface.md`](docs/reference/supported-surface.md)
|
|
22
|
+
- Review the evidence map if you want to verify a claim against code or tests:
|
|
23
|
+
[`docs/reference/evidence-map.yaml`](docs/reference/evidence-map.yaml)
|
|
24
|
+
|
|
25
|
+
## What you learn from the first path
|
|
26
|
+
|
|
27
|
+
After the first-app guide you should understand:
|
|
28
|
+
|
|
29
|
+
- where the domain model lives and why it stays isolated
|
|
30
|
+
- how application APIs wrap command, query, and event buses
|
|
31
|
+
- how ports describe the infrastructure your app needs
|
|
32
|
+
- how entrypoints assemble an app from environment and infrastructure
|
|
33
|
+
- how the use-case tests prove the workflow end to end
|
|
34
|
+
|
|
35
|
+
## The blueprint we actually trust
|
|
36
|
+
|
|
37
|
+
These files are the baseline for the supported adoption story:
|
|
38
|
+
|
|
39
|
+
- `src/example/contacto/domain/contacto.py` - aggregate behavior, value-object
|
|
40
|
+
strategy, and query entry points
|
|
41
|
+
- `src/example/contacto/application/app.py` - application composition through
|
|
42
|
+
`BusAppGroup`
|
|
43
|
+
- `src/example/contacto/ports/drivens.py` - infrastructure contracts and
|
|
44
|
+
repository boundaries
|
|
45
|
+
- `src/example/app/application/api.py` - top-level API wrapper consumers call
|
|
46
|
+
- `src/example/app/entrypoints/main.py` - environment-driven bootstrap via
|
|
47
|
+
`EntrypointGroup`
|
|
48
|
+
- `src/example/app/entrypoints/db/sqlalchemy.py` - SQLAlchemy-specific
|
|
49
|
+
infrastructure assembly
|
|
50
|
+
- `src/tests/use_cases/base.py` - canonical test bootstrap with migrations,
|
|
51
|
+
entrypoint creation, and topic registration
|
|
52
|
+
|
|
53
|
+
## Supported path vs internals
|
|
54
|
+
|
|
55
|
+
The documented path is intentionally narrow.
|
|
56
|
+
|
|
57
|
+
- Start from `hexagonal.domain`, `hexagonal.application`,
|
|
58
|
+
`hexagonal.ports.drivens`, `hexagonal.ports.drivers`, and
|
|
59
|
+
`hexagonal.entrypoints`
|
|
60
|
+
- Reach for `hexagonal.integrations.sqlalchemy` when you need the reusable
|
|
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
|
|
64
|
+
- Treat `hexagonal.entrypoints.sqlalchemy` as adapter-specific convenience, not
|
|
65
|
+
the whole framework story
|
|
66
|
+
- Do not treat `hexagonal.__init__` as the public integration surface; right now
|
|
67
|
+
it only exposes `hello()`
|
|
68
|
+
- Do not cargo-cult example names like `exampleAPI`, `exampleEntrypoint`, or
|
|
69
|
+
`Exampletate`; copy the roles, not the labels
|
|
70
|
+
- Do not build new code against `hexagonal.adapters.*`; that path is kept for
|
|
71
|
+
compatibility, while the supported SQLAlchemy extension surface now lives under
|
|
72
|
+
`hexagonal.integrations.sqlalchemy`
|
|
73
|
+
|
|
74
|
+
## Companion skill
|
|
75
|
+
|
|
76
|
+
This repo also ships an installable companion skill at
|
|
77
|
+
`skills/python-hexagonal-usage/SKILL.md`.
|
|
78
|
+
It is there to inspect a user repository, map it to the same architecture, and
|
|
79
|
+
point people back to the written docs. It does NOT replace the docs and it does
|
|
80
|
+
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.3.0"
|
|
4
4
|
description = "Framework to build hexagonal architecture applications in Python."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -26,9 +26,27 @@ 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 = [
|
|
47
|
+
"alembic>=1.18.4",
|
|
31
48
|
"pytest>=9.0.2",
|
|
49
|
+
"python-dotenv>=1.2.2",
|
|
32
50
|
]
|
|
33
51
|
sqlalchemy = [
|
|
34
52
|
"sqlalchemy>=2.0.45",
|
|
@@ -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,8 +34,34 @@ 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
|
+
if self._write_scope_runner is None or self._outbox_repository_getter is None:
|
|
50
|
+
self.outbox_repository.save(*messages)
|
|
51
|
+
return
|
|
52
|
+
outbox_repository_getter = self._outbox_repository_getter
|
|
53
|
+
|
|
54
|
+
scope = self._write_scope_runner.current_write_scope
|
|
55
|
+
if scope is not None:
|
|
56
|
+
outbox_repository_getter(scope).save(*messages)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self._write_scope_runner.run_in_write_scope(
|
|
60
|
+
lambda write_scope: outbox_repository_getter(write_scope).save(*messages)
|
|
61
|
+
)
|
|
62
|
+
|
|
32
63
|
# publish
|
|
33
|
-
def publish_from_outbox(self, limit: int | None = None):
|
|
64
|
+
def publish_from_outbox(self, limit: int | None = None) -> None:
|
|
34
65
|
self.verify()
|
|
35
66
|
messages = self.outbox_repository.fetch_pending(limit=limit)
|
|
36
67
|
self._publish_messages(*messages)
|
{python_hexagonal-0.1.2 → python_hexagonal-0.3.0}/src/hexagonal/adapters/drivens/buses/base/query.py
RENAMED
|
@@ -7,12 +7,15 @@ 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,
|
|
13
15
|
TView,
|
|
14
16
|
)
|
|
15
17
|
from hexagonal.ports.drivens import IQueryBus, IQueryHandler, TManager
|
|
18
|
+
from hexagonal.ports.drivens.scoped import IReadScopeRunner
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
@@ -20,14 +23,20 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
20
23
|
|
|
21
24
|
def initialize(self, env: Mapping[str, str]) -> None:
|
|
22
25
|
self.handlers = {}
|
|
26
|
+
self._read_scope_runner: IReadScopeRunner[Any] | None = None
|
|
23
27
|
super().initialize(env)
|
|
24
28
|
|
|
25
|
-
def
|
|
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
|
+
|
|
34
|
+
def _get_name(self, query_type: Type[QueryBase[TView]]) -> str:
|
|
26
35
|
return get_topic(query_type)
|
|
27
36
|
|
|
28
37
|
def _get_handler(
|
|
29
|
-
self, query:
|
|
30
|
-
) -> IQueryHandler[TManager,
|
|
38
|
+
self, query: QueryBase[TView]
|
|
39
|
+
) -> IQueryHandler[TManager, QueryBase[TView], TView] | None:
|
|
31
40
|
name = self._get_name(query.__class__)
|
|
32
41
|
return self.handlers.get(name)
|
|
33
42
|
|
|
@@ -35,14 +44,14 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
35
44
|
self,
|
|
36
45
|
query_type: Type[TQuery],
|
|
37
46
|
handler: IQueryHandler[TManager, TQuery, TView],
|
|
38
|
-
):
|
|
47
|
+
) -> None:
|
|
39
48
|
self.verify()
|
|
40
49
|
name = self._get_name(query_type)
|
|
41
50
|
if name in self.handlers:
|
|
42
51
|
raise HandlerAlreadyRegistered(f"Query: {name}")
|
|
43
52
|
self.handlers[name] = handler
|
|
44
53
|
|
|
45
|
-
def unregister_handler(self, query_type: Type[TQuery]):
|
|
54
|
+
def unregister_handler(self, query_type: Type[TQuery]) -> None:
|
|
46
55
|
self.verify()
|
|
47
56
|
name = self._get_name(query_type)
|
|
48
57
|
if name in self.handlers:
|
|
@@ -50,6 +59,14 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
50
59
|
else:
|
|
51
60
|
raise HandlerNotRegistered(f"Query: {name}")
|
|
52
61
|
|
|
62
|
+
@overload
|
|
63
|
+
def get(
|
|
64
|
+
self,
|
|
65
|
+
query: QueryOne[TView],
|
|
66
|
+
*,
|
|
67
|
+
one: bool = False,
|
|
68
|
+
) -> QueryResult[TView]: ...
|
|
69
|
+
|
|
53
70
|
@overload
|
|
54
71
|
def get(self, query: Query[TView], *, one: Literal[True]) -> QueryResult[TView]: ...
|
|
55
72
|
|
|
@@ -63,7 +80,7 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
63
80
|
|
|
64
81
|
def get(
|
|
65
82
|
self,
|
|
66
|
-
query: Query[TView],
|
|
83
|
+
query: Query[TView] | QueryOne[TView],
|
|
67
84
|
*,
|
|
68
85
|
one: bool = False,
|
|
69
86
|
) -> QueryResult[TView] | QueryResults[TView]:
|
|
@@ -73,7 +90,7 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
73
90
|
if not handler:
|
|
74
91
|
raise HandlerNotRegistered(f"Query: {name}")
|
|
75
92
|
results = handler.get(query)
|
|
76
|
-
if not one:
|
|
93
|
+
if not (one or isinstance(query, QueryOne)):
|
|
77
94
|
return results
|
|
78
95
|
if len(results) == 0:
|
|
79
96
|
raise ValueError("No results found")
|
|
@@ -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()
|
|
@@ -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
|
|
@@ -13,6 +15,9 @@ logger = logging.getLogger(__name__)
|
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class InMemoryEventBus(BaseEventBus[TManager]):
|
|
18
|
+
def shutdown(self) -> None:
|
|
19
|
+
return
|
|
20
|
+
|
|
16
21
|
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
17
22
|
super()._publish_message(message)
|
|
18
23
|
self._process_messages(message)
|
|
@@ -20,7 +25,7 @@ class InMemoryEventBus(BaseEventBus[TManager]):
|
|
|
20
25
|
def publish(self, *events: CloudMessage[TEvent]) -> None:
|
|
21
26
|
return self._publish_messages(*events)
|
|
22
27
|
|
|
23
|
-
def consume(self, limit: int | None = None):
|
|
28
|
+
def consume(self, limit: int | None = None) -> None:
|
|
24
29
|
pass # No-op for non-queued bus
|
|
25
30
|
|
|
26
31
|
|
|
@@ -28,7 +33,8 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
28
33
|
def initialize(self, env: Mapping[str, str]) -> None:
|
|
29
34
|
self.queue: Queue[CloudMessage[TEvento]] = Queue() # o Queue(maxsize=...)
|
|
30
35
|
self._stop = threading.Event()
|
|
31
|
-
|
|
36
|
+
daemon = strtobool(env.get("EVENT_BUS_WORKER_DAEMON", "true"))
|
|
37
|
+
self._worker = threading.Thread(target=self._worker_loop, daemon=daemon)
|
|
32
38
|
|
|
33
39
|
super().initialize(env)
|
|
34
40
|
|
|
@@ -41,8 +47,8 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
41
47
|
return self._publish_messages(*events)
|
|
42
48
|
|
|
43
49
|
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
44
|
-
super()._publish_message(message)
|
|
45
50
|
self.verify()
|
|
51
|
+
super()._publish_message(message)
|
|
46
52
|
self.queue.put(message)
|
|
47
53
|
# No llamamos consume() aquí: el worker se encarga.
|
|
48
54
|
|
|
@@ -65,5 +71,5 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
65
71
|
finally:
|
|
66
72
|
self.queue.task_done()
|
|
67
73
|
|
|
68
|
-
def consume(self, limit: int | None = None):
|
|
74
|
+
def consume(self, limit: int | None = None) -> None:
|
|
69
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
|
|
|
@@ -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
|
|