sqlakit 0.22.10__tar.gz → 0.22.12__tar.gz
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.
- {sqlakit-0.22.10 → sqlakit-0.22.12}/PKG-INFO +4 -3
- {sqlakit-0.22.10 → sqlakit-0.22.12}/README.md +2 -2
- {sqlakit-0.22.10 → sqlakit-0.22.12}/pyproject.toml +3 -2
- {sqlakit-0.22.10 → sqlakit-0.22.12}/pyproject.toml.orig +3 -2
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_base.py +68 -48
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_db.py +20 -10
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_model.py +1 -1
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_registry.py +2 -2
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_sql.py +3 -3
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/asyncio/_db.py +22 -10
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/asyncio/_registry.py +2 -2
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/asyncio/orm.py +17 -9
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/asyncio/sql.py +1 -3
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/orm.py +11 -9
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/pytest_plugin.py +4 -4
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/sql.py +1 -3
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/testing.py +2 -2
- {sqlakit-0.22.10 → sqlakit-0.22.12}/LICENSE +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/__init__.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_cli.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_linters.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_project.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_pycharm.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_query.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_recording.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_render.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_routing.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_sql_formatter.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/_static.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/py.typed +0 -0
- {sqlakit-0.22.10 → sqlakit-0.22.12}/sqlakit/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlakit
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.12
|
|
4
4
|
Summary: A toolkit for SQLAlchemy applications.
|
|
5
5
|
Keywords: sqlalchemy,database,orm,sql,asyncio
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
18
19
|
Classifier: Topic :: Database
|
|
19
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
21
|
Classifier: Typing :: Typed
|
|
@@ -387,7 +388,7 @@ there is nothing to await.
|
|
|
387
388
|
## `FastAPI` integration
|
|
388
389
|
|
|
389
390
|
```python
|
|
390
|
-
from collections.abc import
|
|
391
|
+
from collections.abc import AsyncGenerator
|
|
391
392
|
from contextlib import asynccontextmanager
|
|
392
393
|
|
|
393
394
|
from fastapi import FastAPI
|
|
@@ -400,7 +401,7 @@ db = Database("postgresql+psycopg://localhost/app")
|
|
|
400
401
|
|
|
401
402
|
|
|
402
403
|
@asynccontextmanager
|
|
403
|
-
async def lifespan(app: FastAPI) ->
|
|
404
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
404
405
|
yield
|
|
405
406
|
await db.dispose() # close the pool on shutdown
|
|
406
407
|
|
|
@@ -356,7 +356,7 @@ there is nothing to await.
|
|
|
356
356
|
## `FastAPI` integration
|
|
357
357
|
|
|
358
358
|
```python
|
|
359
|
-
from collections.abc import
|
|
359
|
+
from collections.abc import AsyncGenerator
|
|
360
360
|
from contextlib import asynccontextmanager
|
|
361
361
|
|
|
362
362
|
from fastapi import FastAPI
|
|
@@ -369,7 +369,7 @@ db = Database("postgresql+psycopg://localhost/app")
|
|
|
369
369
|
|
|
370
370
|
|
|
371
371
|
@asynccontextmanager
|
|
372
|
-
async def lifespan(app: FastAPI) ->
|
|
372
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
373
373
|
yield
|
|
374
374
|
await db.dispose() # close the pool on shutdown
|
|
375
375
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit"
|
|
3
|
-
version = "0.22.
|
|
3
|
+
version = "0.22.12"
|
|
4
4
|
description = "A toolkit for SQLAlchemy applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
22
|
"Programming Language :: Python :: 3.13",
|
|
23
23
|
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Programming Language :: Python :: 3.15",
|
|
24
25
|
"Topic :: Database",
|
|
25
26
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
27
|
"Typing :: Typed",
|
|
@@ -178,7 +179,7 @@ skip = "./.venv,./site,./uv.lock,./.git"
|
|
|
178
179
|
[dependency-groups]
|
|
179
180
|
dev = [
|
|
180
181
|
"pytest>=9.1.1",
|
|
181
|
-
"ty>=0.0.
|
|
182
|
+
"ty>=0.0.87",
|
|
182
183
|
"ruff>=0.16.6",
|
|
183
184
|
"poethepoet>=0.48.0",
|
|
184
185
|
"anyio>=4.14.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit"
|
|
3
|
-
version = "0.22.
|
|
3
|
+
version = "0.22.12"
|
|
4
4
|
description = "A toolkit for SQLAlchemy applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3.12",
|
|
17
17
|
"Programming Language :: Python :: 3.13",
|
|
18
18
|
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Programming Language :: Python :: 3.15",
|
|
19
20
|
"Topic :: Database",
|
|
20
21
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
22
|
"Typing :: Typed",
|
|
@@ -54,7 +55,7 @@ default-groups = ["dev", "docs"]
|
|
|
54
55
|
[dependency-groups]
|
|
55
56
|
dev = [
|
|
56
57
|
"pytest>=9.1.1",
|
|
57
|
-
"ty>=0.0.
|
|
58
|
+
"ty>=0.0.87",
|
|
58
59
|
"ruff>=0.16.6",
|
|
59
60
|
"poethepoet>=0.48.0",
|
|
60
61
|
"anyio>=4.14.2",
|
|
@@ -5,7 +5,7 @@ import inspect
|
|
|
5
5
|
import random
|
|
6
6
|
import threading
|
|
7
7
|
import time
|
|
8
|
-
from collections.abc import Callable, Mapping
|
|
8
|
+
from collections.abc import Callable, Generator, Mapping
|
|
9
9
|
from contextlib import ExitStack, contextmanager
|
|
10
10
|
from contextvars import ContextVar
|
|
11
11
|
from dataclasses import dataclass, replace
|
|
@@ -56,7 +56,7 @@ from .exceptions import (
|
|
|
56
56
|
|
|
57
57
|
if TYPE_CHECKING:
|
|
58
58
|
import logging
|
|
59
|
-
from collections.abc import
|
|
59
|
+
from collections.abc import Sequence
|
|
60
60
|
from os import PathLike
|
|
61
61
|
|
|
62
62
|
from sqlalchemy.engine import Engine
|
|
@@ -232,6 +232,9 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
232
232
|
"""
|
|
233
233
|
|
|
234
234
|
_engine: Any = None # narrowed by the subclass
|
|
235
|
+
# The database to work on instead of this one, or None. Every public member
|
|
236
|
+
# checks it first, so a database without one pays one attribute read.
|
|
237
|
+
_override: Any = None
|
|
235
238
|
|
|
236
239
|
def __init__(
|
|
237
240
|
self,
|
|
@@ -299,6 +302,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
299
302
|
self._listened: Any = None
|
|
300
303
|
self._listening_lock = threading.Lock()
|
|
301
304
|
self._name = alias or DEFAULT_ALIAS
|
|
305
|
+
self._override = None
|
|
302
306
|
|
|
303
307
|
def __repr__(self) -> str:
|
|
304
308
|
return f"{type(self).__name__}({self.url.render_as_string()!r})"
|
|
@@ -332,6 +336,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
332
336
|
MissingSessionError: if no connection is bound.
|
|
333
337
|
|
|
334
338
|
"""
|
|
339
|
+
if self._override is not None:
|
|
340
|
+
return self._override.session
|
|
335
341
|
try:
|
|
336
342
|
scope = self._scope.get()
|
|
337
343
|
except LookupError:
|
|
@@ -358,7 +364,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
358
364
|
skip_queries_from: Sequence[str | PathLike[str]] = (),
|
|
359
365
|
into: Recording | None = None,
|
|
360
366
|
send_to: Callable[[Recording], object] | None = None,
|
|
361
|
-
) ->
|
|
367
|
+
) -> Generator[Recording]:
|
|
362
368
|
"""Record the statements of this block, and what they add up to.
|
|
363
369
|
|
|
364
370
|
```python
|
|
@@ -384,6 +390,18 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
384
390
|
after. `with` is right on either side, awaited or not: it listens, it does
|
|
385
391
|
not run anything.
|
|
386
392
|
"""
|
|
393
|
+
if self._override is not None:
|
|
394
|
+
with self._override.recording(
|
|
395
|
+
label,
|
|
396
|
+
logger=logger,
|
|
397
|
+
echo=echo,
|
|
398
|
+
stacks=stacks,
|
|
399
|
+
skip_queries_from=skip_queries_from,
|
|
400
|
+
into=into,
|
|
401
|
+
send_to=send_to,
|
|
402
|
+
) as recording:
|
|
403
|
+
yield recording
|
|
404
|
+
return
|
|
387
405
|
recording = Recording(label=label) if into is None else into
|
|
388
406
|
self._listen()
|
|
389
407
|
recordings = self._recordings.set((*self._recordings.get(), recording))
|
|
@@ -412,7 +430,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
412
430
|
*,
|
|
413
431
|
at_most: int | None = None,
|
|
414
432
|
duplicates: bool = True,
|
|
415
|
-
) ->
|
|
433
|
+
) -> Generator[Recording]:
|
|
416
434
|
"""Assert what the block asks of this database.
|
|
417
435
|
|
|
418
436
|
```python
|
|
@@ -507,6 +525,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
507
525
|
|
|
508
526
|
False under ``connect()`` and ``autocommit()``, which open none.
|
|
509
527
|
"""
|
|
528
|
+
if self._override is not None:
|
|
529
|
+
return self._override.in_transaction()
|
|
510
530
|
return self._outer.get(None) is not None
|
|
511
531
|
|
|
512
532
|
@property
|
|
@@ -542,11 +562,13 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
542
562
|
this is False in a block that has only run statements on the
|
|
543
563
|
connection. Reading it opens nothing.
|
|
544
564
|
"""
|
|
565
|
+
if self._override is not None:
|
|
566
|
+
return self._override.in_session()
|
|
545
567
|
scope = self._scope.get(None)
|
|
546
568
|
return scope is not None and scope.session is not None
|
|
547
569
|
|
|
548
570
|
@contextmanager
|
|
549
|
-
def unbound(self) ->
|
|
571
|
+
def unbound(self) -> Generator[None]:
|
|
550
572
|
"""Hide the block open around this one, for the code inside to open its own.
|
|
551
573
|
|
|
552
574
|
A test opens a transaction for the whole test, so code that reaches for
|
|
@@ -563,6 +585,10 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
563
585
|
A block opened inside still joins the transaction around it, and rolls
|
|
564
586
|
back with it.
|
|
565
587
|
"""
|
|
588
|
+
if self._override is not None:
|
|
589
|
+
with self._override.unbound():
|
|
590
|
+
yield
|
|
591
|
+
return
|
|
566
592
|
scope = self._scope.get(None)
|
|
567
593
|
if scope is None:
|
|
568
594
|
yield
|
|
@@ -617,9 +643,24 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
617
643
|
outer = self._outer_to_join()
|
|
618
644
|
return outer, savepoint or rollback or bool(outer and outer.savepoint)
|
|
619
645
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
646
|
+
@contextmanager
|
|
647
|
+
def override(self, db: Self) -> Generator[Self]:
|
|
648
|
+
"""Use another database in place of this one until the block ends.
|
|
649
|
+
|
|
650
|
+
```python
|
|
651
|
+
with Database(TEST_URL) as test_db, db.override(test_db):
|
|
652
|
+
create_user("ada") # runs on test_db
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Every thread sees it. The database stays open when the block ends, so
|
|
656
|
+
close it yourself.
|
|
657
|
+
"""
|
|
658
|
+
before = self._override
|
|
659
|
+
self._override = None if db is self else db
|
|
660
|
+
try:
|
|
661
|
+
yield db
|
|
662
|
+
finally:
|
|
663
|
+
self._override = before
|
|
623
664
|
|
|
624
665
|
def _outer_to_join(self) -> _Outer[ConnectionT] | None:
|
|
625
666
|
"""Return the outer transaction new blocks join, if there is one."""
|
|
@@ -816,6 +857,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
816
857
|
self._default: DatabaseT | None = None
|
|
817
858
|
self._aliased: dict[str, DatabaseT] = {}
|
|
818
859
|
self._overridden: dict[str, DatabaseT] = {}
|
|
860
|
+
self._override = None
|
|
819
861
|
self._routers: tuple[Any, ...] = ()
|
|
820
862
|
self._using: ContextVar[str | None] = ContextVar(
|
|
821
863
|
f"{type(self).__name__}.using", default=None
|
|
@@ -824,7 +866,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
824
866
|
def __repr__(self) -> str:
|
|
825
867
|
if self._built_its_own:
|
|
826
868
|
return super().__repr__()
|
|
827
|
-
held = self.
|
|
869
|
+
held = self._override
|
|
828
870
|
if held is None:
|
|
829
871
|
return f"{type(self).__name__}(unconfigured)"
|
|
830
872
|
return f"{type(self).__name__}({held!r})"
|
|
@@ -884,6 +926,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
884
926
|
if self.is_configured:
|
|
885
927
|
raise DefaultAliasError
|
|
886
928
|
self._default = self._named(alias, db)
|
|
929
|
+
self._follow_default()
|
|
887
930
|
return
|
|
888
931
|
if alias in self._aliased:
|
|
889
932
|
raise AliasInUseError(alias)
|
|
@@ -901,7 +944,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
901
944
|
into: Recording | None = None,
|
|
902
945
|
send_to: Callable[[Recording], object] | None = None,
|
|
903
946
|
using: str | DatabaseT | Sequence[str | DatabaseT] | None = None,
|
|
904
|
-
) ->
|
|
947
|
+
) -> Generator[Recording]:
|
|
905
948
|
"""Record every database this registry has, not the default one alone.
|
|
906
949
|
|
|
907
950
|
```python
|
|
@@ -953,7 +996,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
953
996
|
send_to(together)
|
|
954
997
|
|
|
955
998
|
@contextmanager
|
|
956
|
-
def unbound(self) ->
|
|
999
|
+
def unbound(self) -> Generator[None]:
|
|
957
1000
|
"""Hide the block open on every database this registry holds."""
|
|
958
1001
|
with ExitStack() as stack:
|
|
959
1002
|
for alias in self.aliases:
|
|
@@ -981,7 +1024,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
981
1024
|
db: DatabaseT,
|
|
982
1025
|
*,
|
|
983
1026
|
alias: str = DEFAULT_ALIAS,
|
|
984
|
-
) ->
|
|
1027
|
+
) -> Generator[DatabaseT]:
|
|
985
1028
|
"""Use another database under an alias until the block ends.
|
|
986
1029
|
|
|
987
1030
|
```python
|
|
@@ -995,6 +1038,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
995
1038
|
before = self._overridden.get(alias)
|
|
996
1039
|
named = getattr(db, "_name", None)
|
|
997
1040
|
self._overridden[alias] = self._named(alias, db)
|
|
1041
|
+
self._follow_default()
|
|
998
1042
|
try:
|
|
999
1043
|
yield db
|
|
1000
1044
|
finally:
|
|
@@ -1002,13 +1046,14 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1002
1046
|
del self._overridden[alias]
|
|
1003
1047
|
else:
|
|
1004
1048
|
self._overridden[alias] = before
|
|
1049
|
+
self._follow_default()
|
|
1005
1050
|
if named is not None:
|
|
1006
1051
|
db._name = named # noqa: SLF001
|
|
1007
1052
|
|
|
1008
|
-
def
|
|
1009
|
-
"""
|
|
1053
|
+
def _follow_default(self) -> None:
|
|
1054
|
+
"""Work on the database the default alias leads to, unless it is this one."""
|
|
1010
1055
|
held = self._overridden.get(DEFAULT_ALIAS, self._default)
|
|
1011
|
-
|
|
1056
|
+
self._override = None if held is self else held
|
|
1012
1057
|
|
|
1013
1058
|
def using(self, target: str | DatabaseT) -> _Using:
|
|
1014
1059
|
"""Return that database, standing in for the default one.
|
|
@@ -1130,7 +1175,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1130
1175
|
@property
|
|
1131
1176
|
def is_configured(self) -> bool:
|
|
1132
1177
|
"""Whether this registry has a default database to reach."""
|
|
1133
|
-
return "url" in self.__dict__ or self.
|
|
1178
|
+
return "url" in self.__dict__ or self._override is not None
|
|
1134
1179
|
|
|
1135
1180
|
@property
|
|
1136
1181
|
def _built_its_own(self) -> bool:
|
|
@@ -1138,38 +1183,13 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1138
1183
|
return "url" in self.__dict__
|
|
1139
1184
|
|
|
1140
1185
|
if not TYPE_CHECKING:
|
|
1141
|
-
# Hidden from type checkers, which
|
|
1142
|
-
|
|
1143
|
-
def
|
|
1144
|
-
"""
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
which `super()` reaches.
|
|
1149
|
-
"""
|
|
1150
|
-
|
|
1151
|
-
def reach(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
1152
|
-
held = self._stand_in()
|
|
1153
|
-
found = (
|
|
1154
|
-
getattr(held, name) if held is not None else getattr(super(), name)
|
|
1155
|
-
)
|
|
1156
|
-
return found if attribute else found(*args, **kwargs)
|
|
1157
|
-
|
|
1158
|
-
return property(reach) if attribute else reach
|
|
1159
|
-
|
|
1160
|
-
connection = _proxy("connection", attribute=True)
|
|
1161
|
-
engine = _proxy("engine", attribute=True)
|
|
1162
|
-
session = _proxy("session", attribute=True)
|
|
1163
|
-
sql = _proxy("sql", attribute=True)
|
|
1164
|
-
assert_queries = _proxy("assert_queries")
|
|
1165
|
-
in_session = _proxy("in_session")
|
|
1166
|
-
in_transaction = _proxy("in_transaction")
|
|
1167
|
-
ping = _proxy("ping")
|
|
1168
|
-
provisioned_tables = _proxy("provisioned_tables")
|
|
1169
|
-
query = _proxy("query")
|
|
1170
|
-
del _proxy
|
|
1171
|
-
# `connect`, `autocommit`, `session_factory` and `transaction` are not
|
|
1172
|
-
# proxied: the ones inherited look `_stand_in` up as the block opens.
|
|
1186
|
+
# Hidden from type checkers, which read it off `Database`.
|
|
1187
|
+
@property
|
|
1188
|
+
def sql(self) -> Any: # noqa: ANN401
|
|
1189
|
+
"""The templates of the default database, wherever it was built."""
|
|
1190
|
+
if self._built_its_own or self._override is None:
|
|
1191
|
+
return super().sql
|
|
1192
|
+
return self._override.sql
|
|
1173
1193
|
|
|
1174
1194
|
@overload
|
|
1175
1195
|
def configure(
|
|
@@ -29,7 +29,7 @@ from ._base import (
|
|
|
29
29
|
from .exceptions import TransactionRolledBackError
|
|
30
30
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
|
-
from collections.abc import Callable,
|
|
32
|
+
from collections.abc import Callable, Generator, Sequence
|
|
33
33
|
from types import TracebackType
|
|
34
34
|
|
|
35
35
|
from sqlalchemy.engine import Engine
|
|
@@ -98,6 +98,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
98
98
|
@property
|
|
99
99
|
def engine(self) -> Engine:
|
|
100
100
|
"""The underlying engine, created on first access."""
|
|
101
|
+
if self._override is not None:
|
|
102
|
+
return self._override.engine
|
|
101
103
|
if self._engine is None:
|
|
102
104
|
with self._engine_lock:
|
|
103
105
|
# Two threads reaching this at once would each build one, and
|
|
@@ -122,6 +124,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
122
124
|
MissingConnectionError: if no connection is bound.
|
|
123
125
|
|
|
124
126
|
"""
|
|
127
|
+
if self._override is not None:
|
|
128
|
+
return self._override.connection
|
|
125
129
|
return self._reused(self._current_scope())
|
|
126
130
|
|
|
127
131
|
def _create_session(self, connection: sa.Connection) -> Session:
|
|
@@ -147,9 +151,9 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
147
151
|
return cast("sa.Connection", scope.connection)
|
|
148
152
|
|
|
149
153
|
@contextmanager
|
|
150
|
-
def connect(self) ->
|
|
154
|
+
def connect(self) -> Generator[sa.Connection]:
|
|
151
155
|
"""Open a connection and bind it, or reuse the one already bound."""
|
|
152
|
-
elsewhere = self.
|
|
156
|
+
elsewhere = self._override
|
|
153
157
|
if elsewhere is not None:
|
|
154
158
|
with elsewhere.connect() as connection:
|
|
155
159
|
yield connection
|
|
@@ -194,9 +198,9 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
194
198
|
return autocommit
|
|
195
199
|
|
|
196
200
|
@contextmanager
|
|
197
|
-
def _autocommit(self) ->
|
|
201
|
+
def _autocommit(self) -> Generator[sa.Connection]:
|
|
198
202
|
"""Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
|
|
199
|
-
elsewhere = self.
|
|
203
|
+
elsewhere = self._override
|
|
200
204
|
if elsewhere is not None:
|
|
201
205
|
with elsewhere.autocommit() as connection:
|
|
202
206
|
yield connection
|
|
@@ -324,14 +328,14 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
324
328
|
return transaction
|
|
325
329
|
|
|
326
330
|
@contextmanager
|
|
327
|
-
def session_factory(self) ->
|
|
331
|
+
def session_factory(self) -> Generator[Session]:
|
|
328
332
|
"""Open a session for the block, and bind it.
|
|
329
333
|
|
|
330
334
|
The session arrives at once, the connection on its first query or
|
|
331
335
|
flush, as ``sessionmaker()`` does it. Inside another block it runs on
|
|
332
336
|
the connection already bound.
|
|
333
337
|
"""
|
|
334
|
-
elsewhere = self.
|
|
338
|
+
elsewhere = self._override
|
|
335
339
|
if elsewhere is not None:
|
|
336
340
|
with elsewhere.session_factory() as session:
|
|
337
341
|
yield session
|
|
@@ -361,7 +365,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
361
365
|
*,
|
|
362
366
|
commit: bool = False,
|
|
363
367
|
autocommit: bool = False,
|
|
364
|
-
) ->
|
|
368
|
+
) -> Generator[sa.Connection]:
|
|
365
369
|
"""Bind ``connection`` for the block, ending the session it opened.
|
|
366
370
|
|
|
367
371
|
``commit`` keeps that session's work. Closing a session that isolates
|
|
@@ -385,7 +389,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
385
389
|
metadata: sa.MetaData,
|
|
386
390
|
*,
|
|
387
391
|
tables: Sequence[sa.Table] | None = None,
|
|
388
|
-
) ->
|
|
392
|
+
) -> Generator[None]:
|
|
389
393
|
"""Create these tables here, and drop them when the block ends.
|
|
390
394
|
|
|
391
395
|
A test session opens this once, around everything that needs a schema:
|
|
@@ -400,6 +404,10 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
400
404
|
Every table of the metadata unless ``tables`` names fewer, as a second
|
|
401
405
|
database wants.
|
|
402
406
|
"""
|
|
407
|
+
if self._override is not None:
|
|
408
|
+
with self._override.provisioned_tables(metadata, tables=tables):
|
|
409
|
+
yield
|
|
410
|
+
return
|
|
403
411
|
with self.transaction() as connection:
|
|
404
412
|
metadata.create_all(connection, tables=tables)
|
|
405
413
|
try:
|
|
@@ -410,6 +418,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
410
418
|
|
|
411
419
|
def ping(self) -> bool:
|
|
412
420
|
"""Whether the database answers."""
|
|
421
|
+
if self._override is not None:
|
|
422
|
+
return self._override.ping()
|
|
413
423
|
try:
|
|
414
424
|
with self.engine.connect() as connection:
|
|
415
425
|
connection.execute(sa.text("SELECT 1"))
|
|
@@ -487,7 +497,7 @@ class Transaction(ContextDecorator, AbstractContextManager["sa.Connection"]):
|
|
|
487
497
|
|
|
488
498
|
def __enter__(self) -> sa.Connection:
|
|
489
499
|
stack = ExitStack()
|
|
490
|
-
elsewhere = self.db.
|
|
500
|
+
elsewhere = self.db._override # noqa: SLF001
|
|
491
501
|
if elsewhere is not None:
|
|
492
502
|
connection = stack.enter_context(
|
|
493
503
|
elsewhere.transaction(
|
|
@@ -53,7 +53,7 @@ DatabaseT = TypeVar("DatabaseT", bound="BaseDatabase[Any, Any]")
|
|
|
53
53
|
class DatabaseSource(Protocol):
|
|
54
54
|
"""The registry an alias is looked up in: the importable one, or a stand-in."""
|
|
55
55
|
|
|
56
|
-
def __getitem__(self, alias: str) -> BaseDatabase[Any, Any]: ...
|
|
56
|
+
def __getitem__(self, alias: str, /) -> BaseDatabase[Any, Any]: ...
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
class DatabaseDescriptor(Generic[DatabaseT]):
|
|
@@ -8,7 +8,7 @@ from ._base import _DatabaseRegistryMixin, default_backoff, late_bound
|
|
|
8
8
|
from ._db import Database, RetryingTransaction
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from collections.abc import Callable,
|
|
11
|
+
from collections.abc import Callable, Generator
|
|
12
12
|
|
|
13
13
|
from ._base import RetryOn
|
|
14
14
|
from ._db import Transaction
|
|
@@ -40,7 +40,7 @@ class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
|
40
40
|
def transactions(
|
|
41
41
|
self,
|
|
42
42
|
**arguments: Any, # noqa: ANN401
|
|
43
|
-
) ->
|
|
43
|
+
) -> Generator[None]:
|
|
44
44
|
"""Open a transaction on every database, not the default one alone.
|
|
45
45
|
|
|
46
46
|
A single database needs `transaction(rollback=True)`. A test harness
|
|
@@ -6,7 +6,7 @@ import importlib.util
|
|
|
6
6
|
import inspect
|
|
7
7
|
import itertools
|
|
8
8
|
import re
|
|
9
|
-
from collections.abc import Mapping, Sized
|
|
9
|
+
from collections.abc import Generator, Mapping, Sized
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from contextvars import ContextVar
|
|
12
12
|
from dataclasses import dataclass, is_dataclass
|
|
@@ -49,7 +49,7 @@ from .exceptions import (
|
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
if TYPE_CHECKING:
|
|
52
|
-
from collections.abc import Callable, Iterable,
|
|
52
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
53
53
|
|
|
54
54
|
from pydantic import BaseModel, TypeAdapter
|
|
55
55
|
from sqlalchemy.sql import Executable
|
|
@@ -86,7 +86,7 @@ _calls_kept: ContextVar[bool] = ContextVar("sqlakit.calls_kept", default=False)
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
@contextmanager
|
|
89
|
-
def calls_kept() ->
|
|
89
|
+
def calls_kept() -> Generator[None]:
|
|
90
90
|
"""Write a call that cannot be made with the values at hand as the call itself.
|
|
91
91
|
|
|
92
92
|
An editor shows a template rendered with made-up values, or with none: a
|
|
@@ -37,7 +37,7 @@ from sqlakit._base import (
|
|
|
37
37
|
from sqlakit.exceptions import MissingConnectionError, TransactionRolledBackError
|
|
38
38
|
|
|
39
39
|
if TYPE_CHECKING:
|
|
40
|
-
from collections.abc import
|
|
40
|
+
from collections.abc import AsyncGenerator, Callable, Coroutine, Sequence
|
|
41
41
|
from types import TracebackType
|
|
42
42
|
|
|
43
43
|
from sqlalchemy.ext.asyncio import AsyncTransaction
|
|
@@ -102,6 +102,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
102
102
|
@property
|
|
103
103
|
def engine(self) -> AsyncEngine:
|
|
104
104
|
"""The underlying engine, created on first access."""
|
|
105
|
+
if self._override is not None:
|
|
106
|
+
return self._override.engine
|
|
105
107
|
if self._engine is None:
|
|
106
108
|
with self._engine_lock:
|
|
107
109
|
# Two threads reaching this at once would each build one, and
|
|
@@ -127,6 +129,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
127
129
|
MissingConnectionError: if no connection is bound.
|
|
128
130
|
|
|
129
131
|
"""
|
|
132
|
+
if self._override is not None:
|
|
133
|
+
return self._override.connection
|
|
130
134
|
scope = self._current_scope()
|
|
131
135
|
if scope.connection is None:
|
|
132
136
|
cell = scope.checkout
|
|
@@ -143,6 +147,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
143
147
|
|
|
144
148
|
async def _aconnection(self) -> AsyncConnection:
|
|
145
149
|
"""Return the bound connection, checking it out first if lazy."""
|
|
150
|
+
if self._override is not None:
|
|
151
|
+
return await self._override._aconnection() # noqa: SLF001
|
|
146
152
|
return await self._areused(self._current_scope())
|
|
147
153
|
|
|
148
154
|
def _create_session(self, connection: AsyncConnection) -> AsyncSession:
|
|
@@ -181,9 +187,9 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
181
187
|
return cast("AsyncConnection", scope.connection)
|
|
182
188
|
|
|
183
189
|
@asynccontextmanager
|
|
184
|
-
async def connect(self) ->
|
|
190
|
+
async def connect(self) -> AsyncGenerator[AsyncConnection]:
|
|
185
191
|
"""Open a connection and bind it, or reuse the one already bound."""
|
|
186
|
-
elsewhere = self.
|
|
192
|
+
elsewhere = self._override
|
|
187
193
|
if elsewhere is not None:
|
|
188
194
|
async with elsewhere.connect() as connection:
|
|
189
195
|
yield connection
|
|
@@ -228,9 +234,9 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
228
234
|
return autocommit
|
|
229
235
|
|
|
230
236
|
@asynccontextmanager
|
|
231
|
-
async def _autocommit(self) ->
|
|
237
|
+
async def _autocommit(self) -> AsyncGenerator[AsyncConnection]:
|
|
232
238
|
"""Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
|
|
233
|
-
elsewhere = self.
|
|
239
|
+
elsewhere = self._override
|
|
234
240
|
if elsewhere is not None:
|
|
235
241
|
async with elsewhere.autocommit() as connection:
|
|
236
242
|
yield connection
|
|
@@ -358,14 +364,14 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
358
364
|
return transaction
|
|
359
365
|
|
|
360
366
|
@asynccontextmanager
|
|
361
|
-
async def session_factory(self) ->
|
|
367
|
+
async def session_factory(self) -> AsyncGenerator[AsyncSession]:
|
|
362
368
|
"""Open a session for the block, and bind it.
|
|
363
369
|
|
|
364
370
|
The session arrives at once, the connection on its first query or
|
|
365
371
|
flush, as ``async_sessionmaker()`` does it. Inside another block it
|
|
366
372
|
runs on the connection already bound.
|
|
367
373
|
"""
|
|
368
|
-
elsewhere = self.
|
|
374
|
+
elsewhere = self._override
|
|
369
375
|
if elsewhere is not None:
|
|
370
376
|
async with elsewhere.session_factory() as session:
|
|
371
377
|
yield session
|
|
@@ -395,7 +401,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
395
401
|
*,
|
|
396
402
|
commit: bool = False,
|
|
397
403
|
autocommit: bool = False,
|
|
398
|
-
) ->
|
|
404
|
+
) -> AsyncGenerator[AsyncConnection]:
|
|
399
405
|
"""Bind ``connection`` for the block, ending the session it opened.
|
|
400
406
|
|
|
401
407
|
``commit`` keeps that session's work. Closing a session that isolates
|
|
@@ -419,7 +425,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
419
425
|
metadata: sa.MetaData,
|
|
420
426
|
*,
|
|
421
427
|
tables: Sequence[sa.Table] | None = None,
|
|
422
|
-
) ->
|
|
428
|
+
) -> AsyncGenerator[None]:
|
|
423
429
|
"""Create these tables here, and drop them when the block ends.
|
|
424
430
|
|
|
425
431
|
A test session opens this once, around everything that needs a schema:
|
|
@@ -434,6 +440,10 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
434
440
|
Every table of the metadata unless ``tables`` names fewer, as a second
|
|
435
441
|
database wants.
|
|
436
442
|
"""
|
|
443
|
+
if self._override is not None:
|
|
444
|
+
async with self._override.provisioned_tables(metadata, tables=tables):
|
|
445
|
+
yield
|
|
446
|
+
return
|
|
437
447
|
async with self.transaction() as connection:
|
|
438
448
|
await connection.run_sync(metadata.create_all, tables=tables)
|
|
439
449
|
try:
|
|
@@ -444,6 +454,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
444
454
|
|
|
445
455
|
async def ping(self) -> bool:
|
|
446
456
|
"""Whether the database answers."""
|
|
457
|
+
if self._override is not None:
|
|
458
|
+
return await self._override.ping()
|
|
447
459
|
try:
|
|
448
460
|
async with self.engine.connect() as connection:
|
|
449
461
|
await connection.execute(sa.text("SELECT 1"))
|
|
@@ -527,7 +539,7 @@ class Transaction(
|
|
|
527
539
|
|
|
528
540
|
async def __aenter__(self) -> AsyncConnection:
|
|
529
541
|
stack = AsyncExitStack()
|
|
530
|
-
elsewhere = self.db.
|
|
542
|
+
elsewhere = self.db._override # noqa: SLF001
|
|
531
543
|
if elsewhere is not None:
|
|
532
544
|
connection = await stack.enter_async_context(
|
|
533
545
|
elsewhere.transaction(
|
|
@@ -9,7 +9,7 @@ from sqlakit._base import _DatabaseRegistryMixin, default_backoff, late_bound
|
|
|
9
9
|
from ._db import Database, RetryingTransaction
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from collections.abc import
|
|
12
|
+
from collections.abc import AsyncGenerator, Callable
|
|
13
13
|
|
|
14
14
|
from sqlakit._base import RetryOn
|
|
15
15
|
|
|
@@ -41,7 +41,7 @@ class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
|
41
41
|
async def transactions(
|
|
42
42
|
self,
|
|
43
43
|
**arguments: Any, # noqa: ANN401
|
|
44
|
-
) ->
|
|
44
|
+
) -> AsyncGenerator[None]:
|
|
45
45
|
"""Open a transaction on every database, not the default one alone.
|
|
46
46
|
|
|
47
47
|
A test harness with more than one database needs this, where a single
|
|
@@ -42,10 +42,16 @@ from sqlakit._query import (
|
|
|
42
42
|
from sqlakit.exceptions import InstanceNotFoundError
|
|
43
43
|
|
|
44
44
|
from ._db import Database
|
|
45
|
-
from ._registry import
|
|
45
|
+
from ._registry import db
|
|
46
46
|
|
|
47
47
|
if TYPE_CHECKING:
|
|
48
|
-
from collections.abc import
|
|
48
|
+
from collections.abc import (
|
|
49
|
+
AsyncGenerator,
|
|
50
|
+
AsyncIterator,
|
|
51
|
+
Iterable,
|
|
52
|
+
Mapping,
|
|
53
|
+
Sequence,
|
|
54
|
+
)
|
|
49
55
|
|
|
50
56
|
from sqlalchemy.engine import CursorResult, Result, ScalarResult
|
|
51
57
|
from sqlalchemy.orm import InstrumentedAttribute
|
|
@@ -611,8 +617,8 @@ class QueryDescriptor(Generic[QueryT]):
|
|
|
611
617
|
def __init__(self, query_class: type[QueryT]) -> None:
|
|
612
618
|
self.query_class = query_class
|
|
613
619
|
|
|
614
|
-
# `
|
|
615
|
-
#
|
|
620
|
+
# `QueryDescriptor(Query)` reads as a query of the model it is read from,
|
|
621
|
+
# any other query class as itself.
|
|
616
622
|
@overload
|
|
617
623
|
def __get__(
|
|
618
624
|
self: QueryDescriptor[Query[Any]],
|
|
@@ -644,16 +650,18 @@ class ModelMixin(BaseModel[Database]):
|
|
|
644
650
|
[`set_db`][sqlakit.asyncio.orm.ModelMixin.set_db].
|
|
645
651
|
"""
|
|
646
652
|
|
|
647
|
-
__db__
|
|
648
|
-
__dbs__
|
|
653
|
+
__db__ = DEFAULT_ALIAS
|
|
654
|
+
__dbs__ = db
|
|
649
655
|
|
|
650
|
-
|
|
656
|
+
# Not annotated: a model's `query = UserQuery.as_descriptor()` would take
|
|
657
|
+
# the declared type and read as `Query[User]`.
|
|
658
|
+
query = QueryDescriptor(Query)
|
|
651
659
|
|
|
652
660
|
db: ClassVar[DatabaseDescriptor[Database]] = DatabaseDescriptor()
|
|
653
661
|
|
|
654
662
|
@classmethod
|
|
655
663
|
@asynccontextmanager
|
|
656
|
-
async def provisioned_tables(cls, alias: str | None = None) ->
|
|
664
|
+
async def provisioned_tables(cls, alias: str | None = None) -> AsyncGenerator[None]:
|
|
657
665
|
"""Create the tables that belong on this model's database, and drop them after.
|
|
658
666
|
|
|
659
667
|
A test session opens this once, around everything that needs a schema:
|
|
@@ -668,7 +676,7 @@ class ModelMixin(BaseModel[Database]):
|
|
|
668
676
|
With models on more than one database, name the alias, and each database
|
|
669
677
|
gets the tables of the models pointed at it.
|
|
670
678
|
"""
|
|
671
|
-
db = cls.db if alias is None else cls.
|
|
679
|
+
db = cls.db if alias is None else cls.dbs[alias]
|
|
672
680
|
async with db.provisioned_tables(cls.metadata, tables=tables_for(cls, db)):
|
|
673
681
|
yield
|
|
674
682
|
|
|
@@ -256,9 +256,7 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
|
|
|
256
256
|
|
|
257
257
|
"""
|
|
258
258
|
require_pydantic()
|
|
259
|
-
return
|
|
260
|
-
"SQLRows[OtherT]", self._as(SQLRows, type_=type_, validation=validation)
|
|
261
|
-
)
|
|
259
|
+
return self._as(SQLRows, type_=type_, validation=validation)
|
|
262
260
|
|
|
263
261
|
def scalars(self) -> SQLRows[Any]:
|
|
264
262
|
"""Read the first column of each row instead of whole rows.
|
|
@@ -40,11 +40,11 @@ from ._query import (
|
|
|
40
40
|
orderable,
|
|
41
41
|
ordered,
|
|
42
42
|
)
|
|
43
|
-
from ._registry import
|
|
43
|
+
from ._registry import db
|
|
44
44
|
from .exceptions import InstanceNotFoundError
|
|
45
45
|
|
|
46
46
|
if TYPE_CHECKING:
|
|
47
|
-
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
47
|
+
from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence
|
|
48
48
|
|
|
49
49
|
from sqlalchemy.engine import CursorResult, Result, ScalarResult
|
|
50
50
|
from sqlalchemy.orm import InstrumentedAttribute
|
|
@@ -593,8 +593,8 @@ class QueryDescriptor(Generic[QueryT]):
|
|
|
593
593
|
def __init__(self, query_class: type[QueryT]) -> None:
|
|
594
594
|
self.query_class = query_class
|
|
595
595
|
|
|
596
|
-
# `
|
|
597
|
-
#
|
|
596
|
+
# `QueryDescriptor(Query)` reads as a query of the model it is read from,
|
|
597
|
+
# any other query class as itself.
|
|
598
598
|
@overload
|
|
599
599
|
def __get__(
|
|
600
600
|
self: QueryDescriptor[Query[Any]],
|
|
@@ -625,16 +625,18 @@ class ModelMixin(BaseModel[Database]):
|
|
|
625
625
|
importable registry, or one set with [`set_db`][sqlakit.orm.ModelMixin.set_db].
|
|
626
626
|
"""
|
|
627
627
|
|
|
628
|
-
__db__
|
|
629
|
-
__dbs__
|
|
628
|
+
__db__ = DEFAULT_ALIAS
|
|
629
|
+
__dbs__ = db
|
|
630
630
|
|
|
631
|
-
|
|
631
|
+
# Not annotated: a model's `query = UserQuery.as_descriptor()` would take
|
|
632
|
+
# the declared type and read as `Query[User]`.
|
|
633
|
+
query = QueryDescriptor(Query)
|
|
632
634
|
|
|
633
635
|
db: ClassVar[DatabaseDescriptor[Database]] = DatabaseDescriptor()
|
|
634
636
|
|
|
635
637
|
@classmethod
|
|
636
638
|
@contextmanager
|
|
637
|
-
def provisioned_tables(cls, alias: str | None = None) ->
|
|
639
|
+
def provisioned_tables(cls, alias: str | None = None) -> Generator[None]:
|
|
638
640
|
"""Create the tables that belong on this model's database, and drop them after.
|
|
639
641
|
|
|
640
642
|
A test session opens this once, around everything that needs a schema:
|
|
@@ -649,7 +651,7 @@ class ModelMixin(BaseModel[Database]):
|
|
|
649
651
|
With models on more than one database, name the alias, and each database
|
|
650
652
|
gets the tables of the models pointed at it.
|
|
651
653
|
"""
|
|
652
|
-
db = cls.db if alias is None else cls.
|
|
654
|
+
db = cls.db if alias is None else cls.dbs[alias]
|
|
653
655
|
with db.provisioned_tables(cls.metadata, tables=tables_for(cls, db)):
|
|
654
656
|
yield
|
|
655
657
|
|
|
@@ -55,7 +55,7 @@ from ._registry import db as importable_db
|
|
|
55
55
|
from .exceptions import UnknownDatabaseError
|
|
56
56
|
|
|
57
57
|
if TYPE_CHECKING:
|
|
58
|
-
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
58
|
+
from collections.abc import AsyncIterator, Generator, Iterator, Sequence
|
|
59
59
|
|
|
60
60
|
import sqlalchemy as sa
|
|
61
61
|
|
|
@@ -346,7 +346,7 @@ def assert_queries(sqlakit_db: Any) -> AssertQueries: # noqa: ANN401
|
|
|
346
346
|
at_most: int | None = None,
|
|
347
347
|
duplicates: bool = True,
|
|
348
348
|
using: Any = None, # noqa: ANN401
|
|
349
|
-
) ->
|
|
349
|
+
) -> Generator[Recording]:
|
|
350
350
|
require_expectation(count, at_most, duplicates)
|
|
351
351
|
recording = Recording()
|
|
352
352
|
with ExitStack() as stack:
|
|
@@ -402,7 +402,7 @@ async def _sqlakit_async_transaction(
|
|
|
402
402
|
|
|
403
403
|
|
|
404
404
|
@contextmanager
|
|
405
|
-
def _reported(request: pytest.FixtureRequest, db: Any) ->
|
|
405
|
+
def _reported(request: pytest.FixtureRequest, db: Any) -> Generator[None]: # noqa: ANN401
|
|
406
406
|
"""Record what the test runs, for the report, when one was asked for.
|
|
407
407
|
|
|
408
408
|
The recording is named after the test and filed under the file it lives in,
|
|
@@ -435,7 +435,7 @@ def _named(node: pytest.Item) -> str:
|
|
|
435
435
|
|
|
436
436
|
|
|
437
437
|
@contextmanager
|
|
438
|
-
def _entered(blocks: list[Any]) ->
|
|
438
|
+
def _entered(blocks: list[Any]) -> Generator[None]:
|
|
439
439
|
"""Hold these blocks open, awaiting the ones that need it.
|
|
440
440
|
|
|
441
441
|
An `asyncio` database builds its schema in a coroutine, and this fixture is
|
|
@@ -246,9 +246,7 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
|
|
|
246
246
|
|
|
247
247
|
"""
|
|
248
248
|
require_pydantic()
|
|
249
|
-
return
|
|
250
|
-
"SQLRows[OtherT]", self._as(SQLRows, type_=type_, validation=validation)
|
|
251
|
-
)
|
|
249
|
+
return self._as(SQLRows, type_=type_, validation=validation)
|
|
252
250
|
|
|
253
251
|
def scalars(self) -> SQLRows[Any]:
|
|
254
252
|
"""Read the first column of each row instead of whole rows.
|
|
@@ -12,7 +12,7 @@ from .exceptions import UnknownDatabaseError
|
|
|
12
12
|
from .types import AssertQueries
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from collections.abc import
|
|
15
|
+
from collections.abc import Generator
|
|
16
16
|
|
|
17
17
|
__all__ = ["AssertQueries", "assert_queries"]
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ def assert_queries(
|
|
|
24
24
|
at_most: int | None = None,
|
|
25
25
|
duplicates: bool = True,
|
|
26
26
|
using: str | Any = None, # noqa: ANN401
|
|
27
|
-
) ->
|
|
27
|
+
) -> Generator[Recording]:
|
|
28
28
|
"""Assert what the block asks of the database.
|
|
29
29
|
|
|
30
30
|
```python
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|