sqlakit 0.22.11__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.
Files changed (34) hide show
  1. {sqlakit-0.22.11 → sqlakit-0.22.12}/PKG-INFO +4 -3
  2. {sqlakit-0.22.11 → sqlakit-0.22.12}/README.md +2 -2
  3. {sqlakit-0.22.11 → sqlakit-0.22.12}/pyproject.toml +3 -2
  4. {sqlakit-0.22.11 → sqlakit-0.22.12}/pyproject.toml.orig +3 -2
  5. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_base.py +9 -9
  6. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_db.py +6 -6
  7. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_model.py +1 -1
  8. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_registry.py +2 -2
  9. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_sql.py +3 -3
  10. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/asyncio/_db.py +6 -6
  11. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/asyncio/_registry.py +2 -2
  12. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/asyncio/orm.py +17 -9
  13. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/asyncio/sql.py +1 -3
  14. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/orm.py +11 -9
  15. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/pytest_plugin.py +4 -4
  16. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/sql.py +1 -3
  17. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/testing.py +2 -2
  18. {sqlakit-0.22.11 → sqlakit-0.22.12}/LICENSE +0 -0
  19. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/__init__.py +0 -0
  20. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_cli.py +0 -0
  21. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_discovery.py +0 -0
  22. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_linters.py +0 -0
  23. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_project.py +0 -0
  24. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_pycharm.py +0 -0
  25. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_query.py +0 -0
  26. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_recording.py +0 -0
  27. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_render.py +0 -0
  28. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_routing.py +0 -0
  29. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_sql_formatter.py +0 -0
  30. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/_static.py +0 -0
  31. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/asyncio/__init__.py +0 -0
  32. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/exceptions.py +0 -0
  33. {sqlakit-0.22.11 → sqlakit-0.22.12}/sqlakit/py.typed +0 -0
  34. {sqlakit-0.22.11 → 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.11
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 AsyncIterator
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) -> AsyncIterator[None]:
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 AsyncIterator
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) -> AsyncIterator[None]:
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.11"
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.84",
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.11"
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.84",
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 Iterator, Sequence
59
+ from collections.abc import Sequence
60
60
  from os import PathLike
61
61
 
62
62
  from sqlalchemy.engine import Engine
@@ -364,7 +364,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
364
364
  skip_queries_from: Sequence[str | PathLike[str]] = (),
365
365
  into: Recording | None = None,
366
366
  send_to: Callable[[Recording], object] | None = None,
367
- ) -> Iterator[Recording]:
367
+ ) -> Generator[Recording]:
368
368
  """Record the statements of this block, and what they add up to.
369
369
 
370
370
  ```python
@@ -430,7 +430,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
430
430
  *,
431
431
  at_most: int | None = None,
432
432
  duplicates: bool = True,
433
- ) -> Iterator[Recording]:
433
+ ) -> Generator[Recording]:
434
434
  """Assert what the block asks of this database.
435
435
 
436
436
  ```python
@@ -568,7 +568,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
568
568
  return scope is not None and scope.session is not None
569
569
 
570
570
  @contextmanager
571
- def unbound(self) -> Iterator[None]:
571
+ def unbound(self) -> Generator[None]:
572
572
  """Hide the block open around this one, for the code inside to open its own.
573
573
 
574
574
  A test opens a transaction for the whole test, so code that reaches for
@@ -644,7 +644,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
644
644
  return outer, savepoint or rollback or bool(outer and outer.savepoint)
645
645
 
646
646
  @contextmanager
647
- def override(self, db: Self) -> Iterator[Self]:
647
+ def override(self, db: Self) -> Generator[Self]:
648
648
  """Use another database in place of this one until the block ends.
649
649
 
650
650
  ```python
@@ -944,7 +944,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
944
944
  into: Recording | None = None,
945
945
  send_to: Callable[[Recording], object] | None = None,
946
946
  using: str | DatabaseT | Sequence[str | DatabaseT] | None = None,
947
- ) -> Iterator[Recording]:
947
+ ) -> Generator[Recording]:
948
948
  """Record every database this registry has, not the default one alone.
949
949
 
950
950
  ```python
@@ -996,7 +996,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
996
996
  send_to(together)
997
997
 
998
998
  @contextmanager
999
- def unbound(self) -> Iterator[None]:
999
+ def unbound(self) -> Generator[None]:
1000
1000
  """Hide the block open on every database this registry holds."""
1001
1001
  with ExitStack() as stack:
1002
1002
  for alias in self.aliases:
@@ -1024,7 +1024,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1024
1024
  db: DatabaseT,
1025
1025
  *,
1026
1026
  alias: str = DEFAULT_ALIAS,
1027
- ) -> Iterator[DatabaseT]:
1027
+ ) -> Generator[DatabaseT]:
1028
1028
  """Use another database under an alias until the block ends.
1029
1029
 
