sqlakit 0.5.1__tar.gz → 0.7.0__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.5.1 → sqlakit-0.7.0}/PKG-INFO +1 -1
- {sqlakit-0.5.1 → sqlakit-0.7.0}/pyproject.toml +1 -1
- {sqlakit-0.5.1 → sqlakit-0.7.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/__init__.py +12 -1
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_model.py +20 -5
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_query.py +83 -24
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/asyncio/orm.py +11 -1
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/exceptions.py +15 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/orm.py +13 -1
- {sqlakit-0.5.1 → sqlakit-0.7.0}/LICENSE +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/README.md +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.5.1 → sqlakit-0.7.0}/sqlakit/types.py +0 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
from ._base import DEFAULT_ENGINE_ARGS, DEFAULT_SESSION_ARGS
|
|
2
2
|
from ._db import Database, RetryingTransaction, Transaction
|
|
3
3
|
from ._discovery import import_models, import_string
|
|
4
|
-
from ._query import
|
|
4
|
+
from ._query import (
|
|
5
|
+
CASE_INSENSITIVE_COLLATIONS,
|
|
6
|
+
CursorPage,
|
|
7
|
+
OrderBy,
|
|
8
|
+
Page,
|
|
9
|
+
UncountedPage,
|
|
10
|
+
orderable_columns,
|
|
11
|
+
)
|
|
5
12
|
from ._recording import Recording, Statement
|
|
6
13
|
from ._registry import Databases, db
|
|
7
14
|
from ._routing import Router
|
|
@@ -25,6 +32,7 @@ from .exceptions import (
|
|
|
25
32
|
MissingDatabaseUrlError,
|
|
26
33
|
MissingDefaultDatabaseError,
|
|
27
34
|
MissingDependencyError,
|
|
35
|
+
MissingRegistryError,
|
|
28
36
|
MissingSessionError,
|
|
29
37
|
MultipleInstancesFoundError,
|
|
30
38
|
NullCursorValueError,
|
|
@@ -80,6 +88,7 @@ __all__ = [
|
|
|
80
88
|
"MissingDatabaseUrlError",
|
|
81
89
|
"MissingDefaultDatabaseError",
|
|
82
90
|
"MissingDependencyError",
|
|
91
|
+
"MissingRegistryError",
|
|
83
92
|
"MissingSessionError",
|
|
84
93
|
"MultipleInstancesFoundError",
|
|
85
94
|
"NullCursorValueError",
|
|
@@ -102,6 +111,7 @@ __all__ = [
|
|
|
102
111
|
"Transaction",
|
|
103
112
|
"TransactionRolledBackError",
|
|
104
113
|
"UncomparableOrderingError",
|
|
114
|
+
"UncountedPage",
|
|
105
115
|
"UnknownDatabaseError",
|
|
106
116
|
"UnknownFieldError",
|
|
107
117
|
"UnknownImportPathError",
|
|
@@ -111,4 +121,5 @@ __all__ = [
|
|
|
111
121
|
"db",
|
|
112
122
|
"import_models",
|
|
113
123
|
"import_string",
|
|
124
|
+
"orderable_columns",
|
|
114
125
|
]
|
|
@@ -22,7 +22,7 @@ from sqlalchemy.orm.attributes import set_committed_value
|
|
|
22
22
|
from .exceptions import (
|
|
23
23
|
DEFAULT_ALIAS,
|
|
24
24
|
DetachedInstanceError,
|
|
25
|
-
|
|
25
|
+
MissingRegistryError,
|
|
26
26
|
SQLAKitError,
|
|
27
27
|
UnknownFieldError,
|
|
28
28
|
)
|
|
@@ -120,7 +120,7 @@ class BaseModel(Generic[DatabaseT]):
|
|
|
120
120
|
cls.__db__ = db
|
|
121
121
|
|
|
122
122
|
@classmethod
|
|
123
|
-
def register_db(cls, db: DatabaseT, *, alias: str) -> None:
|
|
123
|
+
def register_db(cls, db: DatabaseT, *, alias: str | None = None) -> None:
|
|
124
124
|
"""Give this model a database under an alias, and the ones under it too.
|
|
125
125
|
|
|
126
126
|
The registry it goes in belongs to this class, so nothing global is
|
|
@@ -138,11 +138,21 @@ class BaseModel(Generic[DatabaseT]):
|
|
|
138
138
|
the open `using()` block, in that order. A model left on the default
|
|
139
139
|
alias follows `using()`, which is what makes the switch above work.
|
|
140
140
|
|
|
141
|
+
Without an alias it points the model at that database, the same as
|
|
142
|
+
[`set_db`][sqlakit.orm.ModelMixin.set_db], for an application with one:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
Base.register_db(Database(DB_URL))
|
|
146
|
+
```
|
|
147
|
+
|
|
141
148
|
Raises:
|
|
142
149
|
AliasInUseError: if another database holds that alias.
|
|
143
150
|
DefaultAliasError: if the alias is `default`.
|
|
144
151
|
|
|
145
152
|
"""
|
|
153
|
+
if alias is None:
|
|
154
|
+
cls.set_db(db)
|
|
155
|
+
return
|
|
146
156
|
if _owns_no_registry(cls):
|
|
147
157
|
# A registry of its own: registering into the importable one would
|
|
148
158
|
# configure it for every model in the process. A class under one
|
|
@@ -257,16 +267,21 @@ def db_for(model: type[Any]) -> BaseDatabase[Any, Any]:
|
|
|
257
267
|
placement = model.__db__
|
|
258
268
|
if isinstance(placement, str):
|
|
259
269
|
if source is None:
|
|
260
|
-
raise
|
|
270
|
+
raise MissingRegistryError(model.__name__, placement)
|
|
261
271
|
return source[placement]
|
|
262
272
|
return placement
|
|
263
273
|
|
|
264
274
|
|
|
265
275
|
def resolve_alias(model: type[Any], alias: str) -> BaseDatabase[Any, Any]:
|
|
266
|
-
"""Return the database a model knows under that alias.
|
|
276
|
+
"""Return the database a model knows under that alias.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
MissingRegistryError: if the model looks aliases up nowhere.
|
|
280
|
+
|
|
281
|
+
"""
|
|
267
282
|
source = getattr(model, "__dbs__", None)
|
|
268
283
|
if source is None:
|
|
269
|
-
raise
|
|
284
|
+
raise MissingRegistryError(model.__name__, alias)
|
|
270
285
|
return source[alias]
|
|
271
286
|
|
|
272
287
|
|
|
@@ -14,7 +14,7 @@ from typing import (
|
|
|
14
14
|
NamedTuple,
|
|
15
15
|
Protocol,
|
|
16
16
|
Self,
|
|
17
|
-
|
|
17
|
+
TypeAlias,
|
|
18
18
|
cast,
|
|
19
19
|
)
|
|
20
20
|
|
|
@@ -31,6 +31,9 @@ from sqlalchemy.orm import (
|
|
|
31
31
|
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
|
32
32
|
from sqlalchemy.sql import operators
|
|
33
33
|
|
|
34
|
+
# `TypeVar` from here, for the default that keeps `Page[User]` the counted one.
|
|
35
|
+
from typing_extensions import TypeVar
|
|
36
|
+
|
|
34
37
|
from ._model import resolve_alias, soft_delete_column
|
|
35
38
|
from .exceptions import (
|
|
36
39
|
BulkQueryError,
|
|
@@ -63,9 +66,11 @@ __all__ = [
|
|
|
63
66
|
"NullsPlacement",
|
|
64
67
|
"OrderBy",
|
|
65
68
|
"Page",
|
|
69
|
+
"UncountedPage",
|
|
66
70
|
"one_row",
|
|
67
71
|
"one_row_or_none",
|
|
68
72
|
"orderable",
|
|
73
|
+
"orderable_columns",
|
|
69
74
|
"ordered",
|
|
70
75
|
]
|
|
71
76
|
|
|
@@ -100,6 +105,8 @@ ONLY = "only"
|
|
|
100
105
|
|
|
101
106
|
ModelT = TypeVar("ModelT")
|
|
102
107
|
OtherT = TypeVar("OtherT")
|
|
108
|
+
TotalT = TypeVar("TotalT", bound="int | None", default=int)
|
|
109
|
+
"""What a page knows about its total: an `int`, or `None` when nobody counted."""
|
|
103
110
|
RowT = TypeVar("RowT")
|
|
104
111
|
RowT_co = TypeVar("RowT_co", covariant=True)
|
|
105
112
|
|
|
@@ -170,18 +177,19 @@ def _compile_nulls_for_mysql(
|
|
|
170
177
|
|
|
171
178
|
|
|
172
179
|
@dataclass(frozen=True, slots=True)
|
|
173
|
-
class Page(Generic[ModelT]):
|
|
174
|
-
"""One page of rows, and how many there are in total.
|
|
180
|
+
class Page(Generic[ModelT, TotalT]):
|
|
181
|
+
"""One page of rows, and how many there are in total.
|
|
175
182
|
|
|
176
|
-
|
|
177
|
-
total
|
|
178
|
-
|
|
183
|
+
``Page[User]`` is the counted page, whose ``total`` is an `int`.
|
|
184
|
+
``page(total=False)`` returns a `Page[User, None]` instead, so the code
|
|
185
|
+
reading it is not asked about a total nobody counted.
|
|
186
|
+
"""
|
|
179
187
|
|
|
188
|
+
items: Sequence[ModelT]
|
|
189
|
+
total: TotalT
|
|
180
190
|
limit: int
|
|
181
191
|
offset: int
|
|
182
|
-
|
|
183
192
|
has_next: bool = False
|
|
184
|
-
"""Whether a page follows this one."""
|
|
185
193
|
|
|
186
194
|
def __post_init__(self) -> None:
|
|
187
195
|
if self.total is not None:
|
|
@@ -189,7 +197,7 @@ class Page(Generic[ModelT]):
|
|
|
189
197
|
self, "has_next", self.offset + len(self.items) < self.total
|
|
190
198
|
)
|
|
191
199
|
|
|
192
|
-
def map(self, transform: Callable[[ModelT], OtherT]) -> Page[OtherT]:
|
|
200
|
+
def map(self, transform: Callable[[ModelT], OtherT]) -> Page[OtherT, TotalT]:
|
|
193
201
|
"""Return the page with every row put through ``transform``.
|
|
194
202
|
|
|
195
203
|
```python
|
|
@@ -201,7 +209,7 @@ class Page(Generic[ModelT]):
|
|
|
201
209
|
def map_all(
|
|
202
210
|
self,
|
|
203
211
|
transform: Callable[[Sequence[ModelT]], Sequence[OtherT]],
|
|
204
|
-
) -> Page[OtherT]:
|
|
212
|
+
) -> Page[OtherT, TotalT]:
|
|
205
213
|
"""Return the page with the rows put through ``transform`` together.
|
|
206
214
|
|
|
207
215
|
For work that reads better in one go than row by row: one query for
|
|
@@ -209,7 +217,7 @@ class Page(Generic[ModelT]):
|
|
|
209
217
|
"""
|
|
210
218
|
return self.with_items(transform(self.items))
|
|
211
219
|
|
|
212
|
-
def with_items(self, items: Sequence[OtherT]) -> Page[OtherT]:
|
|
220
|
+
def with_items(self, items: Sequence[OtherT]) -> Page[OtherT, TotalT]:
|
|
213
221
|
"""Return the page carrying these rows instead, counts unchanged.
|
|
214
222
|
|
|
215
223
|
What an asynchronous transform needs: `page.with_items(await serialize(...))`.
|
|
@@ -229,16 +237,24 @@ class Page(Generic[ModelT]):
|
|
|
229
237
|
)
|
|
230
238
|
|
|
231
239
|
|
|
240
|
+
UncountedPage: TypeAlias = Page[ModelT, None]
|
|
241
|
+
"""A page read with ``total=False``, which counted nothing.
|
|
242
|
+
|
|
243
|
+
The same class, named for what a signature means by it:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
def feed(page: UncountedPage[User]) -> Response: ...
|
|
247
|
+
```
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
|
|
232
251
|
@dataclass(frozen=True, slots=True)
|
|
233
252
|
class CursorPage(Generic[ModelT]):
|
|
234
253
|
"""One page of rows, and the cursors that read the ones either side."""
|
|
235
254
|
|
|
236
255
|
items: Sequence[ModelT]
|
|
237
256
|
next_cursor: str | None = None
|
|
238
|
-
"""Hand it back as ``cursor`` to read on."""
|
|
239
|
-
|
|
240
257
|
previous_cursor: str | None = None
|
|
241
|
-
"""Hand it back as ``cursor`` to read the page in front of this one."""
|
|
242
258
|
|
|
243
259
|
@property
|
|
244
260
|
def has_next(self) -> bool:
|
|
@@ -278,10 +294,11 @@ class CursorPage(Generic[ModelT]):
|
|
|
278
294
|
|
|
279
295
|
|
|
280
296
|
class OrderBy(NamedTuple):
|
|
281
|
-
"""A field to order by that
|
|
297
|
+
"""A field to order by that a foreign key cannot reach on its own.
|
|
282
298
|
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
A plain column of another table needs none of this: naming it in
|
|
300
|
+
``__orderable__`` joins its table on the key between them. This is for what
|
|
301
|
+
that cannot answer, an alias, a subquery, or two paths to one table:
|
|
285
302
|
|
|
286
303
|
```python
|
|
287
304
|
{"team": OrderBy(Team.name, join=cls.team)}
|
|
@@ -290,11 +307,16 @@ class OrderBy(NamedTuple):
|
|
|
290
307
|
Join what holds one row. A collection multiplies the rows, and a page of
|
|
291
308
|
multiplied rows counts wrong: join a subquery that aggregates them instead,
|
|
292
309
|
with the ``on`` it needs.
|
|
310
|
+
|
|
311
|
+
The join is an outer one, so ordering by the field returns the rows with
|
|
312
|
+
nothing on the other side as well. ``outer=False`` makes it an inner join,
|
|
313
|
+
which drops them, and ``nulls`` on `order_by` says where they go.
|
|
293
314
|
"""
|
|
294
315
|
|
|
295
316
|
expression: Any
|
|
296
317
|
join: Any = None
|
|
297
318
|
on: Any = None
|
|
319
|
+
outer: bool = True
|
|
298
320
|
|
|
299
321
|
|
|
300
322
|
class SupportsClause(Protocol):
|
|
@@ -921,6 +943,29 @@ class BaseQuery(Generic[ModelT]):
|
|
|
921
943
|
return _encode(values, backwards=backwards, ordering=ordering_key)
|
|
922
944
|
|
|
923
945
|
|
|
946
|
+
def orderable_columns(model: type[Any]) -> Mapping[str, Any]:
|
|
947
|
+
"""Return every mapped column of a model, by name.
|
|
948
|
+
|
|
949
|
+
What a model orders by when it declares no ``__orderable__``, and what an
|
|
950
|
+
``__orderable__`` that adds to the columns rather than replacing them starts
|
|
951
|
+
from:
|
|
952
|
+
|
|
953
|
+
```python
|
|
954
|
+
@classmethod
|
|
955
|
+
def __orderable__(cls) -> Mapping[str, Any]:
|
|
956
|
+
return {
|
|
957
|
+
**orderable_columns(cls),
|
|
958
|
+
"team": OrderBy(Team.name, join=cls.team),
|
|
959
|
+
}
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
Calling `orderable` there instead recurses: it reads the very
|
|
963
|
+
``__orderable__`` that is running.
|
|
964
|
+
"""
|
|
965
|
+
mapper = sa.inspect(model, raiseerr=True)
|
|
966
|
+
return {attr.key: getattr(model, attr.key) for attr in mapper.column_attrs}
|
|
967
|
+
|
|
968
|
+
|
|
924
969
|
def orderable(model: type[Any]) -> Mapping[str, Any]:
|
|
925
970
|
"""Return the fields a model can be ordered by name.
|
|
926
971
|
|
|
@@ -930,8 +975,7 @@ def orderable(model: type[Any]) -> Mapping[str, Any]:
|
|
|
930
975
|
"""
|
|
931
976
|
fields = getattr(model, "__orderable__", None)
|
|
932
977
|
if fields is None:
|
|
933
|
-
|
|
934
|
-
return {attr.key: getattr(model, attr.key) for attr in mapper.column_attrs}
|
|
978
|
+
return orderable_columns(model)
|
|
935
979
|
if callable(fields):
|
|
936
980
|
return fields()
|
|
937
981
|
return {name: _mapped_column(model, name) for name in fields}
|
|
@@ -975,9 +1019,9 @@ def ordered(
|
|
|
975
1019
|
clause, join = _ordering_for(criterion, fields, ignore_case=folded)
|
|
976
1020
|
clauses.append(_with_nulls(clause, nulls))
|
|
977
1021
|
if join is not None:
|
|
978
|
-
target, onclause = join
|
|
1022
|
+
target, onclause, outer = join
|
|
979
1023
|
if not _is_joined(select, target):
|
|
980
|
-
select = select.join(target, onclause)
|
|
1024
|
+
select = select.join(target, onclause, isouter=outer)
|
|
981
1025
|
return select.order_by(*clauses)
|
|
982
1026
|
|
|
983
1027
|
|
|
@@ -989,16 +1033,16 @@ def _ordering_for(
|
|
|
989
1033
|
) -> tuple[Any, Any]:
|
|
990
1034
|
"""Return the clause a criterion stands for, and the table it needs."""
|
|
991
1035
|
if isinstance(criterion, OrderBy):
|
|
992
|
-
return criterion.expression, (criterion.join, criterion.on)
|
|
1036
|
+
return criterion.expression, (criterion.join, criterion.on, criterion.outer)
|
|
993
1037
|
if not isinstance(criterion, str):
|
|
994
1038
|
return criterion, None
|
|
995
1039
|
asked, descending, nulls = _parse_sort_field(criterion)
|
|
996
1040
|
name = _field_named(asked, fields)
|
|
997
1041
|
field = fields[name]
|
|
998
1042
|
if isinstance(field, OrderBy):
|
|
999
|
-
column, join = field.expression, (field.join, field.on)
|
|
1043
|
+
column, join = field.expression, (field.join, field.on, field.outer)
|
|
1000
1044
|
else:
|
|
1001
|
-
column, join = field,
|
|
1045
|
+
column, join = field, _join_for(field)
|
|
1002
1046
|
if ignore_case is True or (ignore_case is not False and name in ignore_case):
|
|
1003
1047
|
column = _case_insensitive(column)
|
|
1004
1048
|
return _sort_clause(column, descending=descending, nulls=nulls), join
|
|
@@ -1117,6 +1161,21 @@ def _flatten(criteria: Iterable[Any]) -> Iterator[Any]:
|
|
|
1117
1161
|
yield criterion
|
|
1118
1162
|
|
|
1119
1163
|
|
|
1164
|
+
def _join_for(field: Any) -> tuple[Any, None, bool] | None: # noqa: ANN401
|
|
1165
|
+
"""Return the table a field lives in, for a field that is a plain column.
|
|
1166
|
+
|
|
1167
|
+
A column of another table is reachable only through a join, and `SQLAlchemy`
|
|
1168
|
+
works the condition out from the foreign key. An expression may name several
|
|
1169
|
+
tables or none, so it is left alone: `OrderBy` says what to join for those,
|
|
1170
|
+
as it does for an alias or a subquery.
|
|
1171
|
+
"""
|
|
1172
|
+
column = _as_column(field)
|
|
1173
|
+
if not isinstance(column, sa.Column):
|
|
1174
|
+
return None
|
|
1175
|
+
table = getattr(column, "table", None)
|
|
1176
|
+
return None if table is None else (table, None, True)
|
|
1177
|
+
|
|
1178
|
+
|
|
1120
1179
|
def _is_joined(select: sa.Select[Any], target: Any) -> bool: # noqa: ANN401
|
|
1121
1180
|
"""Whether this statement already reaches what a field needs."""
|
|
1122
1181
|
wanted = _join_identity(target)
|
|
@@ -197,9 +197,19 @@ class Query(BaseQuery[ModelT]):
|
|
|
197
197
|
"""Check whether any row matches."""
|
|
198
198
|
return bool(await self.db.session.scalar(self._exists_statement()))
|
|
199
199
|
|
|
200
|
+
@overload
|
|
201
|
+
async def page(
|
|
202
|
+
self, *, limit: int, offset: int = 0, total: Literal[True] = True
|
|
203
|
+
) -> Page[ModelT]: ...
|
|
204
|
+
|
|
205
|
+
@overload
|
|
206
|
+
async def page(
|
|
207
|
+
self, *, limit: int, offset: int = 0, total: Literal[False]
|
|
208
|
+
) -> Page[ModelT, None]: ...
|
|
209
|
+
|
|
200
210
|
async def page(
|
|
201
211
|
self, *, limit: int, offset: int = 0, total: bool = True
|
|
202
|
-
) -> Page[ModelT]:
|
|
212
|
+
) -> Page[ModelT] | Page[ModelT, None]:
|
|
203
213
|
"""Read one page of rows, with the total.
|
|
204
214
|
|
|
205
215
|
The model's key is appended to the ordering, so a row that ties with another
|
|
@@ -22,6 +22,7 @@ __all__ = [
|
|
|
22
22
|
"MissingDatabaseUrlError",
|
|
23
23
|
"MissingDefaultDatabaseError",
|
|
24
24
|
"MissingDependencyError",
|
|
25
|
+
"MissingRegistryError",
|
|
25
26
|
"MissingSessionError",
|
|
26
27
|
"MultipleInstancesFoundError",
|
|
27
28
|
"NullCursorValueError",
|
|
@@ -140,6 +141,20 @@ class UnknownDatabaseError(SQLAKitError, KeyError):
|
|
|
140
141
|
)
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
class MissingRegistryError(SQLAKitError, ValueError):
|
|
145
|
+
"""Raised when a model has nowhere to look a database alias up."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, model: str, alias: str | None = None) -> None:
|
|
148
|
+
self.model = model
|
|
149
|
+
self.alias = alias
|
|
150
|
+
named = f"`{alias}`" if alias is not None else "an alias"
|
|
151
|
+
super().__init__(
|
|
152
|
+
f"`{model}` has no registry to look {named} up in. Pass the "
|
|
153
|
+
f"database itself instead of its name, or set `__dbs__` on the "
|
|
154
|
+
f"model to the registry that holds it."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
143
158
|
class MissingDefaultDatabaseError(SQLAKitError, ValueError):
|
|
144
159
|
"""Raised when a configuration keyed by alias carries no default."""
|
|
145
160
|
|
|
@@ -190,7 +190,19 @@ class Query(BaseQuery[ModelT]):
|
|
|
190
190
|
"""Check whether any row matches."""
|
|
191
191
|
return bool(self.db.session.scalar(self._exists_statement()))
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
@overload
|
|
194
|
+
def page(
|
|
195
|
+
self, *, limit: int, offset: int = 0, total: Literal[True] = True
|
|
196
|
+
) -> Page[ModelT]: ...
|
|
197
|
+
|
|
198
|
+
@overload
|
|
199
|
+
def page(
|
|
200
|
+
self, *, limit: int, offset: int = 0, total: Literal[False]
|
|
201
|
+
) -> Page[ModelT, None]: ...
|
|
202
|
+
|
|
203
|
+
def page(
|
|
204
|
+
self, *, limit: int, offset: int = 0, total: bool = True
|
|
205
|
+
) -> Page[ModelT] | Page[ModelT, None]:
|
|
194
206
|
"""Read one page of rows, with the total.
|
|
195
207
|
|
|
196
208
|
The model's key is appended to the ordering, so a row that ties with another
|
|
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
|