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.
- hexagonal/__init__.py +2 -0
- hexagonal/adapters/drivens/buses/base/__init__.py +15 -0
- hexagonal/adapters/drivens/buses/base/command_bus.py +69 -0
- hexagonal/adapters/drivens/buses/base/event_bus.py +160 -0
- hexagonal/adapters/drivens/buses/base/infrastructure.py +38 -0
- hexagonal/adapters/drivens/buses/base/message_bus.py +73 -0
- hexagonal/adapters/drivens/buses/base/query.py +82 -0
- hexagonal/adapters/drivens/buses/base/utils.py +1 -0
- hexagonal/adapters/drivens/buses/inmemory/__init__.py +12 -0
- hexagonal/adapters/drivens/buses/inmemory/command_bus.py +70 -0
- hexagonal/adapters/drivens/buses/inmemory/event_bus.py +69 -0
- hexagonal/adapters/drivens/buses/inmemory/infra.py +49 -0
- hexagonal/adapters/drivens/mappers.py +127 -0
- hexagonal/adapters/drivens/repository/base/__init__.py +13 -0
- hexagonal/adapters/drivens/repository/base/repository.py +85 -0
- hexagonal/adapters/drivens/repository/base/unit_of_work.py +75 -0
- hexagonal/adapters/drivens/repository/sqlite/__init__.py +18 -0
- hexagonal/adapters/drivens/repository/sqlite/datastore.py +197 -0
- hexagonal/adapters/drivens/repository/sqlite/env_vars.py +2 -0
- hexagonal/adapters/drivens/repository/sqlite/infrastructure.py +20 -0
- hexagonal/adapters/drivens/repository/sqlite/outbox.py +405 -0
- hexagonal/adapters/drivens/repository/sqlite/repository.py +286 -0
- hexagonal/adapters/drivens/repository/sqlite/unit_of_work.py +25 -0
- hexagonal/adapters/drivers/__init__.py +5 -0
- hexagonal/adapters/drivers/app.py +38 -0
- hexagonal/application/__init__.py +29 -0
- hexagonal/application/api.py +61 -0
- hexagonal/application/app.py +76 -0
- hexagonal/application/bus_app.py +70 -0
- hexagonal/application/handlers.py +107 -0
- hexagonal/application/infrastructure.py +64 -0
- hexagonal/application/query.py +71 -0
- hexagonal/domain/__init__.py +77 -0
- hexagonal/domain/aggregate.py +159 -0
- hexagonal/domain/base.py +169 -0
- hexagonal/domain/exceptions.py +38 -0
- hexagonal/entrypoints/__init__.py +4 -0
- hexagonal/entrypoints/app.py +53 -0
- hexagonal/entrypoints/base.py +105 -0
- hexagonal/entrypoints/bus.py +68 -0
- hexagonal/entrypoints/sqlite.py +49 -0
- hexagonal/ports/__init__.py +0 -0
- hexagonal/ports/drivens/__init__.py +43 -0
- hexagonal/ports/drivens/application.py +35 -0
- hexagonal/ports/drivens/buses.py +148 -0
- hexagonal/ports/drivens/infrastructure.py +19 -0
- hexagonal/ports/drivens/repository.py +152 -0
- hexagonal/ports/drivers/__init__.py +3 -0
- hexagonal/ports/drivers/app.py +58 -0
- hexagonal/py.typed +0 -0
- python_hexagonal-0.1.0.dist-info/METADATA +15 -0
- python_hexagonal-0.1.0.dist-info/RECORD +53 -0
- 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)
|
hexagonal/domain/base.py
ADDED
|
@@ -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,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
|
+
]
|