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.
Files changed (74) hide show
  1. python_hexagonal-0.2.0/PKG-INFO +90 -0
  2. python_hexagonal-0.2.0/README.md +76 -0
  3. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/pyproject.toml +7 -1
  4. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/event_bus.py +14 -2
  5. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/query.py +15 -5
  6. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/event_bus.py +5 -2
  7. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/mappers.py +10 -1
  8. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/__init__.py +4 -2
  9. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/repository.py +35 -5
  10. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/__init__.py +33 -0
  11. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/datastore.py +288 -0
  12. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/env_vars.py +22 -0
  13. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/infrastructure.py +41 -0
  14. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/models.py +160 -0
  15. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/outbox.py +456 -0
  16. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/repository.py +442 -0
  17. python_hexagonal-0.2.0/src/hexagonal/adapters/drivens/repository/sqlalchemy/unit_of_work.py +60 -0
  18. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/__init__.py +24 -3
  19. python_hexagonal-0.2.0/src/hexagonal/application/api.py +210 -0
  20. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/handlers.py +40 -11
  21. python_hexagonal-0.2.0/src/hexagonal/application/query.py +85 -0
  22. python_hexagonal-0.2.0/src/hexagonal/application/topics.py +45 -0
  23. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/__init__.py +17 -0
  24. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/aggregate.py +32 -10
  25. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/base.py +10 -2
  26. python_hexagonal-0.2.0/src/hexagonal/domain/queries.py +17 -0
  27. python_hexagonal-0.2.0/src/hexagonal/entrypoints/sqlalchemy.py +146 -0
  28. python_hexagonal-0.2.0/src/hexagonal/integrations/__init__.py +1 -0
  29. python_hexagonal-0.2.0/src/hexagonal/integrations/sqlalchemy.py +49 -0
  30. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/__init__.py +4 -0
  31. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/buses.py +14 -1
  32. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/repository.py +27 -5
  33. python_hexagonal-0.1.1/PKG-INFO +0 -15
  34. python_hexagonal-0.1.1/README.md +0 -2
  35. python_hexagonal-0.1.1/src/hexagonal/application/api.py +0 -61
  36. python_hexagonal-0.1.1/src/hexagonal/application/query.py +0 -71
  37. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/__init__.py +0 -0
  38. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/__init__.py +0 -0
  39. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/__init__.py +0 -0
  40. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/__init__.py +0 -0
  41. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/__init__.py +0 -0
  42. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/command_bus.py +0 -0
  43. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/infrastructure.py +0 -0
  44. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/message_bus.py +0 -0
  45. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/base/utils.py +0 -0
  46. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/__init__.py +0 -0
  47. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/command_bus.py +0 -0
  48. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/buses/inmemory/infra.py +0 -0
  49. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/__init__.py +0 -0
  50. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/base/unit_of_work.py +0 -0
  51. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/__init__.py +0 -0
  52. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/datastore.py +0 -0
  53. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/env_vars.py +0 -0
  54. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +0 -0
  55. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/outbox.py +0 -0
  56. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/repository.py +0 -0
  57. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +0 -0
  58. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivers/__init__.py +0 -0
  59. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/adapters/drivers/app.py +0 -0
  60. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/app.py +0 -0
  61. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/bus_app.py +0 -0
  62. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/application/infrastructure.py +0 -0
  63. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/domain/exceptions.py +0 -0
  64. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/__init__.py +0 -0
  65. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/app.py +0 -0
  66. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/base.py +0 -0
  67. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/bus.py +0 -0
  68. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/entrypoints/sqlite.py +0 -0
  69. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/__init__.py +0 -0
  70. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/application.py +0 -0
  71. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivens/infrastructure.py +0 -0
  72. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivers/__init__.py +0 -0
  73. {python_hexagonal-0.1.1 → python_hexagonal-0.2.0}/src/hexagonal/ports/drivers/app.py +0 -0
  74. {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.1.1"
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__ # pyright: ignore[reportUnknownMemberType]
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()
@@ -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[Query[TView]]) -> str:
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: Query[TView]
30
- ) -> IQueryHandler[TManager, Query[TView], TView] | None:
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
- self._worker = threading.Thread(target=self._worker_loop, daemon=True)
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
- TAggregate,
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
- "TAggregate",
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 AggregateRoot, TIdEntity
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
+ ]