sqlalchemy-foundation-kit 0.0.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.
- sqlalchemy_foundation_kit/__init__.py +119 -0
- sqlalchemy_foundation_kit/__version__.py +1 -0
- sqlalchemy_foundation_kit/_typing.py +28 -0
- sqlalchemy_foundation_kit/base/__init__.py +46 -0
- sqlalchemy_foundation_kit/base/_optional.py +37 -0
- sqlalchemy_foundation_kit/base/engine.py +256 -0
- sqlalchemy_foundation_kit/base/metadata.py +57 -0
- sqlalchemy_foundation_kit/base/models.py +101 -0
- sqlalchemy_foundation_kit/base/serialization.py +98 -0
- sqlalchemy_foundation_kit/base/types.py +72 -0
- sqlalchemy_foundation_kit/config/__init__.py +17 -0
- sqlalchemy_foundation_kit/config/postgres.py +177 -0
- sqlalchemy_foundation_kit/contrib/__init__.py +5 -0
- sqlalchemy_foundation_kit/contrib/_metrics_utils.py +19 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/__init__.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/_deps.py +37 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/database.py +196 -0
- sqlalchemy_foundation_kit/contrib/dependency_injector/metrics.py +85 -0
- sqlalchemy_foundation_kit/contrib/di/__init__.py +21 -0
- sqlalchemy_foundation_kit/contrib/di/_base.py +27 -0
- sqlalchemy_foundation_kit/contrib/di/_deps.py +32 -0
- sqlalchemy_foundation_kit/contrib/di/database.py +169 -0
- sqlalchemy_foundation_kit/contrib/di/metrics.py +71 -0
- sqlalchemy_foundation_kit/contrib/metrics/__init__.py +7 -0
- sqlalchemy_foundation_kit/contrib/metrics/postgres.py +149 -0
- sqlalchemy_foundation_kit/contrib/settings/__init__.py +19 -0
- sqlalchemy_foundation_kit/contrib/settings/postgres.py +183 -0
- sqlalchemy_foundation_kit/contrib/telemetry/__init__.py +17 -0
- sqlalchemy_foundation_kit/contrib/telemetry/instrumentations.py +101 -0
- sqlalchemy_foundation_kit/contrib/telemetry/uow.py +227 -0
- sqlalchemy_foundation_kit/protocols/__init__.py +21 -0
- sqlalchemy_foundation_kit/protocols/metrics.py +77 -0
- sqlalchemy_foundation_kit/py.typed +0 -0
- sqlalchemy_foundation_kit/session/__init__.py +27 -0
- sqlalchemy_foundation_kit/session/builder.py +293 -0
- sqlalchemy_foundation_kit/session/connection.py +33 -0
- sqlalchemy_foundation_kit/session/factories.py +104 -0
- sqlalchemy_foundation_kit/session/locks.py +68 -0
- sqlalchemy_foundation_kit/session/manager.py +252 -0
- sqlalchemy_foundation_kit/session/retry.py +82 -0
- sqlalchemy_foundation_kit/uow/__init__.py +21 -0
- sqlalchemy_foundation_kit/uow/enums.py +18 -0
- sqlalchemy_foundation_kit/uow/protocols.py +80 -0
- sqlalchemy_foundation_kit/uow/sqlalchemy.py +406 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/METADATA +624 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/RECORD +49 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/WHEEL +4 -0
- sqlalchemy_foundation_kit-0.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Async database session manager with connection pooling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import AsyncIterator, Callable
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from types import TracebackType
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Generic, cast
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import event
|
|
14
|
+
from sqlalchemy.exc import TimeoutError as SATimeoutError
|
|
15
|
+
from sqlalchemy.ext.asyncio import (
|
|
16
|
+
AsyncEngine,
|
|
17
|
+
AsyncSession,
|
|
18
|
+
async_sessionmaker,
|
|
19
|
+
create_async_engine,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .._typing import SessionT
|
|
23
|
+
from ..base import build_engine_kwargs, resolve_pool_class
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ..config import PoolSettingsProtocol
|
|
27
|
+
from ..protocols import PostgresMetricsProtocol
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
DEFAULT_DISPOSE_TIMEOUT_SECONDS: float = 30.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _safe_metric_call(func: Callable[[], None], error_msg: str) -> None:
|
|
35
|
+
"""Call a metric-recording function and swallow exceptions.
|
|
36
|
+
|
|
37
|
+
Metrics must never break application logic — any failure inside the metric
|
|
38
|
+
callback is logged at exception level and discarded.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
func: Zero-arg callable that records a metric.
|
|
42
|
+
error_msg: Message logged together with the traceback on failure.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
func()
|
|
46
|
+
except Exception:
|
|
47
|
+
logger.exception(error_msg)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def attach_metrics(engine: AsyncEngine, metrics: PostgresMetricsProtocol) -> None:
|
|
51
|
+
"""Attach metrics event listeners to a SQLAlchemy engine.
|
|
52
|
+
|
|
53
|
+
Registers event handlers for connection checkout, checkin, and error events
|
|
54
|
+
to collect pool statistics and connection metrics.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
engine: SQLAlchemy ``AsyncEngine`` to attach listeners to.
|
|
58
|
+
metrics: Metrics collector implementing ``PostgresMetricsProtocol``.
|
|
59
|
+
"""
|
|
60
|
+
pool = engine.pool
|
|
61
|
+
|
|
62
|
+
def record_pool_stats() -> None:
|
|
63
|
+
_safe_metric_call(
|
|
64
|
+
lambda: metrics.record_pool_stats(
|
|
65
|
+
pool_size=pool.size() if hasattr(pool, "size") else 0,
|
|
66
|
+
pool_checked_out=(pool.checkedout() if hasattr(pool, "checkedout") else 0),
|
|
67
|
+
pool_overflow=pool.overflow() if hasattr(pool, "overflow") else 0,
|
|
68
|
+
),
|
|
69
|
+
"Failed to record database pool stats",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def on_checkout(dbapi_connection: Any, connection_record: Any, connection_proxy: Any) -> None:
|
|
73
|
+
"""Handle connection checkout event."""
|
|
74
|
+
record_pool_stats()
|
|
75
|
+
connection_record.info["checkout_start"] = time.perf_counter()
|
|
76
|
+
|
|
77
|
+
def on_checkin(dbapi_connection: Any, connection_record: Any) -> None:
|
|
78
|
+
"""Handle connection checkin event."""
|
|
79
|
+
record_pool_stats()
|
|
80
|
+
|
|
81
|
+
if "checkout_start" in connection_record.info:
|
|
82
|
+
duration = time.perf_counter() - connection_record.info["checkout_start"]
|
|
83
|
+
_safe_metric_call(
|
|
84
|
+
lambda: metrics.record_checkout(duration=duration),
|
|
85
|
+
"Failed to record database checkout duration",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def on_error(exception_context: Any) -> None:
|
|
89
|
+
"""Handle database error event."""
|
|
90
|
+
exc = exception_context.original_exception
|
|
91
|
+
error_type = type(exc).__name__
|
|
92
|
+
is_timeout = isinstance(exc, (TimeoutError, SATimeoutError))
|
|
93
|
+
_safe_metric_call(
|
|
94
|
+
lambda: metrics.record_error(error_type=error_type, is_timeout=is_timeout),
|
|
95
|
+
"Failed to record database error metric",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
event.listen(pool, "checkout", on_checkout)
|
|
99
|
+
event.listen(pool, "checkin", on_checkin)
|
|
100
|
+
event.listen(engine.sync_engine, "handle_error", on_error)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncSessionManager(Generic[SessionT]):
|
|
104
|
+
"""Manages async database sessions with configurable connection pooling.
|
|
105
|
+
|
|
106
|
+
Supports two initialization approaches:
|
|
107
|
+
|
|
108
|
+
1. **Direct constructor** — all parameters in constructor with defaults.
|
|
109
|
+
2. **Builder pattern** — use :class:`AsyncSessionManagerBuilder` for more
|
|
110
|
+
readable complex configurations.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
url: str,
|
|
116
|
+
echo: bool = False,
|
|
117
|
+
poolclass: str | type = "null",
|
|
118
|
+
session_class: type[SessionT] | None = None,
|
|
119
|
+
expire_on_commit: bool = False,
|
|
120
|
+
connect_args: dict[str, object] | None = None,
|
|
121
|
+
isolation_level: str | None = None,
|
|
122
|
+
pool_settings: PoolSettingsProtocol | None = None,
|
|
123
|
+
use_orjson: bool = False,
|
|
124
|
+
metrics: PostgresMetricsProtocol | None = None,
|
|
125
|
+
on_engine_created: Callable[[AsyncEngine], None] | None = None,
|
|
126
|
+
dispose_timeout: float = DEFAULT_DISPOSE_TIMEOUT_SECONDS,
|
|
127
|
+
**kwargs: object,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Initialize session manager with direct configuration.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
url: Database connection URL (required).
|
|
133
|
+
echo: If True, SQLAlchemy will log all SQL statements (default: False).
|
|
134
|
+
poolclass: SQLAlchemy pool class or name (default: "null").
|
|
135
|
+
Use "queue" for production, "null" for testing.
|
|
136
|
+
session_class: Custom ``AsyncSession`` subclass (default: ``AsyncSession``).
|
|
137
|
+
expire_on_commit: If True, objects expire after commit (default: False).
|
|
138
|
+
connect_args: Arguments passed to the database driver (default: None).
|
|
139
|
+
isolation_level: Default transaction isolation level (default: None).
|
|
140
|
+
pool_settings: Pool configuration settings (default: None).
|
|
141
|
+
use_orjson: If True, use orjson for JSON serialization (default: False).
|
|
142
|
+
metrics: Optional metrics collector (default: None).
|
|
143
|
+
on_engine_created: Optional callback invoked with ``AsyncEngine`` after creation.
|
|
144
|
+
Use for OpenTelemetry instrumentation, custom event listeners, etc.
|
|
145
|
+
dispose_timeout: Maximum seconds to wait for engine disposal in :meth:`aclose`
|
|
146
|
+
(default: 30.0). Lower this in tests or short-lived environments; raise it
|
|
147
|
+
if you have long-running transactions that need more time to settle.
|
|
148
|
+
**kwargs: Additional keyword arguments for ``create_async_engine``.
|
|
149
|
+
"""
|
|
150
|
+
self._closed = False
|
|
151
|
+
self._close_lock = asyncio.Lock()
|
|
152
|
+
self._dispose_timeout = dispose_timeout
|
|
153
|
+
resolved_poolclass = resolve_pool_class(poolclass)
|
|
154
|
+
engine_kwargs = build_engine_kwargs(
|
|
155
|
+
echo=echo,
|
|
156
|
+
poolclass=resolved_poolclass,
|
|
157
|
+
isolation_level=isolation_level,
|
|
158
|
+
pool_settings=pool_settings,
|
|
159
|
+
connect_args=connect_args,
|
|
160
|
+
extra_kwargs=kwargs,
|
|
161
|
+
use_orjson=use_orjson,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._engine: AsyncEngine = create_async_engine(url, **engine_kwargs)
|
|
165
|
+
self._session_maker = cast(
|
|
166
|
+
async_sessionmaker[SessionT],
|
|
167
|
+
async_sessionmaker(
|
|
168
|
+
self._engine,
|
|
169
|
+
class_=session_class or AsyncSession,
|
|
170
|
+
expire_on_commit=expire_on_commit,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if metrics:
|
|
175
|
+
attach_metrics(self._engine, metrics)
|
|
176
|
+
|
|
177
|
+
if on_engine_created is not None:
|
|
178
|
+
on_engine_created(self._engine)
|
|
179
|
+
|
|
180
|
+
async def aclose(self) -> None:
|
|
181
|
+
"""Close the engine and all connections."""
|
|
182
|
+
async with self._close_lock:
|
|
183
|
+
if self._closed:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Use shield so disposal runs even if the task is cancelled; timeout avoids indefinite hang.
|
|
188
|
+
await asyncio.wait_for(
|
|
189
|
+
asyncio.shield(self._engine.dispose()),
|
|
190
|
+
timeout=self._dispose_timeout,
|
|
191
|
+
)
|
|
192
|
+
except TimeoutError:
|
|
193
|
+
logger.warning(
|
|
194
|
+
"Engine disposal timed out after %.1f seconds. Some connections may not have closed cleanly.",
|
|
195
|
+
self._dispose_timeout,
|
|
196
|
+
)
|
|
197
|
+
finally:
|
|
198
|
+
# Always mark as closed to prevent retry loops
|
|
199
|
+
self._closed = True
|
|
200
|
+
|
|
201
|
+
def _ensure_not_closed(self) -> None:
|
|
202
|
+
"""Ensure that the manager is not closed."""
|
|
203
|
+
if self._closed:
|
|
204
|
+
raise RuntimeError("AsyncSessionManager is closed")
|
|
205
|
+
|
|
206
|
+
async def __aenter__(self) -> AsyncSessionManager[SessionT]:
|
|
207
|
+
"""Support for async context manager."""
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
async def __aexit__(
|
|
211
|
+
self,
|
|
212
|
+
exc_type: type[BaseException] | None,
|
|
213
|
+
exc_val: BaseException | None,
|
|
214
|
+
exc_tb: TracebackType | None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Close the engine on exit."""
|
|
217
|
+
await self.aclose()
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def engine(self) -> AsyncEngine:
|
|
221
|
+
"""Get the underlying engine."""
|
|
222
|
+
return self._engine
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def session_maker(self) -> async_sessionmaker[SessionT]:
|
|
226
|
+
"""Get the session maker."""
|
|
227
|
+
return self._session_maker
|
|
228
|
+
|
|
229
|
+
@asynccontextmanager
|
|
230
|
+
async def get_session(self) -> AsyncIterator[SessionT]:
|
|
231
|
+
"""Get a new database session."""
|
|
232
|
+
self._ensure_not_closed()
|
|
233
|
+
async with self._session_maker() as session:
|
|
234
|
+
yield session
|
|
235
|
+
|
|
236
|
+
@asynccontextmanager
|
|
237
|
+
async def get_transaction(self, isolation_level: str | None = None) -> AsyncIterator[SessionT]:
|
|
238
|
+
"""Get a new database session with automatic transaction management.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
isolation_level: Optional isolation level for the transaction.
|
|
242
|
+
|
|
243
|
+
Yields:
|
|
244
|
+
Managed async session with active transaction.
|
|
245
|
+
"""
|
|
246
|
+
self._ensure_not_closed()
|
|
247
|
+
options = {"isolation_level": isolation_level} if isolation_level else {}
|
|
248
|
+
async with (
|
|
249
|
+
self._session_maker(execution_options=options) as session,
|
|
250
|
+
session.begin(),
|
|
251
|
+
):
|
|
252
|
+
yield session
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Retry utilities for database connection startup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
DEFAULT_HEALTHCHECK_QUERY: str = "SELECT 1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class RetryConfig:
|
|
17
|
+
"""Configuration for retry behavior with exponential backoff.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
max_retries: Maximum number of attempts before giving up.
|
|
21
|
+
retry_delay: Base delay in seconds; actual delay is ``retry_delay * 2 ** attempt``,
|
|
22
|
+
capped at ``max_backoff_delay``.
|
|
23
|
+
max_backoff_delay: Maximum delay between retries in seconds.
|
|
24
|
+
Prevents exponential backoff from growing indefinitely.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
max_retries: int = 3
|
|
28
|
+
retry_delay: float = 1.0
|
|
29
|
+
max_backoff_delay: float = 60.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DEFAULT_RETRY_CONFIG: RetryConfig = RetryConfig()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def retry_async_connection(
|
|
36
|
+
connect_func: Callable[[], Awaitable[None]],
|
|
37
|
+
service_name: str,
|
|
38
|
+
config: RetryConfig = DEFAULT_RETRY_CONFIG,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Retry an async connection callable with exponential backoff.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
connect_func: Callable that attempts to establish/test the connection.
|
|
44
|
+
service_name: Human-readable service name used in log messages.
|
|
45
|
+
config: Retry behavior configuration.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
ValueError: If config.max_retries is less than 1.
|
|
49
|
+
Exception: Re-raises the last exception when all attempts fail.
|
|
50
|
+
"""
|
|
51
|
+
if config.max_retries < 1:
|
|
52
|
+
raise ValueError(f"max_retries must be >= 1, got {config.max_retries}")
|
|
53
|
+
|
|
54
|
+
for attempt in range(config.max_retries):
|
|
55
|
+
try:
|
|
56
|
+
await connect_func()
|
|
57
|
+
except Exception:
|
|
58
|
+
if attempt == config.max_retries - 1:
|
|
59
|
+
logger.exception(
|
|
60
|
+
"%s connection failed after %d attempts",
|
|
61
|
+
service_name,
|
|
62
|
+
config.max_retries,
|
|
63
|
+
)
|
|
64
|
+
raise
|
|
65
|
+
logger.warning(
|
|
66
|
+
"%s connection attempt %d failed, retrying...",
|
|
67
|
+
service_name,
|
|
68
|
+
attempt + 1,
|
|
69
|
+
)
|
|
70
|
+
delay = min(config.retry_delay * (2**attempt), config.max_backoff_delay)
|
|
71
|
+
await asyncio.sleep(delay)
|
|
72
|
+
else:
|
|
73
|
+
logger.info("%s connection successful", service_name)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
"DEFAULT_HEALTHCHECK_QUERY",
|
|
79
|
+
"DEFAULT_RETRY_CONFIG",
|
|
80
|
+
"RetryConfig",
|
|
81
|
+
"retry_async_connection",
|
|
82
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Unit of Work pattern implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .enums import IsolationLevel
|
|
6
|
+
from .protocols import AsyncUnitOfWork, AsyncUowTransaction, SupportsAdvisoryLock
|
|
7
|
+
from .sqlalchemy import (
|
|
8
|
+
AsyncSQLAlchemyUnitOfWork,
|
|
9
|
+
AsyncSQLAlchemyUowTransaction,
|
|
10
|
+
PostgresAdvisoryLockMixin,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AsyncSQLAlchemyUnitOfWork",
|
|
15
|
+
"AsyncSQLAlchemyUowTransaction",
|
|
16
|
+
"AsyncUnitOfWork",
|
|
17
|
+
"AsyncUowTransaction",
|
|
18
|
+
"IsolationLevel",
|
|
19
|
+
"PostgresAdvisoryLockMixin",
|
|
20
|
+
"SupportsAdvisoryLock",
|
|
21
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Enums for Unit of Work."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IsolationLevel(StrEnum):
|
|
9
|
+
"""PostgreSQL transaction isolation levels.
|
|
10
|
+
|
|
11
|
+
Values match PostgreSQL's expected form (with spaces) for use with
|
|
12
|
+
SQLAlchemy execution_options(isolation_level=...).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
READ_UNCOMMITTED = "READ UNCOMMITTED"
|
|
16
|
+
READ_COMMITTED = "READ COMMITTED"
|
|
17
|
+
REPEATABLE_READ = "REPEATABLE READ"
|
|
18
|
+
SERIALIZABLE = "SERIALIZABLE"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Unit of Work protocols."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import AbstractAsyncContextManager
|
|
6
|
+
from typing import Any, Generic, Protocol
|
|
7
|
+
|
|
8
|
+
from .._typing import T_co
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncUowTransaction(Protocol):
|
|
12
|
+
"""Transaction-scoped repositories container.
|
|
13
|
+
|
|
14
|
+
Intended to be extended by concrete transaction types that expose
|
|
15
|
+
repository attributes for a specific bounded context.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SupportsAdvisoryLock(Protocol):
|
|
20
|
+
"""Capability protocol for transactions supporting advisory locks.
|
|
21
|
+
|
|
22
|
+
Use this when your transaction needs PostgreSQL advisory lock support.
|
|
23
|
+
Not all transactions require this capability (e.g., in-memory, read-only).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
async def try_advisory_lock(self, key: int) -> bool:
|
|
27
|
+
"""Try to acquire a transaction-scoped advisory lock identified by ``key``.
|
|
28
|
+
|
|
29
|
+
Returns ``True`` if the lock was acquired (and is held for the rest of the
|
|
30
|
+
transaction), ``False`` if another transaction already holds it.
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncUnitOfWork(Protocol, Generic[T_co]):
|
|
36
|
+
"""Provides transactional context for repository operations.
|
|
37
|
+
|
|
38
|
+
Provides three modes of operation:
|
|
39
|
+
- ``transaction()``: For write operations with automatic commit/rollback.
|
|
40
|
+
- ``managed_session()``: For write operations with **manual** commit/rollback control.
|
|
41
|
+
- ``query()``: For read-only operations without transaction management.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def transaction(
|
|
45
|
+
self,
|
|
46
|
+
isolation_level: str | None = None,
|
|
47
|
+
flush_before_commit: bool | None = None,
|
|
48
|
+
) -> AbstractAsyncContextManager[T_co]:
|
|
49
|
+
"""Create a new transaction context with automatic commit/rollback.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
isolation_level: Optional transaction isolation level.
|
|
53
|
+
flush_before_commit: If True, flush session before commit to surface
|
|
54
|
+
constraint violations within transaction. If ``None``, the implementation
|
|
55
|
+
applies its own default (typically configured at construction time).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def managed_session(
|
|
59
|
+
self,
|
|
60
|
+
isolation_level: str | None = None,
|
|
61
|
+
) -> AbstractAsyncContextManager[tuple[T_co, Any]]:
|
|
62
|
+
"""Create a session with manual transaction control.
|
|
63
|
+
|
|
64
|
+
Unlike :meth:`transaction`, this does **NOT** auto-commit on success. The caller
|
|
65
|
+
must explicitly call ``session.commit()`` or ``session.rollback()``. Useful for
|
|
66
|
+
complex transactional logic where the commit decision depends on multiple
|
|
67
|
+
conditions or external factors.
|
|
68
|
+
|
|
69
|
+
The second element of the yielded tuple is the underlying session object
|
|
70
|
+
(typed as ``Any`` in the protocol to avoid leaking SQLAlchemy types — concrete
|
|
71
|
+
implementations like ``AsyncSQLAlchemyUnitOfWork`` yield ``AsyncSession``).
|
|
72
|
+
|
|
73
|
+
On exception inside the context, the session is automatically rolled back.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
isolation_level: Optional transaction isolation level.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def query(self, isolation_level: str | None = None) -> AbstractAsyncContextManager[T_co]:
|
|
80
|
+
"""Create a read-only query context without transaction management."""
|