sqlakit 0.6.0__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.6.0 → sqlakit-0.7.0}/PKG-INFO +1 -1
- {sqlakit-0.6.0 → sqlakit-0.7.0}/pyproject.toml +1 -1
- {sqlakit-0.6.0 → sqlakit-0.7.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/__init__.py +2 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_model.py +20 -5
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_query.py +29 -8
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/exceptions.py +15 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/LICENSE +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/README.md +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/orm.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.6.0 → sqlakit-0.7.0}/sqlakit/types.py +0 -0
|
@@ -32,6 +32,7 @@ from .exceptions import (
|
|
|
32
32
|
MissingDatabaseUrlError,
|
|
33
33
|
MissingDefaultDatabaseError,
|
|
34
34
|
MissingDependencyError,
|
|
35
|
+
MissingRegistryError,
|
|
35
36
|
MissingSessionError,
|
|
36
37
|
MultipleInstancesFoundError,
|
|
37
38
|
NullCursorValueError,
|
|
@@ -87,6 +88,7 @@ __all__ = [
|
|
|
87
88
|
"MissingDatabaseUrlError",
|
|
88
89
|
"MissingDefaultDatabaseError",
|
|
89
90
|
"MissingDependencyError",
|
|
91
|
+
"MissingRegistryError",
|
|
90
92
|
"MissingSessionError",
|
|
91
93
|
"MultipleInstancesFoundError",
|
|
92
94
|
"NullCursorValueError",
|
|
@@ -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
|
|
|
@@ -294,10 +294,11 @@ class CursorPage(Generic[ModelT]):
|
|
|
294
294
|
|
|
295
295
|
|
|
296
296
|
class OrderBy(NamedTuple):
|
|
297
|
-
"""A field to order by that
|
|
297
|
+
"""A field to order by that a foreign key cannot reach on its own.
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
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:
|
|
301
302
|
|
|
302
303
|
```python
|
|
303
304
|
{"team": OrderBy(Team.name, join=cls.team)}
|
|
@@ -306,11 +307,16 @@ class OrderBy(NamedTuple):
|
|
|
306
307
|
Join what holds one row. A collection multiplies the rows, and a page of
|
|
307
308
|
multiplied rows counts wrong: join a subquery that aggregates them instead,
|
|
308
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.
|
|
309
314
|
"""
|
|
310
315
|
|
|
311
316
|
expression: Any
|
|
312
317
|
join: Any = None
|
|
313
318
|
on: Any = None
|
|
319
|
+
outer: bool = True
|
|
314
320
|
|
|
315
321
|
|
|
316
322
|
class SupportsClause(Protocol):
|
|
@@ -1013,9 +1019,9 @@ def ordered(
|
|
|
1013
1019
|
clause, join = _ordering_for(criterion, fields, ignore_case=folded)
|
|
1014
1020
|
clauses.append(_with_nulls(clause, nulls))
|
|
1015
1021
|
if join is not None:
|
|
1016
|
-
target, onclause = join
|
|
1022
|
+
target, onclause, outer = join
|
|
1017
1023
|
if not _is_joined(select, target):
|
|
1018
|
-
select = select.join(target, onclause)
|
|
1024
|
+
select = select.join(target, onclause, isouter=outer)
|
|
1019
1025
|
return select.order_by(*clauses)
|
|
1020
1026
|
|
|
1021
1027
|
|
|
@@ -1027,16 +1033,16 @@ def _ordering_for(
|
|
|
1027
1033
|
) -> tuple[Any, Any]:
|
|
1028
1034
|
"""Return the clause a criterion stands for, and the table it needs."""
|
|
1029
1035
|
if isinstance(criterion, OrderBy):
|
|
1030
|
-
return criterion.expression, (criterion.join, criterion.on)
|
|
1036
|
+
return criterion.expression, (criterion.join, criterion.on, criterion.outer)
|
|
1031
1037
|
if not isinstance(criterion, str):
|
|
1032
1038
|
return criterion, None
|
|
1033
1039
|
asked, descending, nulls = _parse_sort_field(criterion)
|
|
1034
1040
|
name = _field_named(asked, fields)
|
|
1035
1041
|
field = fields[name]
|
|
1036
1042
|
if isinstance(field, OrderBy):
|
|
1037
|
-
column, join = field.expression, (field.join, field.on)
|
|
1043
|
+
column, join = field.expression, (field.join, field.on, field.outer)
|
|
1038
1044
|
else:
|
|
1039
|
-
column, join = field,
|
|
1045
|
+
column, join = field, _join_for(field)
|
|
1040
1046
|
if ignore_case is True or (ignore_case is not False and name in ignore_case):
|
|
1041
1047
|
column = _case_insensitive(column)
|
|
1042
1048
|
return _sort_clause(column, descending=descending, nulls=nulls), join
|
|
@@ -1155,6 +1161,21 @@ def _flatten(criteria: Iterable[Any]) -> Iterator[Any]:
|
|
|
1155
1161
|
yield criterion
|
|
1156
1162
|
|
|
1157
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
|
+
|
|
1158
1179
|
def _is_joined(select: sa.Select[Any], target: Any) -> bool: # noqa: ANN401
|
|
1159
1180
|
"""Whether this statement already reaches what a field needs."""
|
|
1160
1181
|
wanted = _join_identity(target)
|
|
@@ -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
|
|
|
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
|
|
File without changes
|
|
File without changes
|