sqlakit 0.5.0__tar.gz → 0.6.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.0 → sqlakit-0.6.0}/PKG-INFO +1 -1
- {sqlakit-0.5.0 → sqlakit-0.6.0}/pyproject.toml +1 -1
- {sqlakit-0.5.0 → sqlakit-0.6.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/__init__.py +14 -1
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_base.py +27 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_model.py +57 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_query.py +54 -16
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/asyncio/orm.py +11 -1
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/exceptions.py +23 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/orm.py +13 -1
- {sqlakit-0.5.0 → sqlakit-0.6.0}/LICENSE +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/README.md +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.5.0 → sqlakit-0.6.0}/sqlakit/types.py +0 -0
|
@@ -1,17 +1,26 @@
|
|
|
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
|
|
8
15
|
from .exceptions import (
|
|
9
16
|
DEFAULT_ALIAS,
|
|
17
|
+
AliasInUseError,
|
|
10
18
|
AsyncFilterError,
|
|
11
19
|
BulkQueryError,
|
|
12
20
|
ConflictingDatabaseUrlError,
|
|
13
21
|
DatabaseAlreadyConfiguredError,
|
|
14
22
|
DatabaseNotConfiguredError,
|
|
23
|
+
DefaultAliasError,
|
|
15
24
|
DetachedInstanceError,
|
|
16
25
|
InstanceNotFoundError,
|
|
17
26
|
InvalidCursorError,
|
|
@@ -55,6 +64,7 @@ __all__ = [
|
|
|
55
64
|
"DEFAULT_ALIAS",
|
|
56
65
|
"DEFAULT_ENGINE_ARGS",
|
|
57
66
|
"DEFAULT_SESSION_ARGS",
|
|
67
|
+
"AliasInUseError",
|
|
58
68
|
"AsyncFilterError",
|
|
59
69
|
"BulkQueryError",
|
|
60
70
|
"ConflictingDatabaseUrlError",
|
|
@@ -64,6 +74,7 @@ __all__ = [
|
|
|
64
74
|
"DatabaseConfig",
|
|
65
75
|
"DatabaseNotConfiguredError",
|
|
66
76
|
"Databases",
|
|
77
|
+
"DefaultAliasError",
|
|
67
78
|
"DetachedInstanceError",
|
|
68
79
|
"EngineArgs",
|
|
69
80
|
"InstanceNotFoundError",
|
|
@@ -98,6 +109,7 @@ __all__ = [
|
|
|
98
109
|
"Transaction",
|
|
99
110
|
"TransactionRolledBackError",
|
|
100
111
|
"UncomparableOrderingError",
|
|
112
|
+
"UncountedPage",
|
|
101
113
|
"UnknownDatabaseError",
|
|
102
114
|
"UnknownFieldError",
|
|
103
115
|
"UnknownImportPathError",
|
|
@@ -107,4 +119,5 @@ __all__ = [
|
|
|
107
119
|
"db",
|
|
108
120
|
"import_models",
|
|
109
121
|
"import_string",
|
|
122
|
+
"orderable_columns",
|
|
110
123
|
]
|
|
@@ -35,9 +35,11 @@ from ._recording import (
|
|
|
35
35
|
from ._routing import Router, as_router
|
|
36
36
|
from .exceptions import (
|
|
37
37
|
DEFAULT_ALIAS,
|
|
38
|
+
AliasInUseError,
|
|
38
39
|
ConflictingDatabaseUrlError,
|
|
39
40
|
DatabaseAlreadyConfiguredError,
|
|
40
41
|
DatabaseNotConfiguredError,
|
|
42
|
+
DefaultAliasError,
|
|
41
43
|
MissingConnectionError,
|
|
42
44
|
MissingDatabaseUrlError,
|
|
43
45
|
MissingDefaultDatabaseError,
|
|
@@ -693,6 +695,31 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
693
695
|
def __contains__(self, alias: str) -> bool:
|
|
694
696
|
return alias == DEFAULT_ALIAS or alias in self._aliased
|
|
695
697
|
|
|
698
|
+
def register(self, alias: str, db: DatabaseT) -> None:
|
|
699
|
+
"""Put a database already built under an alias.
|
|
700
|
+
|
|
701
|
+
`configure` takes settings and builds the databases. This takes one you
|
|
702
|
+
built yourself, for a shard that only exists once the application is
|
|
703
|
+
running, or for a registry that never reads settings at all:
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
db.register("shard-7", Database(SHARD_URL))
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
The alias has to be free. Replacing one under a name already in use
|
|
710
|
+
would leave the code that holds the old database talking to it.
|
|
711
|
+
|
|
712
|
+
Raises:
|
|
713
|
+
AliasInUseError: if another database holds that alias.
|
|
714
|
+
DefaultAliasError: if the alias is `default`, which this registry is.
|
|
715
|
+
|
|
716
|
+
"""
|
|
717
|
+
if alias == DEFAULT_ALIAS:
|
|
718
|
+
raise DefaultAliasError
|
|
719
|
+
if alias in self._aliased:
|
|
720
|
+
raise AliasInUseError(alias)
|
|
721
|
+
self._aliased[alias] = self._named(alias, db)
|
|
722
|
+
|
|
696
723
|
@contextmanager
|
|
697
724
|
def recording(
|
|
698
725
|
self,
|
|
@@ -37,7 +37,9 @@ __all__ = [
|
|
|
37
37
|
"BaseModel",
|
|
38
38
|
"BaseSoftDeletes",
|
|
39
39
|
"DatabaseDescriptor",
|
|
40
|
+
"DatabaseRegistry",
|
|
40
41
|
"DatabaseSource",
|
|
42
|
+
"RegistryDescriptor",
|
|
41
43
|
"db_for",
|
|
42
44
|
"resolve_alias",
|
|
43
45
|
"soft_delete_column",
|
|
@@ -66,6 +68,19 @@ class DatabaseDescriptor(Generic[DatabaseT]):
|
|
|
66
68
|
return cast("DatabaseT", db_for(owner))
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
class DatabaseRegistry(DatabaseSource, Protocol):
|
|
72
|
+
"""A source that also takes a database under an alias it does not have."""
|
|
73
|
+
|
|
74
|
+
def register(self, alias: str, db: Any) -> None: ... # noqa: ANN401
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RegistryDescriptor:
|
|
78
|
+
"""Reads ``__dbs__``, on the class as well as on an instance."""
|
|
79
|
+
|
|
80
|
+
def __get__(self, instance: object | None, owner: type[Any]) -> Any: # noqa: ANN401
|
|
81
|
+
return owner.__dbs__
|
|
82
|
+
|
|
83
|
+
|
|
69
84
|
class BaseModel(Generic[DatabaseT]):
|
|
70
85
|
"""What the sync and async models share: everything that is not IO.
|
|
71
86
|
|
|
@@ -87,6 +102,7 @@ class BaseModel(Generic[DatabaseT]):
|
|
|
87
102
|
# Unannotated on purpose: an annotation here reads as a field to
|
|
88
103
|
# pydantic, and SQLModel models would refuse to build.
|
|
89
104
|
db = DatabaseDescriptor[DatabaseT]()
|
|
105
|
+
dbs = RegistryDescriptor()
|
|
90
106
|
|
|
91
107
|
@classmethod
|
|
92
108
|
def set_db(cls, db: str | DatabaseT) -> None:
|
|
@@ -103,6 +119,37 @@ class BaseModel(Generic[DatabaseT]):
|
|
|
103
119
|
"""
|
|
104
120
|
cls.__db__ = db
|
|
105
121
|
|
|
122
|
+
@classmethod
|
|
123
|
+
def register_db(cls, db: DatabaseT, *, alias: str) -> None:
|
|
124
|
+
"""Give this model a database under an alias, and the ones under it too.
|
|
125
|
+
|
|
126
|
+
The registry it goes in belongs to this class, so nothing global is
|
|
127
|
+
configured and two sets of models can each have their own `shard`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
Base.register_db(Database(DB1_URL), alias="db1")
|
|
131
|
+
Base.register_db(Database(DB2_URL), alias="db2")
|
|
132
|
+
|
|
133
|
+
with Base.dbs.using("db2").transaction():
|
|
134
|
+
User(name="ada").save()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Which database a model resolves to is still `__db__`, the routers and
|
|
138
|
+
the open `using()` block, in that order. A model left on the default
|
|
139
|
+
alias follows `using()`, which is what makes the switch above work.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
AliasInUseError: if another database holds that alias.
|
|
143
|
+
DefaultAliasError: if the alias is `default`.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
if _owns_no_registry(cls):
|
|
147
|
+
# A registry of its own: registering into the importable one would
|
|
148
|
+
# configure it for every model in the process. A class under one
|
|
149
|
+
# that already has its own registers into that one.
|
|
150
|
+
cls.__dbs__ = type(cls.__dbs__)()
|
|
151
|
+
cast("DatabaseRegistry", cls.__dbs__).register(alias, db)
|
|
152
|
+
|
|
106
153
|
def update(self, values: Mapping[str, Any]) -> Self:
|
|
107
154
|
"""Set these fields on this instance, and return it.
|
|
108
155
|
|
|
@@ -187,6 +234,16 @@ class BaseModel(Generic[DatabaseT]):
|
|
|
187
234
|
self.db.session.add(self)
|
|
188
235
|
|
|
189
236
|
|
|
237
|
+
def _owns_no_registry(model: type[Any]) -> bool:
|
|
238
|
+
"""Whether this model still looks its aliases up in the importable registry."""
|
|
239
|
+
declared = next(
|
|
240
|
+
(klass for klass in model.__mro__ if "__dbs__" in klass.__dict__), None
|
|
241
|
+
)
|
|
242
|
+
return (
|
|
243
|
+
declared is None or declared.__module__.split(".")[0] == __name__.split(".")[0]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
190
247
|
def db_for(model: type[Any]) -> BaseDatabase[Any, Any]:
|
|
191
248
|
"""Return the database a model lives on.
|
|
192
249
|
|
|
@@ -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:
|
|
@@ -921,6 +937,29 @@ class BaseQuery(Generic[ModelT]):
|
|
|
921
937
|
return _encode(values, backwards=backwards, ordering=ordering_key)
|
|
922
938
|
|
|
923
939
|
|
|
940
|
+
def orderable_columns(model: type[Any]) -> Mapping[str, Any]:
|
|
941
|
+
"""Return every mapped column of a model, by name.
|
|
942
|
+
|
|
943
|
+
What a model orders by when it declares no ``__orderable__``, and what an
|
|
944
|
+
``__orderable__`` that adds to the columns rather than replacing them starts
|
|
945
|
+
from:
|
|
946
|
+
|
|
947
|
+
```python
|
|
948
|
+
@classmethod
|
|
949
|
+
def __orderable__(cls) -> Mapping[str, Any]:
|
|
950
|
+
return {
|
|
951
|
+
**orderable_columns(cls),
|
|
952
|
+
"team": OrderBy(Team.name, join=cls.team),
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Calling `orderable` there instead recurses: it reads the very
|
|
957
|
+
``__orderable__`` that is running.
|
|
958
|
+
"""
|
|
959
|
+
mapper = sa.inspect(model, raiseerr=True)
|
|
960
|
+
return {attr.key: getattr(model, attr.key) for attr in mapper.column_attrs}
|
|
961
|
+
|
|
962
|
+
|
|
924
963
|
def orderable(model: type[Any]) -> Mapping[str, Any]:
|
|
925
964
|
"""Return the fields a model can be ordered by name.
|
|
926
965
|
|
|
@@ -930,8 +969,7 @@ def orderable(model: type[Any]) -> Mapping[str, Any]:
|
|
|
930
969
|
"""
|
|
931
970
|
fields = getattr(model, "__orderable__", None)
|
|
932
971
|
if fields is None:
|
|
933
|
-
|
|
934
|
-
return {attr.key: getattr(model, attr.key) for attr in mapper.column_attrs}
|
|
972
|
+
return orderable_columns(model)
|
|
935
973
|
if callable(fields):
|
|
936
974
|
return fields()
|
|
937
975
|
return {name: _mapped_column(model, name) for name in fields}
|
|
@@ -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
|
|
@@ -5,11 +5,13 @@ from sqlalchemy.orm import exc as sa_exc
|
|
|
5
5
|
DEFAULT_ALIAS = "default"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
|
+
"AliasInUseError",
|
|
8
9
|
"AsyncFilterError",
|
|
9
10
|
"BulkQueryError",
|
|
10
11
|
"ConflictingDatabaseUrlError",
|
|
11
12
|
"DatabaseAlreadyConfiguredError",
|
|
12
13
|
"DatabaseNotConfiguredError",
|
|
14
|
+
"DefaultAliasError",
|
|
13
15
|
"DetachedInstanceError",
|
|
14
16
|
"InstanceNotFoundError",
|
|
15
17
|
"InvalidCursorError",
|
|
@@ -107,6 +109,27 @@ class DatabaseAlreadyConfiguredError(SQLAKitError, RuntimeError):
|
|
|
107
109
|
super().__init__(message)
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
class AliasInUseError(SQLAKitError, ValueError):
|
|
113
|
+
"""Raised when registering a database under an alias another one holds."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, alias: str) -> None:
|
|
116
|
+
self.alias = alias
|
|
117
|
+
super().__init__(
|
|
118
|
+
f"`{alias}` is already registered. Dispose of that database before "
|
|
119
|
+
"registering another under the same name."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DefaultAliasError(SQLAKitError, ValueError):
|
|
124
|
+
"""Raised when registering a database as the default one."""
|
|
125
|
+
|
|
126
|
+
def __init__(self) -> None:
|
|
127
|
+
super().__init__(
|
|
128
|
+
f"`{DEFAULT_ALIAS}` is the registry itself. Configure it with "
|
|
129
|
+
"`configure()`, and register the others under their own names."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
110
133
|
class UnknownDatabaseError(SQLAKitError, KeyError):
|
|
111
134
|
"""Raised when asking for a database alias that was never configured."""
|
|
112
135
|
|
|
@@ -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
|