1030
1030
  ```python
@@ -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, Iterator, Sequence
32
+ from collections.abc import Callable, Generator, Sequence
33
33
  from types import TracebackType
34
34
 
35
35
  from sqlalchemy.engine import Engine
@@ -151,7 +151,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
151
151
  return cast("sa.Connection", scope.connection)
152
152
 
153
153
  @contextmanager
154
- def connect(self) -> Iterator[sa.Connection]:
154
+ def connect(self) -> Generator[sa.Connection]:
155
155
  """Open a connection and bind it, or reuse the one already bound."""
156
156
  elsewhere = self._override
157
157
  if elsewhere is not None:
@@ -198,7 +198,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
198
198
  return autocommit
199
199
 
200
200
  @contextmanager
201
- def _autocommit(self) -> Iterator[sa.Connection]:
201
+ def _autocommit(self) -> Generator[sa.Connection]:
202
202
  """Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
203
203
  elsewhere = self._override
204
204
  if elsewhere is not None:
@@ -328,7 +328,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
328
328
  return transaction
329
329
 
330
330
  @contextmanager
331
- def session_factory(self) -> Iterator[Session]:
331
+ def session_factory(self) -> Generator[Session]:
332
332
  """Open a session for the block, and bind it.
333
333
 
334
334
  The session arrives at once, the connection on its first query or
@@ -365,7 +365,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
365
365
  *,
366
366
  commit: bool = False,
367
367
  autocommit: bool = False,
368
- ) -> Iterator[sa.Connection]:
368
+ ) -> Generator[sa.Connection]:
369
369
  """Bind ``connection`` for the block, ending the session it opened.
370
370
 
371
371
  ``commit`` keeps that session's work. Closing a session that isolates
@@ -389,7 +389,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
389
389
  metadata: sa.MetaData,
390
390
  *,
391
391
  tables: Sequence[sa.Table] | None = None,
392
- ) -> Iterator[None]:
392
+ ) -> Generator[None]:
393
393
  """Create these tables here, and drop them when the block ends.
394
394
 
395
395
  A test session opens this once, around everything that needs a schema:
@@ -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, Iterator
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
- ) -> Iterator[None]:
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, Iterator, Sequence
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() -> Iterator[None]:
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 AsyncIterator, Callable, Coroutine, Sequence
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
@@ -187,7 +187,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
187
187
  return cast("AsyncConnection", scope.connection)
188
188
 
189
189
  @asynccontextmanager
190
- async def connect(self) -> AsyncIterator[AsyncConnection]:
190
+ async def connect(self) -> AsyncGenerator[AsyncConnection]:
191
191
  """Open a connection and bind it, or reuse the one already bound."""
192
192
  elsewhere = self._override
193
193
  if elsewhere is not None:
@@ -234,7 +234,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
234
234
  return autocommit
235
235
 
236
236
  @asynccontextmanager
237
- async def _autocommit(self) -> AsyncIterator[AsyncConnection]:
237
+ async def _autocommit(self) -> AsyncGenerator[AsyncConnection]:
238
238
  """Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
239
239
  elsewhere = self._override
240
240
  if elsewhere is not None:
@@ -364,7 +364,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
364
364
  return transaction
365
365
 
366
366
  @asynccontextmanager
367
- async def session_factory(self) -> AsyncIterator[AsyncSession]:
367
+ async def session_factory(self) -> AsyncGenerator[AsyncSession]:
368
368
  """Open a session for the block, and bind it.
369
369
 
370
370
  The session arrives at once, the connection on its first query or
@@ -401,7 +401,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
401
401
  *,
402
402
  commit: bool = False,
403
403
  autocommit: bool = False,
404
- ) -> AsyncIterator[AsyncConnection]:
404
+ ) -> AsyncGenerator[AsyncConnection]:
405
405
  """Bind ``connection`` for the block, ending the session it opened.
406
406
 
407
407
  ``commit`` keeps that session's work. Closing a session that isolates
@@ -425,7 +425,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
425
425
  metadata: sa.MetaData,
426
426
  *,
427
427
  tables: Sequence[sa.Table] | None = None,
428
- ) -> AsyncIterator[None]:
428
+ ) -> AsyncGenerator[None]:
429
429
  """Create these tables here, and drop them when the block ends.
430
430
 
431
431
  A test session opens this once, around everything that needs a schema:
@@ -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 AsyncIterator, Callable
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
- ) -> AsyncIterator[None]:
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 Databases, db
45
+ from ._registry import db
46
46
 
47
47
  if TYPE_CHECKING:
48
- from collections.abc import AsyncIterator, Iterable, Mapping, Sequence
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
- # `ClassVar` cannot hold a type variable, so the base declares the query
615
- # every model has and a read narrows it to the model it is read from.
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__: ClassVar[str | Database] = DEFAULT_ALIAS
648
- __dbs__: ClassVar[Databases] = db
653
+ __db__ = DEFAULT_ALIAS
654
+ __dbs__ = db
649
655
 
650
- query: ClassVar[QueryDescriptor[Query[Any]]] = QueryDescriptor(Query)
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) -> AsyncIterator[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.__dbs__[alias]
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 cast(
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 Databases, db
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
- # `ClassVar` cannot hold a type variable, so the base declares the query
597
- # every model has and a read narrows it to the model it is read from.
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__: ClassVar[str | Database] = DEFAULT_ALIAS
629
- __dbs__: ClassVar[Databases] = db
628
+ __db__ = DEFAULT_ALIAS
629
+ __dbs__ = db
630
630
 
631
- query: ClassVar[QueryDescriptor[Query[Any]]] = QueryDescriptor(Query)
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) -> Iterator[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.__dbs__[alias]
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
- ) -> Iterator[Recording]:
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) -> Iterator[None]: # noqa: ANN401
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]) -> Iterator[None]:
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 cast(
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 Iterator
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
- ) -> Iterator[Recording]:
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