python-hexagonal 0.1.0__py3-none-any.whl

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 (53) hide show
  1. hexagonal/__init__.py +2 -0
  2. hexagonal/adapters/drivens/buses/base/__init__.py +15 -0
  3. hexagonal/adapters/drivens/buses/base/command_bus.py +69 -0
  4. hexagonal/adapters/drivens/buses/base/event_bus.py +160 -0
  5. hexagonal/adapters/drivens/buses/base/infrastructure.py +38 -0
  6. hexagonal/adapters/drivens/buses/base/message_bus.py +73 -0
  7. hexagonal/adapters/drivens/buses/base/query.py +82 -0
  8. hexagonal/adapters/drivens/buses/base/utils.py +1 -0
  9. hexagonal/adapters/drivens/buses/inmemory/__init__.py +12 -0
  10. hexagonal/adapters/drivens/buses/inmemory/command_bus.py +70 -0
  11. hexagonal/adapters/drivens/buses/inmemory/event_bus.py +69 -0
  12. hexagonal/adapters/drivens/buses/inmemory/infra.py +49 -0
  13. hexagonal/adapters/drivens/mappers.py +127 -0
  14. hexagonal/adapters/drivens/repository/base/__init__.py +13 -0
  15. hexagonal/adapters/drivens/repository/base/repository.py +85 -0
  16. hexagonal/adapters/drivens/repository/base/unit_of_work.py +75 -0
  17. hexagonal/adapters/drivens/repository/sqlite/__init__.py +18 -0
  18. hexagonal/adapters/drivens/repository/sqlite/datastore.py +197 -0
  19. hexagonal/adapters/drivens/repository/sqlite/env_vars.py +2 -0
  20. hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +20 -0
  21. hexagonal/adapters/drivens/repository/sqlite/outbox.py +405 -0
  22. hexagonal/adapters/drivens/repository/sqlite/repository.py +286 -0
  23. hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +25 -0
  24. hexagonal/adapters/drivers/__init__.py +5 -0
  25. hexagonal/adapters/drivers/app.py +38 -0
  26. hexagonal/application/__init__.py +29 -0
  27. hexagonal/application/api.py +61 -0
  28. hexagonal/application/app.py +76 -0
  29. hexagonal/application/bus_app.py +70 -0
  30. hexagonal/application/handlers.py +107 -0
  31. hexagonal/application/infrastructure.py +64 -0
  32. hexagonal/application/query.py +71 -0
  33. hexagonal/domain/__init__.py +77 -0
  34. hexagonal/domain/aggregate.py +159 -0
  35. hexagonal/domain/base.py +169 -0
  36. hexagonal/domain/exceptions.py +38 -0
  37. hexagonal/entrypoints/__init__.py +4 -0
  38. hexagonal/entrypoints/app.py +53 -0
  39. hexagonal/entrypoints/base.py +105 -0
  40. hexagonal/entrypoints/bus.py +68 -0
  41. hexagonal/entrypoints/sqlite.py +49 -0
  42. hexagonal/ports/__init__.py +0 -0
  43. hexagonal/ports/drivens/__init__.py +43 -0
  44. hexagonal/ports/drivens/application.py +35 -0
  45. hexagonal/ports/drivens/buses.py +148 -0
  46. hexagonal/ports/drivens/infrastructure.py +19 -0
  47. hexagonal/ports/drivens/repository.py +152 -0
  48. hexagonal/ports/drivers/__init__.py +3 -0
  49. hexagonal/ports/drivers/app.py +58 -0
  50. hexagonal/py.typed +0 -0
  51. python_hexagonal-0.1.0.dist-info/METADATA +15 -0
  52. python_hexagonal-0.1.0.dist-info/RECORD +53 -0
  53. python_hexagonal-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,159 @@
