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,406 @@
|
|
|
1
|
+
"""Unit of Work implementation (async SQLAlchemy)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import AsyncIterator, Callable
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from typing import Generic
|
|
10
|
+
|
|
11
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
13
|
+
|
|
14
|
+
from .._typing import T
|
|
15
|
+
from ..session.locks import try_advisory_xact_lock
|
|
16
|
+
from .enums import IsolationLevel
|
|
17
|
+
from .protocols import AsyncUnitOfWork, AsyncUowTransaction
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Cache valid isolation levels for performance (avoid recreating set on each call)
|
|
22
|
+
_VALID_ISOLATION_LEVELS: frozenset[str] = frozenset(item.value for item in IsolationLevel)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_isolation_level(
|
|
26
|
+
isolation_level: IsolationLevel | str | None,
|
|
27
|
+
) -> str | None:
|
|
28
|
+
"""Normalize isolation_level to a valid PostgreSQL string or None.
|
|
29
|
+
|
|
30
|
+
Accepts both enum members and strings. Strings may use either underscores
|
|
31
|
+
or spaces (e.g., "READ_COMMITTED" or "READ COMMITTED") for convenience.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
isolation_level: Enum member, string, or None.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
PostgreSQL-form string (with spaces) valid for execution_options, or None.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If isolation_level is not supported.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
>>> normalize_isolation_level(None)
|
|
44
|
+
None
|
|
45
|
+
>>> normalize_isolation_level(IsolationLevel.READ_COMMITTED)
|
|
46
|
+
'READ COMMITTED'
|
|
47
|
+
>>> normalize_isolation_level("READ_COMMITTED")
|
|
48
|
+
'READ COMMITTED'
|
|
49
|
+
>>> normalize_isolation_level("read committed")
|
|
50
|
+
'READ COMMITTED'
|
|
51
|
+
"""
|
|
52
|
+
# Explicit None check (PEP 8: explicit is better than implicit)
|
|
53
|
+
if isolation_level is None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Fast path for enum members
|
|
57
|
+
if isinstance(isolation_level, IsolationLevel):
|
|
58
|
+
return isolation_level.value
|
|
59
|
+
|
|
60
|
+
# Normalize string input: uppercase and replace underscores with spaces
|
|
61
|
+
normalized = str(isolation_level).upper().replace("_", " ")
|
|
62
|
+
|
|
63
|
+
# Validate against cached valid levels
|
|
64
|
+
if normalized not in _VALID_ISOLATION_LEVELS:
|
|
65
|
+
supported = ", ".join(sorted(_VALID_ISOLATION_LEVELS))
|
|
66
|
+
raise ValueError(f"Invalid isolation level: {isolation_level!r}. Supported values: {supported}")
|
|
67
|
+
|
|
68
|
+
return normalized
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def apply_isolation_level(
|
|
72
|
+
session: AsyncSession,
|
|
73
|
+
isolation_level: IsolationLevel | str | None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Apply isolation level to an async session's connection.
|
|
76
|
+
|
|
77
|
+
**Implementation Detail**:
|
|
78
|
+
We use ``run_sync()`` because SQLAlchemy's ``execution_options()`` is a
|
|
79
|
+
synchronous method that configures the underlying DBAPI connection object.
|
|
80
|
+
We must bridge from async context to sync method via ``run_sync()``.
|
|
81
|
+
|
|
82
|
+
This is a DRY utility to eliminate duplication of isolation level application
|
|
83
|
+
logic across ``transaction()``, ``managed_session()``, and ``query()`` methods.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
session: SQLAlchemy AsyncSession to configure.
|
|
87
|
+
isolation_level: Desired isolation level (enum, string, or None).
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If isolation_level is not supported (raised by normalize_isolation_level).
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
>>> async with session_maker() as session:
|
|
94
|
+
... await apply_isolation_level(session, IsolationLevel.SERIALIZABLE)
|
|
95
|
+
... result = await session.execute(select(User))
|
|
96
|
+
... # Query runs with SERIALIZABLE isolation level
|
|
97
|
+
|
|
98
|
+
Using string isolation level:
|
|
99
|
+
>>> await apply_isolation_level(session, "READ COMMITTED")
|
|
100
|
+
|
|
101
|
+
No-op when None:
|
|
102
|
+
>>> await apply_isolation_level(session, None) # Does nothing
|
|
103
|
+
"""
|
|
104
|
+
normalized = normalize_isolation_level(isolation_level)
|
|
105
|
+
if normalized is not None:
|
|
106
|
+
conn = await session.connection()
|
|
107
|
+
# run_sync bridges async → sync for DBAPI-level configuration
|
|
108
|
+
await conn.run_sync(lambda c: c.execution_options(isolation_level=normalized))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AsyncSQLAlchemyUowTransaction(AsyncUowTransaction):
|
|
112
|
+
"""Base async SQLAlchemy transaction-scoped repositories.
|
|
113
|
+
|
|
114
|
+
This class provides access to the underlying SQLAlchemy session and is
|
|
115
|
+
intended to be subclassed by services to expose specific repositories.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
class IdentityTransaction(AsyncSQLAlchemyUowTransaction):
|
|
119
|
+
@property
|
|
120
|
+
def users(self) -> UserRepository:
|
|
121
|
+
return PostgresUserRepository(self.session)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
125
|
+
self._session = session
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def session(self) -> AsyncSession:
|
|
129
|
+
"""Get the underlying SQLAlchemy async session."""
|
|
130
|
+
return self._session
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PostgresAdvisoryLockMixin:
|
|
134
|
+
"""Mixin providing PostgreSQL advisory lock support for UoW transactions.
|
|
135
|
+
|
|
136
|
+
Requires the class to have a `session` property returning AsyncSession.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
class IdentityTransaction(AsyncSQLAlchemyUowTransaction, PostgresAdvisoryLockMixin):
|
|
140
|
+
@property
|
|
141
|
+
def users(self) -> UserRepository:
|
|
142
|
+
return PostgresUserRepository(self.session)
|
|
143
|
+
|
|
144
|
+
# Now has access to try_advisory_lock method
|
|
145
|
+
async with uow.transaction() as tx:
|
|
146
|
+
if await tx.try_advisory_lock(12345):
|
|
147
|
+
# Protected operation
|
|
148
|
+
...
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
session: AsyncSession # Type annotation for protocol compliance
|
|
152
|
+
|
|
153
|
+
async def try_advisory_lock(self, key: int) -> bool:
|
|
154
|
+
"""Acquire a Postgres transaction-scoped advisory lock.
|
|
155
|
+
|
|
156
|
+
Delegates to :func:`try_advisory_xact_lock` for actual locking logic.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
key: Integer lock key.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if lock was acquired, False if already held by another session.
|
|
163
|
+
"""
|
|
164
|
+
return await try_advisory_xact_lock(self.session, key)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class AsyncSQLAlchemyUnitOfWork(AsyncUnitOfWork[T], Generic[T]):
|
|
168
|
+
"""Base async SQLAlchemy Unit of Work.
|
|
169
|
+
|
|
170
|
+
Provides transactional context for repository operations using SQLAlchemy AsyncSession.
|
|
171
|
+
|
|
172
|
+
Methods:
|
|
173
|
+
transaction(): For write operations with automatic commit/rollback.
|
|
174
|
+
query(): For read-only operations without transaction management.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
session_maker: async_sessionmaker[AsyncSession],
|
|
180
|
+
transaction_factory: Callable[[AsyncSession], T],
|
|
181
|
+
*,
|
|
182
|
+
flush_before_commit: bool = True,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Initialize the unit of work.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
session_maker: Async session factory.
|
|
188
|
+
transaction_factory: Callable producing the transaction object exposed to callers.
|
|
189
|
+
flush_before_commit: Default ``flush_before_commit`` policy applied when
|
|
190
|
+
:meth:`transaction` is called without an explicit override.
|
|
191
|
+
Set to ``False`` here once if your service prefers SQLAlchemy's default
|
|
192
|
+
"flush on commit" semantics instead of an early flush.
|
|
193
|
+
"""
|
|
194
|
+
self._session_maker = session_maker
|
|
195
|
+
self._transaction_factory = transaction_factory
|
|
196
|
+
self._flush_before_commit = flush_before_commit
|
|
197
|
+
|
|
198
|
+
@asynccontextmanager
|
|
199
|
+
async def open_session(
|
|
200
|
+
self,
|
|
201
|
+
isolation_level: IsolationLevel | str | None = None,
|
|
202
|
+
) -> AsyncIterator[AsyncSession]:
|
|
203
|
+
"""Open a session with optional isolation level applied.
|
|
204
|
+
|
|
205
|
+
This is the extension point for subclasses that need custom session setup
|
|
206
|
+
(e.g., RLS context, session-level GUCs, custom statement timeouts).
|
|
207
|
+
Override to wrap or augment session creation while preserving isolation handling.
|
|
208
|
+
|
|
209
|
+
Used internally by :meth:`transaction` and :meth:`query`.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
isolation_level: Optional transaction isolation level.
|
|
213
|
+
|
|
214
|
+
Yields:
|
|
215
|
+
Configured AsyncSession instance.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If isolation_level is not supported.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
Subclass that sets a session-level GUC for every transaction:
|
|
222
|
+
|
|
223
|
+
class TenantUnitOfWork(AsyncSQLAlchemyUnitOfWork):
|
|
224
|
+
def __init__(self, session_maker, tx_factory, tenant_id):
|
|
225
|
+
super().__init__(session_maker, tx_factory)
|
|
226
|
+
self._tenant_id = tenant_id
|
|
227
|
+
|
|
228
|
+
@asynccontextmanager
|
|
229
|
+
async def open_session(self, isolation_level=None):
|
|
230
|
+
async with super().open_session(isolation_level) as session:
|
|
231
|
+
await session.execute(
|
|
232
|
+
text("SET app.tenant_id = :tid"),
|
|
233
|
+
{"tid": self._tenant_id},
|
|
234
|
+
)
|
|
235
|
+
yield session
|
|
236
|
+
"""
|
|
237
|
+
async with self._session_maker() as session:
|
|
238
|
+
# Apply isolation level if specified (DRY: using utility function)
|
|
239
|
+
await apply_isolation_level(session, isolation_level)
|
|
240
|
+
yield session
|
|
241
|
+
|
|
242
|
+
@asynccontextmanager
|
|
243
|
+
async def transaction(
|
|
244
|
+
self,
|
|
245
|
+
isolation_level: IsolationLevel | str | None = None,
|
|
246
|
+
flush_before_commit: bool | None = None,
|
|
247
|
+
) -> AsyncIterator[T]:
|
|
248
|
+
"""Create a new transaction context with automatic commit/rollback.
|
|
249
|
+
|
|
250
|
+
The Unit of Work automatically commits the transaction on successful exit
|
|
251
|
+
and rolls back on exception. This ensures atomic operations.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
isolation_level: Optional transaction isolation level.
|
|
255
|
+
Can be an IsolationLevel enum member or a string value.
|
|
256
|
+
Supported values: "READ_COMMITTED", "REPEATABLE_READ", "SERIALIZABLE", "READ_UNCOMMITTED".
|
|
257
|
+
flush_before_commit: If True, flush the session before commit to surface
|
|
258
|
+
constraint violations while still inside the transaction. If ``None`` (default),
|
|
259
|
+
falls back to the value passed to the constructor (``True`` unless overridden).
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
ValueError: If isolation_level is not supported.
|
|
263
|
+
|
|
264
|
+
Examples:
|
|
265
|
+
Write operation with automatic commit:
|
|
266
|
+
async with uow.transaction() as tx:
|
|
267
|
+
await tx.users.create(...)
|
|
268
|
+
# Auto-commit on exit, rollback on exception
|
|
269
|
+
"""
|
|
270
|
+
if flush_before_commit is None:
|
|
271
|
+
flush_before_commit = self._flush_before_commit
|
|
272
|
+
|
|
273
|
+
async with self.open_session(isolation_level) as session, session.begin():
|
|
274
|
+
uow = self._transaction_factory(session)
|
|
275
|
+
yield uow
|
|
276
|
+
if flush_before_commit:
|
|
277
|
+
# Flush changes before commit to catch constraint violations early
|
|
278
|
+
# while still inside the transaction context.
|
|
279
|
+
try:
|
|
280
|
+
await session.flush()
|
|
281
|
+
except SQLAlchemyError as e:
|
|
282
|
+
logger.warning("Database flush failed", extra={"error": str(e)})
|
|
283
|
+
raise
|
|
284
|
+
|
|
285
|
+
@asynccontextmanager
|
|
286
|
+
async def managed_session(
|
|
287
|
+
self,
|
|
288
|
+
isolation_level: IsolationLevel | str | None = None,
|
|
289
|
+
) -> AsyncIterator[tuple[T, AsyncSession]]:
|
|
290
|
+
"""Create a session with manual transaction control.
|
|
291
|
+
|
|
292
|
+
Unlike transaction(), this does NOT auto-commit. The caller must
|
|
293
|
+
explicitly call session.commit() or session.rollback(). This is useful
|
|
294
|
+
for complex transactional logic where commit decision depends on multiple
|
|
295
|
+
conditions or external factors.
|
|
296
|
+
|
|
297
|
+
A transaction is started automatically, but you have full control over
|
|
298
|
+
when to commit or rollback. If you exit without calling either, SQLAlchemy
|
|
299
|
+
will automatically rollback on session close.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
isolation_level: Optional transaction isolation level.
|
|
303
|
+
Can be an IsolationLevel enum member or a string value.
|
|
304
|
+
Supported values: "READ_COMMITTED", "REPEATABLE_READ", "SERIALIZABLE", "READ_UNCOMMITTED".
|
|
305
|
+
|
|
306
|
+
Yields:
|
|
307
|
+
Tuple of (transaction object, session) for manual control.
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
ValueError: If isolation_level is not supported.
|
|
311
|
+
|
|
312
|
+
Examples:
|
|
313
|
+
Manual transaction control:
|
|
314
|
+
async with uow.managed_session() as (tx, session):
|
|
315
|
+
await tx.users.create(...)
|
|
316
|
+
|
|
317
|
+
# Manually decide when to commit
|
|
318
|
+
if should_commit:
|
|
319
|
+
await session.commit()
|
|
320
|
+
else:
|
|
321
|
+
await session.rollback()
|
|
322
|
+
|
|
323
|
+
Conditional commit based on external service:
|
|
324
|
+
async with uow.managed_session() as (tx, session):
|
|
325
|
+
user = await tx.users.create(...)
|
|
326
|
+
|
|
327
|
+
# Call external service
|
|
328
|
+
result = await external_api.validate(user)
|
|
329
|
+
|
|
330
|
+
if result.success:
|
|
331
|
+
await session.commit()
|
|
332
|
+
else:
|
|
333
|
+
await session.rollback()
|
|
334
|
+
|
|
335
|
+
Multiple operations with intermediate decision:
|
|
336
|
+
async with uow.managed_session() as (tx, session):
|
|
337
|
+
user = await tx.users.create(...)
|
|
338
|
+
|
|
339
|
+
# First checkpoint
|
|
340
|
+
await session.flush()
|
|
341
|
+
|
|
342
|
+
# More operations
|
|
343
|
+
await tx.profiles.create(user_id=user.id)
|
|
344
|
+
|
|
345
|
+
# Final decision
|
|
346
|
+
await session.commit()
|
|
347
|
+
|
|
348
|
+
Note:
|
|
349
|
+
Prefer :meth:`transaction` for the vast majority of use cases — it commits
|
|
350
|
+
automatically and enforces the UoW pattern. This method is an advanced escape
|
|
351
|
+
hatch for scenarios where the commit/rollback decision depends on conditions
|
|
352
|
+
that can only be evaluated after data is written (e.g., external service
|
|
353
|
+
validation). ``session.commit()`` calls belong exclusively at the use-case
|
|
354
|
+
boundary via this method, never inside repository implementations.
|
|
355
|
+
|
|
356
|
+
Warning:
|
|
357
|
+
You MUST explicitly call session.commit() or session.rollback().
|
|
358
|
+
Exiting the context without calling either will result in automatic
|
|
359
|
+
rollback when the session closes.
|
|
360
|
+
"""
|
|
361
|
+
async with self.open_session(isolation_level) as session:
|
|
362
|
+
# Start transaction WITHOUT context manager - no auto-commit
|
|
363
|
+
await session.begin()
|
|
364
|
+
try:
|
|
365
|
+
uow = self._transaction_factory(session)
|
|
366
|
+
yield uow, session
|
|
367
|
+
except (Exception, asyncio.CancelledError):
|
|
368
|
+
# Auto-rollback on exception OR cancellation
|
|
369
|
+
await session.rollback()
|
|
370
|
+
raise
|
|
371
|
+
# User must call session.commit() or session.rollback() explicitly
|
|
372
|
+
|
|
373
|
+
@asynccontextmanager
|
|
374
|
+
async def query(
|
|
375
|
+
self,
|
|
376
|
+
isolation_level: IsolationLevel | str | None = None,
|
|
377
|
+
) -> AsyncIterator[T]:
|
|
378
|
+
"""Create a read-only query context without transaction management.
|
|
379
|
+
|
|
380
|
+
This method is designed for read-only operations and does not start a transaction
|
|
381
|
+
or perform any commit/rollback. It's semantically clearer than managed_session()
|
|
382
|
+
for read operations.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
isolation_level: Optional transaction isolation level.
|
|
386
|
+
Can be an IsolationLevel enum member or a string value.
|
|
387
|
+
Supported values: "READ_COMMITTED", "REPEATABLE_READ", "SERIALIZABLE", "READ_UNCOMMITTED".
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
ValueError: If isolation_level is not supported.
|
|
391
|
+
|
|
392
|
+
Examples:
|
|
393
|
+
Read-only query:
|
|
394
|
+
async with uow.query() as qx:
|
|
395
|
+
users = await qx.users.list_all()
|
|
396
|
+
user = await qx.users.get_by_id(user_id)
|
|
397
|
+
# No commit/rollback - just closes session
|
|
398
|
+
|
|
399
|
+
Note:
|
|
400
|
+
While this method is intended for read-only operations, SQLAlchemy does not enforce
|
|
401
|
+
this at the session level. It's up to the caller to ensure only read operations are performed.
|
|
402
|
+
"""
|
|
403
|
+
async with self.open_session(isolation_level) as session:
|
|
404
|
+
# No transaction begin/commit - just yield the session
|
|
405
|
+
uow = self._transaction_factory(session)
|
|
406
|
+
yield uow
|