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,127 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Hashable, cast
3
+ from uuid import UUID
4
+
5
+ import orjson
6
+ from eventsourcing.domain import DomainEventProtocol
7
+ from eventsourcing.persistence import (
8
+ Mapper,
9
+ MapperDeserialisationError,
10
+ StoredEvent,
11
+ Transcoder,
12
+ find_id_convertor,
13
+ )
14
+ from eventsourcing.utils import get_topic, resolve_topic
15
+
16
+ from hexagonal.domain import (
17
+ CloudMessage,
18
+ Command,
19
+ DomainEvent,
20
+ IntegrationEvent,
21
+ )
22
+ from hexagonal.domain.aggregate import AggregateSnapshot
23
+ from hexagonal.domain.base import Inmutable
24
+
25
+
26
+ @dataclass
27
+ class StoredMessage:
28
+ topic: str
29
+ state: bytes
30
+
31
+
32
+ TMessage = Command | DomainEvent | IntegrationEvent | CloudMessage[Any]
33
+
34
+
35
+ class MessageMapper(Mapper[UUID]):
36
+ def to_stored_event(self, domain_event: DomainEventProtocol[UUID]) -> StoredEvent:
37
+ """Converts the given domain event to a :class:`StoredEvent` object."""
38
+ topic = get_topic(domain_event.__class__)
39
+ event_state = (
40
+ domain_event.model_dump(mode="json")
41
+ if isinstance(domain_event, Inmutable)
42
+ else domain_event.__dict__.copy()
43
+ )
44
+ originator_id = event_state.pop("originator_id")
45
+ originator_version = event_state.pop("originator_version")
46
+ class_version = getattr(type(domain_event), "class_version", 1)
47
+ if class_version > 1:
48
+ event_state["class_version"] = class_version
49
+ stored_state = self.transcoder.encode(event_state)
50
+ if self.compressor:
51
+ stored_state = self.compressor.compress(stored_state)
52
+ if self.cipher:
53
+ stored_state = self.cipher.encrypt(stored_state)
54
+ return StoredEvent(
55
+ originator_id=originator_id,
56
+ originator_version=originator_version,
57
+ topic=topic,
58
+ state=stored_state,
59
+ )
60
+
61
+ def to_domain_event(self, stored_event: StoredEvent) -> DomainEventProtocol[UUID]:
62
+ """Converts the given :class:`StoredEvent` to a domain event object."""
63
+ cls = resolve_topic(stored_event.topic)
64
+
65
+ stored_state = stored_event.state
66
+ try:
67
+ if self.cipher:
68
+ stored_state = self.cipher.decrypt(stored_state)
69
+ if self.compressor:
70
+ stored_state = self.compressor.decompress(stored_state)
71
+ event_state: dict[str, Any] = self.transcoder.decode(stored_state)
72
+ except Exception as e:
73
+ msg = (
74
+ f"Failed to deserialise state of stored event with "
75
+ f"topic '{stored_event.topic}', "
76
+ f"originator_id '{stored_event.originator_id}' and "
77
+ f"originator_version {stored_event.originator_version}: {e}"
78
+ )
79
+ raise MapperDeserialisationError(msg) from e
80
+
81
+ id_convertor = find_id_convertor(
82
+ cls, cast(Hashable, type(stored_event.originator_id))
83
+ )
84
+ # print("ID of convertor:", id(convertor))
85
+ event_state["originator_id"] = id_convertor(stored_event.originator_id)
86
+ event_state["originator_version"] = stored_event.originator_version
87
+ class_version = getattr(cls, "class_version", 1)
88
+ from_version = event_state.pop("class_version", 1)
89
+ while from_version < class_version:
90
+ getattr(cls, f"upcast_v{from_version}_v{from_version + 1}")(event_state)
91
+ from_version += 1
92
+ if issubclass(cls, AggregateSnapshot):
93
+ return cls.model_validate(event_state) # type: ignore[return-value]
94
+ domain_event = object.__new__(cls)
95
+ domain_event.__dict__.update(event_state)
96
+ return domain_event
97
+
98
+ def to_stored_message(self, message: TMessage) -> StoredMessage:
99
+ topic = get_topic(message.__class__)
100
+ event_state = message.model_dump(mode="json")
101
+ stored_state = self.transcoder.encode(event_state)
102
+ if self.compressor:
103
+ stored_state = self.compressor.compress(stored_state)
104
+ if self.cipher:
105
+ stored_state = self.cipher.encrypt(stored_state)
106
+ return StoredMessage(
107
+ topic=topic,
108
+ state=stored_state,
109
+ )
110
+
111
+ def to_message(self, stored_message: StoredMessage) -> TMessage:
112
+ stored_state = stored_message.state
113
+ if self.cipher:
114
+ stored_state = self.cipher.decrypt(stored_state)
115
+ if self.compressor:
116
+ stored_state = self.compressor.decompress(stored_state)
117
+ event_state: dict[str, Any] = self.transcoder.decode(stored_state)
118
+ cls = cast(TMessage, resolve_topic(stored_message.topic))
119
+ return cls.model_validate(event_state)
120
+
121
+
122
+ class OrjsonTranscoder(Transcoder):
123
+ def encode(self, obj: Any) -> bytes:
124
+ return orjson.dumps(obj)
125
+
126
+ def decode(self, data: bytes) -> Any:
127
+ return orjson.loads(data)
@@ -0,0 +1,13 @@
1
+ from .repository import (
2
+ BaseAggregateRepositoryAdapter,
3
+ BaseRepositoryAdapter,
4
+ TAggregate,
5
+ )
6
+ from .unit_of_work import BaseUnitOfWork
7
+
8
+ __all__ = [
9
+ "BaseRepositoryAdapter",
10
+ "BaseAggregateRepositoryAdapter",
11
+ "BaseUnitOfWork",
12
+ "TAggregate",
13
+ ]
@@ -0,0 +1,85 @@
1
+ # pyright: reportMissingTypeStubs=false, reportUnknownArgumentType=false, reportMissingParameterType=none, reportGeneralTypeIssues=none
2
+
3
+ from typing import (
4
+ Any,
5
+ ClassVar,
6
+ Dict,
7
+ Mapping,
8
+ Type,
9
+ TypeVar,
10
+ get_args,
11
+ get_origin,
12
+ )
13
+ from uuid import UUID
14
+
15
+ from eventsourcing.persistence import Mapper
16
+ from eventsourcing.utils import Environment
17
+
18
+ from hexagonal.application import Infrastructure
19
+ from hexagonal.domain import AggregateRoot, TIdEntity
20
+ from hexagonal.ports.drivens import (
21
+ IAggregateRepository,
22
+ IBaseRepository,
23
+ IUnitOfWork,
24
+ TManager,
25
+ )
26
+
27
+ TAggregate = TypeVar("TAggregate", bound=AggregateRoot[Any, Any])
28
+
29
+
30
+ class BaseRepositoryAdapter(IBaseRepository[TManager], Infrastructure):
31
+ ENV: ClassVar[Dict[str, str]] = {}
32
+ NAME: ClassVar[str | None] = None
33
+
34
+ def __init__(self, connection_manager: TManager):
35
+ super().__init__()
36
+ self._connection_manager = connection_manager
37
+
38
+ def initialize(self, env: Mapping[str, str]) -> None:
39
+ env2 = self.ENV.copy()
40
+ name = self.NAME or self.__class__.__name__.upper()
41
+ env2.update(env)
42
+ self.env = Environment(name, env2)
43
+ self._attached_to_uow = False
44
+ self._manager_at_uow: TManager | None = None
45
+ super().initialize(self.env)
46
+
47
+ def attach_to_unit_of_work(self, uow: IUnitOfWork[TManager]) -> None:
48
+ self._attached_to_uow = True
49
+ self._manager_at_uow = uow.connection_manager
50
+
51
+ def detach_from_unit_of_work(self) -> None:
52
+ self._attached_to_uow = False
53
+ self._manager_at_uow = None
54
+
55
+ @property
56
+ def connection_manager(self) -> TManager:
57
+ if self._attached_to_uow and self._manager_at_uow is not None:
58
+ return self._manager_at_uow
59
+ return self._connection_manager
60
+
61
+
62
+ class BaseAggregateRepositoryAdapter(
63
+ BaseRepositoryAdapter[TManager],
64
+ IAggregateRepository[TManager, TAggregate, TIdEntity],
65
+ ):
66
+ _type_of_aggregate: Type[TAggregate]
67
+
68
+ def __init_subclass__(cls) -> None:
69
+ super().__init_subclass__()
70
+ # Inspect generic base to find the concrete type argument
71
+ for base in getattr(cls, "__orig_bases__", []):
72
+ origin = get_origin(base)
73
+ if origin and issubclass(origin, BaseAggregateRepositoryAdapter):
74
+ args = get_args(base)
75
+ if args:
76
+ cls._type_of_aggregate = args[0]
77
+ cls.NAME = cls._type_of_aggregate.__name__.upper()
78
+
79
+ def __init__(self, mapper: Mapper[UUID], connection_manager: TManager):
80
+ super().__init__(connection_manager)
81
+ self._mapper = mapper
82
+
83
+ @property
84
+ def aggregate_name(self) -> str:
85
+ return self._type_of_aggregate.__name__
@@ -0,0 +1,75 @@
1
+ from typing import Mapping
2
+
3
+ from eventsourcing.utils import get_topic
4
+
5
+ from hexagonal.application import InfrastructureGroup
6
+ from hexagonal.ports.drivens import IBaseRepository, IUnitOfWork, TManager
7
+
8
+
9
+ class BaseUnitOfWork(IUnitOfWork[TManager], InfrastructureGroup):
10
+ def __init__(
11
+ self,
12
+ *repositories: IBaseRepository[TManager],
13
+ connection_manager: TManager,
14
+ ):
15
+ self._repositories = {get_topic(repo.__class__): repo for repo in repositories}
16
+ self._initialized = False
17
+ self._manager = connection_manager
18
+ super().__init__(self._manager, *repositories)
19
+
20
+ def initialize(self, env: Mapping[str, str]) -> None:
21
+ self._initialized = True
22
+ self._active = False
23
+ return super().initialize(env)
24
+
25
+ @property
26
+ def initialized(self) -> bool:
27
+ return self._initialized and super().initialized
28
+
29
+ def attach_repo(self, repo: IBaseRepository[TManager]):
30
+ topic = get_topic(repo.__class__)
31
+ if topic in self._repositories:
32
+ return
33
+
34
+ self._repositories[topic] = repo
35
+ if self.initialized and self._active:
36
+ repo.attach_to_unit_of_work(self)
37
+
38
+ def detach_repo(self, repo: IBaseRepository[TManager]) -> None:
39
+ topic = get_topic(repo.__class__)
40
+ if topic not in self._repositories:
41
+ return
42
+
43
+ if self.initialized and self._active:
44
+ repo.detach_from_unit_of_work()
45
+ del self._repositories[topic]
46
+
47
+ @property
48
+ def connection_manager(self):
49
+ return self._manager
50
+
51
+ def __enter__(self):
52
+ self.verify()
53
+ if self._active:
54
+ return self
55
+ # get connection from manager, the connection is entered yet
56
+ self._ctx = self._manager.start_connection()
57
+ for repo in self._repositories.values():
58
+ repo.attach_to_unit_of_work(self)
59
+ self._active = True
60
+ return self
61
+
62
+ def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
63
+ self._active = False
64
+ try:
65
+ if exc_type is None:
66
+ self.commit()
67
+ else:
68
+ self.rollback()
69
+ except Exception as e:
70
+ # Log or handle commit/rollback errors
71
+ raise RuntimeError(f"Failed to finalize transaction: {e}") from e
72
+ finally:
73
+ for repo in self._repositories.values():
74
+ repo.detach_from_unit_of_work()
75
+ self._ctx.__exit__(exc_type, exc_val, exc_tb) # type: ignore
@@ -0,0 +1,18 @@
1
+ """SQLite adapters for the repository pattern."""
2
+
3
+ from .datastore import SQLiteConnectionContextManager, SQLiteDatastore
4
+ from .infrastructure import SQLiteInfrastructure
5
+ from .outbox import SQLiteInboxRepository, SQLiteOutboxRepository, SQLitePairInboxOutbox
6
+ from .repository import SQLiteRepositoryAdapter
7
+ from .unit_of_work import SQLiteUnitOfWork
8
+
9
+ __all__ = [
10
+ "SQLiteConnectionContextManager",
11
+ "SQLiteDatastore",
12
+ "SQLiteRepositoryAdapter",
13
+ "SQLiteUnitOfWork",
14
+ "SQLiteOutboxRepository",
15
+ "SQLiteInboxRepository",
16
+ "SQLiteInfrastructure",
17
+ "SQLitePairInboxOutbox",
18
+ ]
@@ -0,0 +1,197 @@
1
+ """SQLite datastore implementation for the repository pattern."""
2
+
3
+ import sqlite3
4
+ from contextlib import contextmanager
5
+ from typing import Callable, Iterator, Literal, Mapping, Optional, TypeVar
6
+
7
+ from eventsourcing.utils import strtobool
8
+
9
+ from hexagonal.application import Infrastructure
10
+ from hexagonal.ports.drivens import IConnectionManager
11
+
12
+ from .env_vars import SQLITE_CHECK_SAME_THREAD, SQLITE_DB_PATH
13
+
14
+ # Type variable for the connection type
15
+ T = TypeVar("T", bound=sqlite3.Connection)
16
+
17
+ # Type alias for SQLite isolation levels
18
+
19
+
20
+ class SQLiteDatastore(Infrastructure):
21
+ """SQLite datastore adapter that provides connection and transaction management."""
22
+
23
+ def __init__(
24
+ self,
25
+ database: str,
26
+ *,
27
+ timeout: float = 5.0,
28
+ detect_types: int = 0,
29
+ isolation_level: Optional[Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]] = None,
30
+ check_same_thread: bool = True,
31
+ factory: Optional[Callable[..., sqlite3.Connection]] = None,
32
+ cached_statements: int = 128,
33
+ uri: bool = False,
34
+ autocommit: bool = False,
35
+ ):
36
+ """Initialize SQLite datastore.
37
+
38
+ Args:
39
+ database: Path to the SQLite database file
40
+ timeout: How many seconds the connection should wait
41
+ before raising an exception
42
+ detect_types: Control the type detection
43
+ isolation_level: See sqlite3.Connection.isolation_level
44
+ check_same_thread: If True, only the creating thread may use the connection
45
+ factory: Custom connection factory
46
+ (must be a callable that returns a Connection)
47
+ cached_statements: Number of statements to cache
48
+ uri: If True, database is interpreted as a URI
49
+ autocommit: If True, the connection will be in autocommit mode
50
+ """
51
+ self.database = database
52
+ self.timeout = timeout
53
+ self.detect_types = detect_types
54
+ self.isolation_level = isolation_level
55
+ self.check_same_thread = check_same_thread
56
+ self.factory = factory
57
+ self.cached_statements = cached_statements
58
+ self.uri = uri
59
+ self.autocommit = autocommit
60
+ self._connection: Optional[sqlite3.Connection] = None
61
+
62
+ @contextmanager
63
+ def get_connection(self) -> Iterator[sqlite3.Connection]:
64
+ """Get a database connection from the pool.
65
+
66
+ Yields:
67
+ A database connection that will be automatically
68
+ closed when the context exits
69
+
70
+ Example:
71
+ with datastore.get_connection() as conn:
72
+ cursor = conn.cursor()
73
+ cursor.execute('SELECT 1')
74
+ result = cursor.fetchone()
75
+ """
76
+ conn = sqlite3.connect(
77
+ database=self.database,
78
+ timeout=self.timeout,
79
+ detect_types=self.detect_types,
80
+ check_same_thread=self.check_same_thread,
81
+ cached_statements=self.cached_statements,
82
+ uri=self.uri,
83
+ )
84
+ try:
85
+ # Enable foreign key constraints
86
+ conn.execute("PRAGMA foreign_keys = ON")
87
+ yield conn
88
+ except sqlite3.DatabaseError as e:
89
+ raise Exception(f"SQLite DatabaseError: {e}") from e
90
+ except sqlite3.InterfaceError as e:
91
+ raise Exception(f"SQLite InterfaceError: {e}") from e
92
+ # except Exception as e:
93
+ # raise Exception(f"SQLite Unknown error: {e}") from e
94
+ finally:
95
+ conn.close()
96
+
97
+ @contextmanager
98
+ def transaction(self, commit: bool = False) -> Iterator[sqlite3.Cursor]:
99
+ """Execute a transaction on the database.
100
+
101
+ Args:
102
+ commit: If True, commit the transaction after the block completes.
103
+ If False, rollback the transaction if an exception occurs.
104
+
105
+ Yields:
106
+ A database cursor for executing SQL statements.
107
+
108
+ Example:
109
+ with datastore.transaction(commit=True) as cursor:
110
+ cursor.execute('INSERT INTO table VALUES (?)', (value,))
111
+ """
112
+ with self.get_connection() as conn:
113
+ cursor = conn.cursor()
114
+ try:
115
+ yield cursor
116
+ if commit:
117
+ conn.commit()
118
+ except Exception as exc:
119
+ conn.rollback()
120
+ raise RuntimeError("Transaction failed") from exc
121
+ finally:
122
+ cursor.close()
123
+
124
+ def close(self) -> None:
125
+ """Close any open connection."""
126
+ if self._connection:
127
+ self._connection.close()
128
+ self._connection = None
129
+
130
+ def __enter__(self):
131
+ self._connection = self.get_connection().__enter__()
132
+ self._connection.row_factory = sqlite3.Row
133
+ return self
134
+
135
+ def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
136
+ self.close()
137
+
138
+ def __del__(self):
139
+ self.close()
140
+
141
+
142
+ def dict_factory(cursor: sqlite3.Cursor, row: sqlite3.Row):
143
+ fields = [column[0] for column in cursor.description]
144
+ return {key: value for key, value in zip(fields, row, strict=False)}
145
+
146
+
147
+ class SQLiteConnectionContextManager(IConnectionManager, Infrastructure):
148
+ def __init__(self, datastore: SQLiteDatastore | None = None):
149
+ self._current_connection: sqlite3.Connection | None = None
150
+ self._setted_datastore: bool = datastore is not None
151
+ if datastore is not None:
152
+ self._datastore = datastore
153
+ super().__init__()
154
+
155
+ def initialize(self, env: Mapping[str, str]) -> None:
156
+ if self._setted_datastore:
157
+ return super().initialize(env)
158
+ db_path = env.get(SQLITE_DB_PATH, ":memory:")
159
+ check_same_thread = strtobool(env.get(SQLITE_CHECK_SAME_THREAD, "True"))
160
+ self._datastore = SQLiteDatastore(
161
+ database=db_path,
162
+ check_same_thread=check_same_thread,
163
+ )
164
+ super().initialize(env)
165
+
166
+ @property
167
+ def datastore(self) -> SQLiteDatastore:
168
+ self.verify()
169
+ return self._datastore
170
+
171
+ def start_connection(self):
172
+ self._ctx_con = self.datastore.get_connection()
173
+ self._current_connection = self._ctx_con.__enter__()
174
+ return self._ctx_con
175
+
176
+ @property
177
+ def current_connection(self) -> sqlite3.Connection:
178
+ if self._current_connection is None:
179
+ self._ctx_con = self.datastore.get_connection()
180
+ self._current_connection = self._ctx_con.__enter__()
181
+ return self._current_connection
182
+
183
+ @current_connection.setter
184
+ def current_connection(self, connection: sqlite3.Connection) -> None:
185
+ if getattr(self, "_ctx_con", None):
186
+ self._ctx_con.__exit__(None, None, None)
187
+ self._current_connection = connection
188
+
189
+ @contextmanager
190
+ def cursor(self) -> Iterator[sqlite3.Cursor]:
191
+ try:
192
+ cursor = self.current_connection.cursor()
193
+ except sqlite3.ProgrammingError:
194
+ self._current_connection = None
195
+ cursor = self.current_connection.cursor()
196
+ yield cursor
197
+ cursor.close()
@@ -0,0 +1,2 @@
1
+ SQLITE_DB_PATH = "SQLITE_DB_PATH"
2
+ SQLITE_CHECK_SAME_THREAD = "SQLITE_CHECK_SAME_THREAD"
@@ -0,0 +1,20 @@
1
+ from hexagonal.adapters.drivens.mappers import MessageMapper
2
+ from hexagonal.application import ComposableInfrastructure
3
+
4
+ from .datastore import SQLiteConnectionContextManager, SQLiteDatastore
5
+
6
+
7
+ class SQLiteInfrastructure(ComposableInfrastructure):
8
+ def __init__(self, mapper: MessageMapper, datastore: SQLiteDatastore | None = None):
9
+ self._datastore = datastore
10
+ self._mapper = mapper
11
+ self._connection_manager = SQLiteConnectionContextManager(datastore)
12
+ super().__init__(self._connection_manager)
13
+
14
+ @property
15
+ def connection_manager(self) -> SQLiteConnectionContextManager:
16
+ return self._connection_manager
17
+
18
+ @property
19
+ def mapper(self) -> MessageMapper:
20
+ return self._mapper