1
+ from datetime import datetime
2
+ from typing import (
3
+ Any,
4
+ Generic,
5
+ Self,
6
+ Type,
7
+ TypeVar,
8
+ get_args,
9
+ get_origin,
10
+ )
11
+ from uuid import UUID
12
+
13
+ from eventsourcing.domain import (
14
+ AggregateCreated,
15
+ AggregateEvent,
16
+ BaseAggregate,
17
+ CanSnapshotAggregate,
18
+ MutableOrImmutableAggregate,
19
+ event,
20
+ )
21
+ from eventsourcing.utils import get_topic
22
+ from pydantic import ConfigDict, TypeAdapter
23
+ from uuid6 import uuid7
24
+
25
+ from .base import Inmutable, ValueObject
26
+
27
+ command = event
28
+
29
+
30
+ class IdValueObject(ValueObject[UUID]):
31
+ @classmethod
32
+ def new(cls, *_: Any, **__: Any) -> Self:
33
+ return cls(value=uuid7())
34
+
35
+
36
+ TIdEntity = TypeVar("TIdEntity", bound=IdValueObject)
37
+ datetime_adapter = TypeAdapter(datetime)
38
+
39
+
40
+ class SnapshotState(Inmutable, Generic[TIdEntity]):
41
+ model_config = ConfigDict(extra="allow")
42
+ id: TIdEntity
43
+ created_on: datetime
44
+ modified_on: datetime
45
+
46
+ def __init__(self, **kwargs: Any) -> None:
47
+ for key in ["_created_on", "_modified_on"]:
48
+ kwargs[key.removeprefix("_")] = datetime_adapter.validate_python(
49
+ kwargs[key]
50
+ )
51
+ super().__init__(**kwargs)
52
+
53
+
54
+ TSnapshotState = TypeVar("TSnapshotState", bound=SnapshotState[Any])
55
+
56
+
57
+ class AggregateSnapshot(Inmutable, CanSnapshotAggregate[UUID], Generic[TSnapshotState]):
58
+ originator_id: UUID
59
+ originator_version: int
60
+ timestamp: datetime
61
+ topic: str
62
+ state: TSnapshotState
63
+
64
+ @classmethod
65
+ def take(
66
+ cls,
67
+ aggregate: MutableOrImmutableAggregate[UUID],
68
+ ) -> Self:
69
+ """Creates a snapshot of the given :class:`Aggregate` object."""
70
+ aggregate_state = dict(aggregate.__dict__)
71
+ class_version = getattr(type(aggregate), "class_version", 1)
72
+ if class_version > 1:
73
+ aggregate_state["class_version"] = class_version
74
+ if isinstance(aggregate, AggregateRoot):
75
+ aggregate.complete_snapshot_state(aggregate_state)
76
+ aggregate_state.pop("_id")
77
+ aggregate_state.pop("_version")
78
+ aggregate_state.pop("_pending_events")
79
+ dict_snap = dict(
80
+ originator_id=aggregate.id, # type: ignore[call-arg]
81
+ originator_version=aggregate.version, # pyright: ignore[reportCallIssue]
82
+ timestamp=cls.create_timestamp(), # pyright: ignore[reportCallIssue]
83
+ topic=get_topic(type(aggregate)), # type: ignore[call-arg]
84
+ state=aggregate_state, # pyright: ignore[reportCallIssue]
85
+ )
86
+ return cls.model_validate(dict_snap)
87
+
88
+
89
+ class AggregateRoot(BaseAggregate[UUID], Generic[TIdEntity, TSnapshotState]):
90
+ _id_type: Type[TIdEntity]
91
+
92
+ Snapshot: Type[AggregateSnapshot[TSnapshotState]]
93
+
94
+ class Event(AggregateEvent):
95
+ pass
96
+
97
+ class Created(Event, AggregateCreated):
98
+ pass
99
+
100
+ class Deleted(Event):
101
+ def mutate(self, aggregate: Any) -> Any:
102
+ super().mutate(aggregate)
103
+ return None
104
+
105
+ def __init_subclass__(cls) -> None:
106
+ super().__init_subclass__()
107
+
108
+ # Inspect generic base to find the concrete type argument
109
+ for base in getattr(cls, "__orig_bases__", []):
110
+ origin = get_origin(base)
111
+ if issubclass(origin, AggregateRoot):
112
+ args = get_args(base)
113
+ if args:
114
+ cls._id_type = args[0]
115
+ state_type = args[1]
116
+
117
+ cls.Snapshot = AggregateSnapshot[state_type] # type: ignore[valid-type]
118
+
119
+ @classmethod
120
+ def create_id(cls, *args: Any, **kwargs: Any):
121
+ return cls._id_type.new(*args, **kwargs).value
122
+
123
+ @property
124
+ def value_id(self) -> TIdEntity:
125
+ # Instantiate the captured type using the stored `id` value
126
+ try:
127
+ return self._id_type(value=self.id)
128
+ except Exception as e:
129
+ raise ValueError(
130
+ f"Cannot instantiate {self._id_type} with value {self.id}: {e}"
131
+ ) from e
132
+
133
+ def __eq__(self, other: object) -> bool:
134
+ if not isinstance(other, AggregateRoot):
135
+ return False
136
+ return self.value_id == other.value_id # type: ignore
137
+
138
+ def __hash__(self) -> int:
139
+ return hash(self.value_id)
140
+
141
+ def complete_snapshot_state(self, state: dict[str, Any]) -> dict[str, Any]:
142
+ state["id"] = self.value_id
143
+ return state
144
+
145
+ @property
146
+ def state(self) -> TSnapshotState:
147
+ return self.Snapshot.take(self).state
148
+
149
+ @classmethod
150
+ def reconstruct_from_snapshot(
151
+ cls,
152
+ snapshot: AggregateSnapshot[TSnapshotState],
153
+ ) -> Self:
154
+ agg = snapshot.mutate(None)
155
+ assert isinstance(agg, cls)
156
+ return agg
157
+
158
+ def take_snapshot(self) -> AggregateSnapshot[TSnapshotState]:
159
+ return self.Snapshot.take(self)
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from datetime import datetime
5
+ from typing import Any, ClassVar, Dict, Generic, Optional, Self, Type, TypeVar
6
+ from uuid import UUID
7
+
8
+ from eventsourcing.domain import CanCreateTimestamp
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+ from uuid6 import uuid7
11
+
12
+
13
+ class HasTopic:
14
+ TOPIC: ClassVar[str] = ""
15
+
16
+ def __init_subclass__(cls, *, topic_suffix: Optional[str] = None, **kwargs: Any):
17
+ # Extraemos nuestro argumento para no pasárselo a Pydantic
18
+ super().__init_subclass__(**kwargs) # aquí Pydantic recibe sólo lo suyo
19
+ new_topic = topic_suffix or cls.__name__
20
+
21
+ # Buscamos el topic base más específico en toda la cadena de herencia
22
+ base_topic = ""
23
+ for base in cls.__mro__[1:]: # Empezamos desde el padre más inmediato
24
+ if issubclass(base, HasTopic) and hasattr(base, "TOPIC") and base.TOPIC:
25
+ base_topic = base.TOPIC
26
+ break
27
+ actual_topic = cls.TOPIC
28
+ if actual_topic != base_topic:
29
+ set_topic = actual_topic
30
+ else:
31
+ set_topic = f"{base_topic}{'.' if base_topic != '' else ''}{new_topic}"
32
+ cls.TOPIC = set_topic # type: ignore
33
+
34
+
35
+ class Inmutable(BaseModel):
36
+ model_config = ConfigDict(extra="forbid", frozen=True)
37
+
38
+
39
+ class FactoryMethod(ABC):
40
+ @classmethod
41
+ @abstractmethod
42
+ def new(cls, *_: Any, **__: Any) -> Self:
43
+ raise NotImplementedError
44
+
45
+
46
+ class Message(Inmutable, FactoryMethod):
47
+ message_id: UUID = Field(default_factory=uuid7)
48
+ timestamp: datetime = Field(default_factory=CanCreateTimestamp.create_timestamp)
49
+
50
+
51
+ class DomainEvent(Message, HasTopic): ...
52
+
53
+
54
+ class IntegrationEvent(Message, HasTopic): ...
55
+
56
+
57
+ class Command(Message, HasTopic): ...
58
+
59
+
60
+ TValue = TypeVar("TValue")
61
+
62
+
63
+ class ValueObject(Inmutable, FactoryMethod, Generic[TValue]):
64
+ value: TValue
65
+
66
+ @classmethod
67
+ def new(cls, value: TValue, *_: Any, **__: Any) -> Self:
68
+ return cls(value=value)
69
+
70
+
71
+ TMessagePayload = IntegrationEvent | Command | DomainEvent
72
+
73
+ TMessage = TypeVar("TMessage", bound=TMessagePayload)
74
+
75
+ TMessagePayloadType = TypeVar("TMessagePayloadType", bound=TMessagePayload)
76
+
77
+
78
+ class CloudMessage(Inmutable, FactoryMethod, Generic[TMessage]):
79
+ type: str
80
+ payload: TMessage
81
+ message_id: UUID
82
+ correlation_id: Optional[UUID] = None
83
+ causation_id: Optional[UUID] = None
84
+ occurred_at: datetime
85
+ metadata: Dict[str, Any] = Field(default_factory=dict)
86
+
87
+ @classmethod
88
+ def new(
89
+ cls,
90
+ payload: TMessage,
91
+ *,
92
+ mid: UUID | None = None,
93
+ **kw: Any,
94
+ ) -> "CloudMessage[TMessage]":
95
+ mid = mid or payload.message_id
96
+ return cls(
97
+ type=payload.TOPIC,
98
+ payload=payload,
99
+ occurred_at=payload.timestamp,
100
+ message_id=mid,
101
+ correlation_id=mid,
102
+ metadata=kw,
103
+ )
104
+
105
+ def derive(
106
+ self,
107
+ payload: TMessagePayloadType,
108
+ *,
109
+ mid: UUID | None = None,
110
+ **kw: Any,
111
+ ) -> "CloudMessage[TMessagePayloadType]":
112
+ mid = mid or payload.message_id
113
+ return CloudMessage(
114
+ message_id=mid,
115
+ type=payload.TOPIC,
116
+ payload=payload,
117
+ correlation_id=self.correlation_id,
118
+ causation_id=self.message_id,
119
+ metadata=kw,
120
+ occurred_at=payload.timestamp,
121
+ )
122
+
123
+
124
+ TCommand = TypeVar("TCommand", bound=Command)
125
+ TEvento = DomainEvent | IntegrationEvent
126
+ TEvent = TypeVar("TEvent", bound=TEvento)
127
+
128
+
129
+ class View(Inmutable): ...
130
+
131
+
132
+ TView = TypeVar("TView", bound=ValueObject[Any])
133
+
134
+
135
+ class Query(Inmutable, FactoryMethod, HasTopic, Generic[TView]):
136
+ view: Type[TView]
137
+
138
+
139
+ class QueryResults(Inmutable, Generic[TView], FactoryMethod):
140
+ items: list[TView]
141
+ limit: int
142
+ next_cursor: Optional[str] = None
143
+ prev_cursor: Optional[str] = None
144
+
145
+ def __len__(self):
146
+ return len(self.items)
147
+
148
+ @classmethod
149
+ def new(
150
+ cls,
151
+ items: list[TView],
152
+ limit: int,
153
+ next_cursor: Optional[str] = None,
154
+ prev_cursor: Optional[str] = None,
155
+ ):
156
+ return cls(
157
+ items=items, limit=limit, next_cursor=next_cursor, prev_cursor=prev_cursor
158
+ )
159
+
160
+
161
+ class QueryResult(Inmutable, Generic[TView], FactoryMethod):
162
+ item: TView
163
+
164
+ @classmethod
165
+ def new(cls, item: TView):
166
+ return cls(item=item)
167
+
168
+
169
+ TQuery = TypeVar("TQuery", bound=Query[Any], contravariant=True)
@@ -0,0 +1,38 @@
1
+ class DomainException(Exception):
2
+ def __init__(self, message: str = ""):
3
+ self.message = f"Domain Exception: {message}"
4
+
5
+
6
+ class DomainValueError(DomainException, ValueError):
7
+ def __init__(self, message: str = ""):
8
+ self.message = f"Domain Value Error: {message}"
9
+
10
+
11
+ class AggregateNotFound(DomainException):
12
+ def __init__(self, message: str = ""):
13
+ self.message = f"Aggregate Not Found Exception: {message}"
14
+
15
+
16
+ class AggregateVersionMismatch(DomainException):
17
+ def __init__(self, message: str = ""):
18
+ self.message = f"Aggregate Version Mismatch Exception: {message}"
19
+
20
+
21
+ class HandlerNotRegistered(DomainException):
22
+ def __init__(self, message: str = ""):
23
+ self.message = f"Handler Not Registered Exception: {message}"
24
+
25
+
26
+ class HandlerNotFound(DomainException):
27
+ def __init__(self, message: str = ""):
28
+ self.message = f"Handler Not Found Exception: {message}"
29
+
30
+
31
+ class HandlerAlreadyRegistered(DomainException):
32
+ def __init__(self, message: str = ""):
33
+ self.message = f"Handler Already Registered Exception: {message}"
34
+
35
+
36
+ class InfrastructureNotInitialized(DomainException, RuntimeError):
37
+ def __init__(self, message: str = ""):
38
+ self.message = f"Infrastructure Not Initialized Exception: {message}"
@@ -0,0 +1,4 @@
1
+ from .app import AppEntrypoint
2
+ from .base import Entrypoint, EntrypointGroup
3
+
4
+ __all__ = ["AppEntrypoint", "Entrypoint", "EntrypointGroup"]
@@ -0,0 +1,53 @@
1
+ # pyright: reportMissingParameterType=none, reportGeneralTypeIssues=none
2
+
3
+ from typing import Type
4
+
5
+ from hexagonal.adapters.drivers import ApplicationProxyAdapter
6
+ from hexagonal.application import Application
7
+ from hexagonal.ports.drivens import IPairInboxOutbox, TManager
8
+ from hexagonal.ports.drivers import IBaseApplication, IBusApp
9
+
10
+ from .base import Entrypoint
11
+ from .bus import BusEntrypoint, BusEntrypointGroup
12
+
13
+
14
+ class AppEntrypoint(Entrypoint[IBaseApplication[TManager]]):
15
+ name = "AppEntrypoint"
16
+ BUS_APP: Type[Entrypoint[IBusApp[TManager]]]
17
+ OUTBOX: Type[Entrypoint[IPairInboxOutbox[TManager]]]
18
+ BUS_INFRASTRUCTURE: Type[BusEntrypoint[TManager]]
19
+ BUS_GROUP: Type[BusEntrypointGroup[TManager]] = BusEntrypointGroup[TManager]
20
+
21
+ @classmethod
22
+ def setBusApp(cls, bus_app: Type[Entrypoint[IBusApp[TManager]]]):
23
+ cls.BUS_APP = bus_app
24
+
25
+ @classmethod
26
+ def setOutbox(cls, outbox: Type[Entrypoint[IPairInboxOutbox[TManager]]]):
27
+ cls.OUTBOX = outbox
28
+
29
+ @classmethod
30
+ def setBusInfrastructure(cls, bus_infrastructure: Type[BusEntrypoint[TManager]]):
31
+ cls.BUS_INFRASTRUCTURE = bus_infrastructure
32
+
33
+ @classmethod
34
+ def setBusEntrypointGroup(cls, bus_group: Type[BusEntrypointGroup[TManager]]):
35
+ cls.BUS_GROUP = bus_group
36
+
37
+ @classmethod
38
+ def get(cls, env=None) -> IBaseApplication[TManager]:
39
+ env = cls.construct_env(env)
40
+ if not hasattr(cls, "BUS_APP"):
41
+ raise ValueError("Bus app is not configured")
42
+ if not hasattr(cls, "OUTBOX"):
43
+ raise ValueError("Outbox is not configured")
44
+ if not hasattr(cls, "BUS_INFRASTRUCTURE"):
45
+ cls.setBusInfrastructure(cls.BUS_GROUP.getEntrypoint(env))
46
+
47
+ cls.BUS_INFRASTRUCTURE.setOutbox(cls.OUTBOX)
48
+ GeneralBusEntrypoint = cls.BUS_INFRASTRUCTURE
49
+
50
+ bus_app = cls.BUS_APP.get(env)
51
+ bus_infrastructure = GeneralBusEntrypoint.get(env)
52
+ app = Application(bus_app, bus_infrastructure)
53
+ return ApplicationProxyAdapter(app)
@@ -0,0 +1,105 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+ from typing import (
4
+ Any,
5
+ Callable,
6
+ ClassVar,
7
+ Dict,
8
+ Generic,
9
+ List,
10
+ Mapping,
11
+ Optional,
12
+ Type,
13
+ TypeVar,
14
+ )
15
+
16
+ from eventsourcing.utils import Environment
17
+
18
+ T = TypeVar("T", covariant=True)
19
+
20
+
21
+ class Entrypoint(ABC, Generic[T]):
22
+ env: ClassVar[Mapping[str, str]] = {}
23
+ name: ClassVar[str]
24
+
25
+ @classmethod
26
+ def setName(cls, name: str | None = None):
27
+ if hasattr(cls, "name"):
28
+ return
29
+ cls.name = name or cls.__name__
30
+
31
+ @classmethod
32
+ @abstractmethod
33
+ def get(cls, env: Optional[Mapping[str, str]] = None) -> T: ...
34
+
35
+ @classmethod
36
+ def strategy(cls) -> Dict[str, Callable[[Optional[Mapping[str, str]]], T]]:
37
+ cls.setName()
38
+ return {cls.name: cls.get}
39
+
40
+ @classmethod
41
+ def construct_env(cls, env: Optional[Mapping[str, str]] = None) -> Environment:
42
+ """Constructs environment from which application will be configured."""
43
+ cls.setName()
44
+ _env = dict(cls.env)
45
+ _env.update(os.environ)
46
+ if env is not None:
47
+ _env.update(env)
48
+ return Environment(cls.name, _env)
49
+
50
+
51
+ class EntrypointGroup(Entrypoint[T]):
52
+ env_key: ClassVar[str]
53
+ entrypoints: List[Type[Entrypoint[T]]]
54
+
55
+ def __init_subclass__(
56
+ cls,
57
+ **kwargs: Any,
58
+ ):
59
+ super().__init_subclass__(**kwargs)
60
+ cls.name = getattr(cls, "name", cls.__name__)
61
+ env_key = getattr(cls, "env_key", None)
62
+ if not env_key:
63
+ raise ValueError("env_key is required")
64
+ entrypoints = getattr(cls, "entrypoints", None)
65
+ if not entrypoints:
66
+ raise ValueError("entrypoints is required")
67
+ cls.env_key = env_key
68
+ cls.entrypoints = entrypoints
69
+
70
+ @classmethod
71
+ def addEntrypoint(cls, entrypoint: Type[Entrypoint[T]]):
72
+ cls.entrypoints.append(entrypoint)
73
+
74
+ @classmethod
75
+ def strategy(cls) -> Dict[str, Callable[[Optional[Mapping[str, str]]], T]]:
76
+ strategies: Dict[str, Callable[[Optional[Mapping[str, str]]], T]] = {}
77
+ for entrypoint in cls.entrypoints:
78
+ strategies.update(entrypoint.strategy())
79
+ return strategies
80
+
81
+ @classmethod
82
+ def getEntrypoint(
83
+ cls, env: Optional[Mapping[str, str]] = None
84
+ ) -> Type[Entrypoint[T]]:
85
+ env = cls.construct_env(env)
86
+ entrypoint_name = env.get(cls.env_key)
87
+ if entrypoint_name is None:
88
+ raise ValueError(f"Missing entrypoint name: {cls.env_key}")
89
+ entrypoint = next(
90
+ (ep for ep in cls.entrypoints if ep.name == entrypoint_name), None
91
+ )
92
+ if entrypoint is None:
93
+ raise ValueError(f"Unknown entrypoint: {entrypoint_name}")
94
+ return entrypoint
95
+
96
+ @classmethod
97
+ def get(cls, env: Optional[Mapping[str, str]] = None) -> T:
98
+ env = cls.construct_env(env)
99
+ strategy = cls.strategy()
100
+ entrypoint_name = env.get(cls.env_key)
101
+ if entrypoint_name is None:
102
+ raise ValueError(f"Missing entrypoint name: {cls.env_key}")
103
+ if entrypoint_name not in strategy:
104
+ raise ValueError(f"Unknown entrypoint: {entrypoint_name}")
105
+ return strategy[entrypoint_name](env)
@@ -0,0 +1,68 @@
1
+ import logging
2
+ from abc import ABC
3
+ from typing import Mapping, Optional, Type, cast
4
+
5
+ from hexagonal.adapters.drivens.buses.inmemory import (
6
+ InMemoryBusInfrastructure,
7
+ InMemoryQueueBusInfrastructure,
8
+ )
9
+ from hexagonal.entrypoints.base import Entrypoint, EntrypointGroup
10
+ from hexagonal.ports.drivens import (
11
+ IBusInfrastructure,
12
+ IPairInboxOutbox,
13
+ TManager,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class BusEntrypoint(Entrypoint[IBusInfrastructure[TManager]], ABC):
20
+ env = {"ONLY_INTEGRATION": "False"}
21
+ OUTBOX: Type[Entrypoint[IPairInboxOutbox[TManager]]]
22
+
23
+ @classmethod
24
+ def setOutbox(cls, outbox: Type[Entrypoint[IPairInboxOutbox[TManager]]]):
25
+ cls.OUTBOX = outbox
26
+
27
+
28
+ class InMemoryBusEntrypoint(BusEntrypoint[TManager]):
29
+ name = "inmemory"
30
+
31
+ @classmethod
32
+ def get(cls, env: Optional[Mapping[str, str]] = None):
33
+ env = cls.construct_env(env)
34
+ outbox = cls.OUTBOX.get(env)
35
+ logger.info("Using %s for events", type(outbox).__name__)
36
+ buses = InMemoryBusInfrastructure(outbox.inbox, outbox.outbox)
37
+ buses.initialize(env)
38
+ return buses
39
+
40
+
41
+ class InMemoryQueueBusEntrypoint(BusEntrypoint[TManager]):
42
+ name = "inmemory_queue"
43
+
44
+ @classmethod
45
+ def get(cls, env: Optional[Mapping[str, str]] = None):
46
+ env = cls.construct_env(env)
47
+ outbox = cls.OUTBOX.get(env)
48
+ logger.info("Using %s for events", type(outbox).__name__)
49
+ buses = InMemoryQueueBusInfrastructure(outbox.inbox, outbox.outbox)
50
+ buses.initialize(env)
51
+ return buses
52
+
53
+
54
+ class BusEntrypointGroup(EntrypointGroup[IBusInfrastructure[TManager]]):
55
+ env_key = "ENV_BUS"
56
+ entrypoints = [
57
+ InMemoryBusEntrypoint[TManager],
58
+ InMemoryQueueBusEntrypoint[TManager],
59
+ ]
60
+ env = {"ENV_BUS": "inmemory_queue"}
61
+
62
+ @classmethod
63
+ def getEntrypoint(
64
+ cls, env: Optional[Mapping[str, str]] = None
65
+ ) -> Type[BusEntrypoint[TManager]]:
66
+ entry = super().getEntrypoint(env)
67
+
68
+ return cast(Type[BusEntrypoint[TManager]], entry)
@@ -0,0 +1,49 @@
1
+ from typing import Mapping, Optional
2
+
3
+ from eventsourcing.compressor import ZlibCompressor
4
+ from eventsourcing.utils import strtobool
5
+
6
+ from hexagonal.adapters.drivens.mappers import MessageMapper, OrjsonTranscoder
7
+ from hexagonal.adapters.drivens.repository.sqlite import (
8
+ SQLiteConnectionContextManager,
9
+ SQLiteDatastore,
10
+ SQLiteInfrastructure,
11
+ SQLitePairInboxOutbox,
12
+ )
13
+ from hexagonal.entrypoints import AppEntrypoint, Entrypoint
14
+
15
+
16
+ class SQLiteInfrastructureEntrypoint(Entrypoint[SQLiteInfrastructure]):
17
+ @classmethod
18
+ def get(cls, env: Optional[Mapping[str, str]] = None):
19
+ env = cls.construct_env(env)
20
+ db_path = env.get("SQLITE_DB_PATH")
21
+ if db_path is None:
22
+ raise ValueError("Database configuration is missing")
23
+ check_same_thread = strtobool(env.get("SQLITE_CHECK_SAME_THREAD", "True"))
24
+ datastore = SQLiteDatastore(
25
+ db_path,
26
+ check_same_thread=check_same_thread,
27
+ )
28
+ mapper = MessageMapper(
29
+ transcoder=OrjsonTranscoder(), compressor=ZlibCompressor()
30
+ )
31
+ infrastructure = SQLiteInfrastructure(mapper, datastore)
32
+ infrastructure.initialize(env)
33
+ return infrastructure
34
+
35
+
36
+ class SQLiteOutboxEntrypoint(Entrypoint[SQLitePairInboxOutbox]):
37
+ @classmethod
38
+ def get(cls, env: Optional[Mapping[str, str]] = None):
39
+ env = cls.construct_env(env)
40
+ infrastructure = SQLiteInfrastructureEntrypoint.get(env)
41
+ pair = SQLitePairInboxOutbox(
42
+ infrastructure.mapper, infrastructure.connection_manager
43
+ )
44
+ pair.initialize(env)
45
+ return pair
46
+
47
+
48
+ class SQLiteAppEntrypoint(AppEntrypoint[SQLiteConnectionContextManager]):
49
+ OUTBOX = SQLiteOutboxEntrypoint
File without changes
@@ -0,0 +1,43 @@
1
+ from .application import IMessageHandler, IQueryHandler, IUseCase
2
+ from .buses import (
3
+ IBaseMessageBus,
4
+ IBusInfrastructure,
5
+ ICommandBus,
6
+ IEventBus,
7
+ IQueryBus,
8
+ )
9
+ from .infrastructure import IBaseInfrastructure
10
+ from .repository import (
11
+ IAggregateRepository,
12
+ IBaseRepository,
13
+ IConnectionManager,
14
+ IInboxRepository,
15
+ IOutboxRepository,
16
+ IPairInboxOutbox,
17
+ ISearchRepository,
18
+ IUnitOfWork,
19
+ TAggregate,
20
+ TManager,
21
+ )
22
+
23
+ __all__ = [
24
+ "IBaseInfrastructure",
25
+ "IBaseMessageBus",
26
+ "ICommandBus",
27
+ "IEventBus",
28
+ "IQueryBus",
29
+ "IBusInfrastructure",
30
+ "IInboxRepository",
31
+ "IOutboxRepository",
32
+ "IAggregateRepository",
33
+ "IBaseRepository",
34
+ "ISearchRepository",
35
+ "IConnectionManager",
36
+ "IUnitOfWork",
37
+ "IMessageHandler",
38
+ "IQueryHandler",
39
+ "TManager",
40
+ "IUseCase",
41
+ "TAggregate",
42
+ "IPairInboxOutbox",
43
+ ]