python-hexagonal 0.1.2__py3-none-any.whl → 0.2.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/adapters/drivens/buses/base/query.py +15 -5
- hexagonal/adapters/drivens/buses/inmemory/event_bus.py +5 -2
- hexagonal/adapters/drivens/mappers.py +2 -0
- hexagonal/adapters/drivens/repository/base/__init__.py +4 -2
- hexagonal/adapters/drivens/repository/base/repository.py +35 -5
- hexagonal/adapters/drivens/repository/sqlalchemy/__init__.py +7 -1
- hexagonal/adapters/drivens/repository/sqlalchemy/models.py +9 -5
- hexagonal/adapters/drivens/repository/sqlalchemy/outbox.py +1 -1
- hexagonal/adapters/drivens/repository/sqlalchemy/repository.py +123 -5
- hexagonal/application/__init__.py +24 -3
- hexagonal/application/api.py +168 -19
- hexagonal/application/handlers.py +40 -11
- hexagonal/application/query.py +40 -26
- hexagonal/application/topics.py +45 -0
- hexagonal/domain/__init__.py +17 -0
- hexagonal/domain/aggregate.py +26 -10
- hexagonal/domain/base.py +10 -2
- hexagonal/domain/queries.py +17 -0
- hexagonal/entrypoints/sqlalchemy.py +6 -8
- hexagonal/integrations/__init__.py +1 -0
- hexagonal/integrations/sqlalchemy.py +49 -0
- hexagonal/ports/drivens/__init__.py +4 -0
- hexagonal/ports/drivens/buses.py +14 -1
- hexagonal/ports/drivens/repository.py +27 -5
- python_hexagonal-0.2.0.dist-info/METADATA +90 -0
- {python_hexagonal-0.1.2.dist-info → python_hexagonal-0.2.0.dist-info}/RECORD +27 -23
- {python_hexagonal-0.1.2.dist-info → python_hexagonal-0.2.0.dist-info}/WHEEL +1 -1
- python_hexagonal-0.1.2.dist-info/METADATA +0 -16
|
@@ -7,6 +7,8 @@ from hexagonal.domain import (
|
|
|
7
7
|
HandlerAlreadyRegistered,
|
|
8
8
|
HandlerNotRegistered,
|
|
9
9
|
Query,
|
|
10
|
+
QueryBase,
|
|
11
|
+
QueryOne,
|
|
10
12
|
QueryResult,
|
|
11
13
|
QueryResults,
|
|
12
14
|
TQuery,
|
|
@@ -22,12 +24,12 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
22
24
|
self.handlers = {}
|
|
23
25
|
super().initialize(env)
|
|
24
26
|
|
|
25
|
-
def _get_name(self, query_type: Type[
|
|
27
|
+
def _get_name(self, query_type: Type[QueryBase[TView]]) -> str:
|
|
26
28
|
return get_topic(query_type)
|
|
27
29
|
|
|
28
30
|
def _get_handler(
|
|
29
|
-
self, query:
|
|
30
|
-
) -> IQueryHandler[TManager,
|
|
31
|
+
self, query: QueryBase[TView]
|
|
32
|
+
) -> IQueryHandler[TManager, QueryBase[TView], TView] | None:
|
|
31
33
|
name = self._get_name(query.__class__)
|
|
32
34
|
return self.handlers.get(name)
|
|
33
35
|
|
|
@@ -50,6 +52,14 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
50
52
|
else:
|
|
51
53
|
raise HandlerNotRegistered(f"Query: {name}")
|
|
52
54
|
|
|
55
|
+
@overload
|
|
56
|
+
def get(
|
|
57
|
+
self,
|
|
58
|
+
query: QueryOne[TView],
|
|
59
|
+
*,
|
|
60
|
+
one: bool = False,
|
|
61
|
+
) -> QueryResult[TView]: ...
|
|
62
|
+
|
|
53
63
|
@overload
|
|
54
64
|
def get(self, query: Query[TView], *, one: Literal[True]) -> QueryResult[TView]: ...
|
|
55
65
|
|
|
@@ -63,7 +73,7 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
63
73
|
|
|
64
74
|
def get(
|
|
65
75
|
self,
|
|
66
|
-
query: Query[TView],
|
|
76
|
+
query: Query[TView] | QueryOne[TView],
|
|
67
77
|
*,
|
|
68
78
|
one: bool = False,
|
|
69
79
|
) -> QueryResult[TView] | QueryResults[TView]:
|
|
@@ -73,7 +83,7 @@ class QueryBus(IQueryBus[TManager], Infrastructure):
|
|
|
73
83
|
if not handler:
|
|
74
84
|
raise HandlerNotRegistered(f"Query: {name}")
|
|
75
85
|
results = handler.get(query)
|
|
76
|
-
if not one:
|
|
86
|
+
if not (one or isinstance(query, QueryOne)):
|
|
77
87
|
return results
|
|
78
88
|
if len(results) == 0:
|
|
79
89
|
raise ValueError("No results found")
|
|
@@ -5,6 +5,8 @@ import threading
|
|
|
5
5
|
from queue import Empty, Queue
|
|
6
6
|
from typing import Mapping
|
|
7
7
|
|
|
8
|
+
from eventsourcing.utils import strtobool
|
|
9
|
+
|
|
8
10
|
from hexagonal.adapters.drivens.buses.base import BaseEventBus
|
|
9
11
|
from hexagonal.domain import CloudMessage, TEvent, TEvento
|
|
10
12
|
from hexagonal.ports.drivens import TManager
|
|
@@ -28,7 +30,8 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
28
30
|
def initialize(self, env: Mapping[str, str]) -> None:
|
|
29
31
|
self.queue: Queue[CloudMessage[TEvento]] = Queue() # o Queue(maxsize=...)
|
|
30
32
|
self._stop = threading.Event()
|
|
31
|
-
|
|
33
|
+
daemon = strtobool(env.get("EVENT_BUS_WORKER_DAEMON", "true"))
|
|
34
|
+
self._worker = threading.Thread(target=self._worker_loop, daemon=daemon)
|
|
32
35
|
|
|
33
36
|
super().initialize(env)
|
|
34
37
|
|
|
@@ -41,8 +44,8 @@ class InMemoryQueueEventBus(BaseEventBus[TManager]):
|
|
|
41
44
|
return self._publish_messages(*events)
|
|
42
45
|
|
|
43
46
|
def _publish_message(self, message: CloudMessage[TEvento]) -> None:
|
|
44
|
-
super()._publish_message(message)
|
|
45
47
|
self.verify()
|
|
48
|
+
super()._publish_message(message)
|
|
46
49
|
self.queue.put(message)
|
|
47
50
|
# No llamamos consume() aquí: el worker se encarga.
|
|
48
51
|
|
|
@@ -123,6 +123,8 @@ class MessageMapper(Mapper[UUID]):
|
|
|
123
123
|
def default_orjson_value_serializer(obj: Any) -> Any:
|
|
124
124
|
if isinstance(obj, (UUID, Decimal)):
|
|
125
125
|
return str(obj)
|
|
126
|
+
if isinstance(obj, Inmutable):
|
|
127
|
+
return obj.model_dump(mode="json")
|
|
126
128
|
raise TypeError
|
|
127
129
|
|
|
128
130
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from .repository import (
|
|
2
2
|
BaseAggregateRepositoryAdapter,
|
|
3
|
+
BaseEntityRepositoryAdapter,
|
|
3
4
|
BaseRepositoryAdapter,
|
|
4
|
-
|
|
5
|
+
BaseSearchRepositoryAdapter,
|
|
5
6
|
)
|
|
6
7
|
from .unit_of_work import BaseUnitOfWork
|
|
7
8
|
|
|
@@ -9,5 +10,6 @@ __all__ = [
|
|
|
9
10
|
"BaseRepositoryAdapter",
|
|
10
11
|
"BaseAggregateRepositoryAdapter",
|
|
11
12
|
"BaseUnitOfWork",
|
|
12
|
-
"
|
|
13
|
+
"BaseEntityRepositoryAdapter",
|
|
14
|
+
"BaseSearchRepositoryAdapter",
|
|
13
15
|
]
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# pyright: reportMissingTypeStubs=false, reportUnknownArgumentType=false, reportMissingParameterType=none, reportGeneralTypeIssues=none
|
|
2
2
|
|
|
3
3
|
from typing import (
|
|
4
|
-
Any,
|
|
5
4
|
ClassVar,
|
|
6
5
|
Dict,
|
|
7
6
|
Mapping,
|
|
8
7
|
Type,
|
|
9
|
-
TypeVar,
|
|
10
8
|
get_args,
|
|
11
9
|
get_origin,
|
|
12
10
|
)
|
|
@@ -16,16 +14,16 @@ from eventsourcing.persistence import Mapper
|
|
|
16
14
|
from eventsourcing.utils import Environment
|
|
17
15
|
|
|
18
16
|
from hexagonal.application import Infrastructure
|
|
19
|
-
from hexagonal.domain import
|
|
17
|
+
from hexagonal.domain import TAggregate, TEntity, TIdEntity, TQuery, TView
|
|
20
18
|
from hexagonal.ports.drivens import (
|
|
21
19
|
IAggregateRepository,
|
|
22
20
|
IBaseRepository,
|
|
21
|
+
IEntityRepository,
|
|
22
|
+
ISearchRepository,
|
|
23
23
|
IUnitOfWork,
|
|
24
24
|
TManager,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
TAggregate = TypeVar("TAggregate", bound=AggregateRoot[Any, Any])
|
|
28
|
-
|
|
29
27
|
|
|
30
28
|
class BaseRepositoryAdapter(IBaseRepository[TManager], Infrastructure):
|
|
31
29
|
ENV: ClassVar[Dict[str, str]] = {}
|
|
@@ -83,3 +81,35 @@ class BaseAggregateRepositoryAdapter(
|
|
|
83
81
|
@property
|
|
84
82
|
def aggregate_name(self) -> str:
|
|
85
83
|
return self._type_of_aggregate.__name__
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class BaseEntityRepositoryAdapter(
|
|
87
|
+
BaseRepositoryAdapter[TManager],
|
|
88
|
+
IEntityRepository[TManager, TEntity, TIdEntity],
|
|
89
|
+
):
|
|
90
|
+
_type_of_entity: Type[TEntity]
|
|
91
|
+
|
|
92
|
+
def __init_subclass__(cls) -> None:
|
|
93
|
+
super().__init_subclass__()
|
|
94
|
+
# Inspect generic base to find the concrete type argument
|
|
95
|
+
for base in getattr(cls, "__orig_bases__", []):
|
|
96
|
+
origin = get_origin(base)
|
|
97
|
+
if origin and issubclass(origin, BaseEntityRepositoryAdapter):
|
|
98
|
+
args = get_args(base)
|
|
99
|
+
if args:
|
|
100
|
+
cls._type_of_entity = args[0]
|
|
101
|
+
cls.NAME = cls._type_of_entity.__name__.upper()
|
|
102
|
+
|
|
103
|
+
def __init__(self, mapper: Mapper[UUID], connection_manager: TManager):
|
|
104
|
+
super().__init__(connection_manager)
|
|
105
|
+
self._mapper = mapper
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def entity_name(self) -> str:
|
|
109
|
+
return self._type_of_entity.__name__
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class BaseSearchRepositoryAdapter(
|
|
113
|
+
BaseRepositoryAdapter[TManager],
|
|
114
|
+
ISearchRepository[TManager, TQuery, TView],
|
|
115
|
+
): ...
|
|
@@ -12,7 +12,11 @@ from .outbox import (
|
|
|
12
12
|
SQLAlchemyOutboxRepository,
|
|
13
13
|
SQLAlchemyPairInboxOutbox,
|
|
14
14
|
)
|
|
15
|
-
from .repository import
|
|
15
|
+
from .repository import (
|
|
16
|
+
SQLAlchemyEntityRepositoryAdapter,
|
|
17
|
+
SQLAlchemyRepositoryAdapter,
|
|
18
|
+
SQLAlchemySearchRepositoryAdapter,
|
|
19
|
+
)
|
|
16
20
|
from .unit_of_work import SQLAlchemyUnitOfWork
|
|
17
21
|
|
|
18
22
|
__all__ = [
|
|
@@ -24,4 +28,6 @@ __all__ = [
|
|
|
24
28
|
"SQLAlchemyInboxRepository",
|
|
25
29
|
"SQLAlchemyInfrastructure",
|
|
26
30
|
"SQLAlchemyPairInboxOutbox",
|
|
31
|
+
"SQLAlchemyEntityRepositoryAdapter",
|
|
32
|
+
"SQLAlchemySearchRepositoryAdapter",
|
|
27
33
|
]
|
|
@@ -19,7 +19,7 @@ metadata = MetaData()
|
|
|
19
19
|
|
|
20
20
|
def _get_table_key(table_name: str, schema: str | None = None) -> str:
|
|
21
21
|
"""Get the key used by SQLAlchemy metadata to store the table."""
|
|
22
|
-
if schema:
|
|
22
|
+
if schema and schema != "":
|
|
23
23
|
return f"{schema}.{table_name}"
|
|
24
24
|
return table_name
|
|
25
25
|
|
|
@@ -34,6 +34,7 @@ def create_aggregates_table(table_name: str, schema: str | None = None) -> Table
|
|
|
34
34
|
Returns:
|
|
35
35
|
SQLAlchemy Table object for aggregate snapshots
|
|
36
36
|
"""
|
|
37
|
+
schema = schema if schema and schema != "" else None
|
|
37
38
|
full_table_name = f"aggregates_{table_name}"
|
|
38
39
|
table_key = _get_table_key(full_table_name, schema)
|
|
39
40
|
|
|
@@ -45,7 +46,7 @@ def create_aggregates_table(table_name: str, schema: str | None = None) -> Table
|
|
|
45
46
|
full_table_name,
|
|
46
47
|
metadata,
|
|
47
48
|
Column("originator_id", String(36), nullable=False),
|
|
48
|
-
Column("aggregate_name",
|
|
49
|
+
Column("aggregate_name", String(255), nullable=False),
|
|
49
50
|
Column("originator_version", Integer, nullable=False),
|
|
50
51
|
Column("topic", Text, nullable=False),
|
|
51
52
|
Column("state", LargeBinary, nullable=False),
|
|
@@ -66,6 +67,7 @@ def create_events_table(table_name: str, schema: str | None = None) -> Table:
|
|
|
66
67
|
Returns:
|
|
67
68
|
SQLAlchemy Table object for aggregate events history
|
|
68
69
|
"""
|
|
70
|
+
schema = schema if schema and schema != "" else None
|
|
69
71
|
full_table_name = f"aggregates_{table_name}_events"
|
|
70
72
|
table_key = _get_table_key(full_table_name, schema)
|
|
71
73
|
|
|
@@ -77,7 +79,7 @@ def create_events_table(table_name: str, schema: str | None = None) -> Table:
|
|
|
77
79
|
full_table_name,
|
|
78
80
|
metadata,
|
|
79
81
|
Column("originator_id", String(36), nullable=False),
|
|
80
|
-
Column("aggregate_name",
|
|
82
|
+
Column("aggregate_name", String(255), nullable=False),
|
|
81
83
|
Column("originator_version", Integer, nullable=False),
|
|
82
84
|
Column("topic", Text, nullable=False),
|
|
83
85
|
Column("state", LargeBinary, nullable=False),
|
|
@@ -97,6 +99,7 @@ def create_outbox_table(table_name: str = "outbox", schema: str | None = None) -
|
|
|
97
99
|
Returns:
|
|
98
100
|
SQLAlchemy Table object for outbox messages
|
|
99
101
|
"""
|
|
102
|
+
schema = schema if schema and schema != "" else None
|
|
100
103
|
table_key = _get_table_key(table_name, schema)
|
|
101
104
|
|
|
102
105
|
# Return existing table if already defined
|
|
@@ -107,7 +110,7 @@ def create_outbox_table(table_name: str = "outbox", schema: str | None = None) -
|
|
|
107
110
|
table_name,
|
|
108
111
|
metadata,
|
|
109
112
|
Column("message_id", String(36), primary_key=True),
|
|
110
|
-
Column("topic",
|
|
113
|
+
Column("topic", String(255), nullable=False),
|
|
111
114
|
Column("message", LargeBinary, nullable=False),
|
|
112
115
|
Column("published_at", DateTime(timezone=True), nullable=True),
|
|
113
116
|
Column("failed_at", DateTime(timezone=True), nullable=True),
|
|
@@ -134,6 +137,7 @@ def create_inbox_table(table_name: str = "inbox", schema: str | None = None) ->
|
|
|
134
137
|
Returns:
|
|
135
138
|
SQLAlchemy Table object for inbox messages
|
|
136
139
|
"""
|
|
140
|
+
schema = schema if schema and schema != "" else None
|
|
137
141
|
table_key = _get_table_key(table_name, schema)
|
|
138
142
|
|
|
139
143
|
# Return existing table if already defined
|
|
@@ -144,7 +148,7 @@ def create_inbox_table(table_name: str = "inbox", schema: str | None = None) ->
|
|
|
144
148
|
table_name,
|
|
145
149
|
metadata,
|
|
146
150
|
Column("message_id", String(36), nullable=False),
|
|
147
|
-
Column("handler",
|
|
151
|
+
Column("handler", String(255), nullable=False),
|
|
148
152
|
Column("received_at", DateTime(timezone=True), nullable=False),
|
|
149
153
|
Column("processed_at", DateTime(timezone=True), nullable=True),
|
|
150
154
|
Column("error", Text, nullable=True),
|
|
@@ -313,7 +313,7 @@ class SQLAlchemyInboxRepository(
|
|
|
313
313
|
|
|
314
314
|
def _cursor(self):
|
|
315
315
|
"""Get the current connection for executing statements."""
|
|
316
|
-
return self.
|
|
316
|
+
return self.connection_manager.cursor()
|
|
317
317
|
|
|
318
318
|
def create_tables(self) -> None:
|
|
319
319
|
"""Create the inbox table if it doesn't exist."""
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from typing import Any, ClassVar, Dict, Mapping, Sequence, Tuple, cast
|
|
7
8
|
from uuid import UUID
|
|
8
9
|
|
|
9
10
|
from eventsourcing.domain import CanMutateAggregate
|
|
@@ -11,24 +12,104 @@ from eventsourcing.persistence import StoredEvent
|
|
|
11
12
|
from eventsourcing.utils import strtobool
|
|
12
13
|
from sqlalchemy import Connection, delete, insert, select, update
|
|
13
14
|
|
|
14
|
-
from hexagonal.adapters.drivens.repository.base import
|
|
15
|
+
from hexagonal.adapters.drivens.repository.base import (
|
|
16
|
+
BaseAggregateRepositoryAdapter,
|
|
17
|
+
BaseEntityRepositoryAdapter,
|
|
18
|
+
BaseSearchRepositoryAdapter,
|
|
19
|
+
)
|
|
15
20
|
from hexagonal.domain import (
|
|
16
21
|
AggregateNotFound,
|
|
17
|
-
AggregateRoot,
|
|
18
22
|
AggregateSnapshot,
|
|
19
23
|
AggregateVersionMismatch,
|
|
20
24
|
SnapshotState,
|
|
25
|
+
TAggregate,
|
|
26
|
+
TEntity,
|
|
21
27
|
TIdEntity,
|
|
28
|
+
TQuery,
|
|
29
|
+
TView,
|
|
22
30
|
)
|
|
23
31
|
|
|
24
32
|
from .datastore import SQLAlchemyConnectionContextManager
|
|
25
33
|
from .models import create_aggregates_table, create_events_table
|
|
26
34
|
|
|
27
|
-
TAggregate = TypeVar("TAggregate", bound=AggregateRoot[Any, Any])
|
|
28
|
-
|
|
29
35
|
logger = logging.getLogger(__name__)
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
class SQLAlchemyEntityRepositoryAdapter(
|
|
39
|
+
BaseEntityRepositoryAdapter[SQLAlchemyConnectionContextManager, TEntity, TIdEntity]
|
|
40
|
+
):
|
|
41
|
+
"""SQLAlchemy repository adapter for entities.
|
|
42
|
+
|
|
43
|
+
This adapter implements the IEntityRepository interface using SQLAlchemy
|
|
44
|
+
as the backing store. It handles the persistence and retrieval of entities.
|
|
45
|
+
|
|
46
|
+
Supports multiple database backends (PostgreSQL, MySQL, SQLite) through
|
|
47
|
+
SQLAlchemy's abstraction layer.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
mapper: Mapper for converting between domain and persistence models
|
|
51
|
+
connection_manager: SQLAlchemy connection context manager
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def initialize(self, env: Mapping[str, str]) -> None:
|
|
55
|
+
"""Initialize the repository from environment variables."""
|
|
56
|
+
super().initialize(env)
|
|
57
|
+
self._schema_name: str | None = self.env.get("SCHEMA_NAME")
|
|
58
|
+
|
|
59
|
+
def _get(self, conn: Connection, id: TIdEntity) -> TEntity | None:
|
|
60
|
+
"""Fetch entity from database."""
|
|
61
|
+
# Implement entity retrieval logic using SQLAlchemy
|
|
62
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def _insert(self, conn: Connection, entity: TEntity) -> None:
|
|
66
|
+
"""Save an entity to the database."""
|
|
67
|
+
# Implement entity persistence logic using SQLAlchemy
|
|
68
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def _update(self, conn: Connection, entity: TEntity) -> None:
|
|
72
|
+
"""Update an existing entity in the database."""
|
|
73
|
+
# Implement entity update logic using SQLAlchemy
|
|
74
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def _delete(self, conn: Connection, id: TIdEntity) -> None:
|
|
78
|
+
"""Delete an entity from the database."""
|
|
79
|
+
# Implement entity deletion logic using SQLAlchemy
|
|
80
|
+
# This is a placeholder implementation and should be replaced with actual logic
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def get(self, id: TIdEntity) -> TEntity:
|
|
84
|
+
"""Get an entity by its ID."""
|
|
85
|
+
self.verify()
|
|
86
|
+
with self.connection_manager.cursor() as conn:
|
|
87
|
+
entity = self._get(conn, id)
|
|
88
|
+
if entity is None:
|
|
89
|
+
raise AggregateNotFound(f"Entity with id {id} not found")
|
|
90
|
+
return entity
|
|
91
|
+
|
|
92
|
+
def save(self, entity: TEntity):
|
|
93
|
+
"""Save an entity to the repository."""
|
|
94
|
+
self.verify()
|
|
95
|
+
with self.connection_manager.cursor() as conn:
|
|
96
|
+
existing = self._get(conn, entity.id)
|
|
97
|
+
if existing is None:
|
|
98
|
+
self._insert(conn, entity)
|
|
99
|
+
else:
|
|
100
|
+
self._update(conn, entity)
|
|
101
|
+
|
|
102
|
+
def delete(self, id: TIdEntity) -> TEntity:
|
|
103
|
+
"""Delete an entity from the repository."""
|
|
104
|
+
self.verify()
|
|
105
|
+
with self.connection_manager.cursor() as conn:
|
|
106
|
+
entity = self._get(conn, id)
|
|
107
|
+
if entity is None:
|
|
108
|
+
raise AggregateNotFound(f"Entity with id {id} not found")
|
|
109
|
+
self._delete(conn, id)
|
|
110
|
+
return entity
|
|
111
|
+
|
|
112
|
+
|
|
32
113
|
class SQLAlchemyRepositoryAdapter(
|
|
33
114
|
BaseAggregateRepositoryAdapter[
|
|
34
115
|
SQLAlchemyConnectionContextManager, TAggregate, TIdEntity
|
|
@@ -322,3 +403,40 @@ class SQLAlchemyRepositoryAdapter(
|
|
|
322
403
|
self._save_event_history(conn, events)
|
|
323
404
|
self._delete(conn, id)
|
|
324
405
|
return agg
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class SQLAlchemySearchRepositoryAdapter(
|
|
409
|
+
BaseSearchRepositoryAdapter[SQLAlchemyConnectionContextManager, TQuery, TView]
|
|
410
|
+
):
|
|
411
|
+
"""SQLAlchemy repository adapter for search queries.
|
|
412
|
+
|
|
413
|
+
This adapter implements the ISearchRepository interface using SQLAlchemy
|
|
414
|
+
as the backing store. It handles executing search queries and returning
|
|
415
|
+
results in a view format.
|
|
416
|
+
|
|
417
|
+
Supports multiple database backends (PostgreSQL, MySQL, SQLite) through
|
|
418
|
+
SQLAlchemy's abstraction layer.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
mapper: Mapper for converting between domain and persistence models
|
|
422
|
+
connection_manager: SQLAlchemy connection context manager
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
@abstractmethod
|
|
426
|
+
def _search(self, conn: Connection, query: TQuery) -> Sequence[TView]: ...
|
|
427
|
+
|
|
428
|
+
def search(self, query: TQuery) -> Sequence[TView]:
|
|
429
|
+
"""Execute a search query against the repository.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
query: The search query to execute
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
A sequence of view objects matching the query criteria
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
RuntimeError: If not attached to a unit of work
|
|
439
|
+
"""
|
|
440
|
+
self.verify()
|
|
441
|
+
with self.connection_manager.cursor() as conn:
|
|
442
|
+
return self._search(conn, query)
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
from .api import BaseAPI, GetEvent
|
|
1
|
+
from .api import BaseAPI, GetEvent, TBaseApp
|
|
2
2
|
from .app import Application
|
|
3
3
|
from .bus_app import BusAppGroup, ComposableBusApp
|
|
4
|
-
from .handlers import
|
|
4
|
+
from .handlers import (
|
|
5
|
+
CommandHandler,
|
|
6
|
+
CommandHandlerBase,
|
|
7
|
+
EventHandler,
|
|
8
|
+
EventHandlerBase,
|
|
9
|
+
MessageHandler,
|
|
10
|
+
QueryHandler,
|
|
11
|
+
)
|
|
5
12
|
from .infrastructure import (
|
|
6
13
|
ComposableInfrastructure,
|
|
7
14
|
Infrastructure,
|
|
8
15
|
InfrastructureGroup,
|
|
9
16
|
)
|
|
10
|
-
from .query import
|
|
17
|
+
from .query import (
|
|
18
|
+
AggregateView,
|
|
19
|
+
GetAggregateByIdHandler,
|
|
20
|
+
GetById,
|
|
21
|
+
GetByIdHandler,
|
|
22
|
+
GetEntityByIdHandler,
|
|
23
|
+
SearchAggregateRepository,
|
|
24
|
+
)
|
|
25
|
+
from .topics import RegisterTopics
|
|
11
26
|
|
|
12
27
|
__all__ = [
|
|
28
|
+
"TBaseApp",
|
|
13
29
|
"BaseAPI",
|
|
14
30
|
"GetEvent",
|
|
15
31
|
"Application",
|
|
@@ -26,4 +42,9 @@ __all__ = [
|
|
|
26
42
|
"SearchAggregateRepository",
|
|
27
43
|
"AggregateView",
|
|
28
44
|
"GetByIdHandler",
|
|
45
|
+
"RegisterTopics",
|
|
46
|
+
"GetAggregateByIdHandler",
|
|
47
|
+
"GetEntityByIdHandler",
|
|
48
|
+
"CommandHandlerBase",
|
|
49
|
+
"EventHandlerBase",
|
|
29
50
|
]
|