sqlakit 0.16.0__tar.gz → 0.18.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.16.0 → sqlakit-0.18.0}/PKG-INFO +1 -1
- {sqlakit-0.16.0 → sqlakit-0.18.0}/pyproject.toml +1 -1
- {sqlakit-0.16.0 → sqlakit-0.18.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_model.py +41 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/asyncio/orm.py +45 -5
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/orm.py +45 -5
- {sqlakit-0.16.0 → sqlakit-0.18.0}/LICENSE +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/README.md +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/__init__.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_cli.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_debugserver.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_query.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/debugserver.html +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.18.0}/sqlakit/types.py +0 -0
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
# Imported here rather than under TYPE_CHECKING: SQLAlchemy resolves the
|
|
4
4
|
# annotation of `deleted_at` in this module, and needs both names at runtime.
|
|
5
|
+
from collections.abc import Iterable # noqa: TC003
|
|
5
6
|
from datetime import datetime # noqa: TC003
|
|
6
7
|
from typing import (
|
|
7
8
|
TYPE_CHECKING,
|
|
@@ -274,6 +275,46 @@ def db_for(model: type[Any]) -> BaseDatabase[Any, Any]:
|
|
|
274
275
|
return placement
|
|
275
276
|
|
|
276
277
|
|
|
278
|
+
def names_of(
|
|
279
|
+
attributes: tuple[Any, ...], listed: Iterable[str] | None
|
|
280
|
+
) -> list[str] | None:
|
|
281
|
+
"""Return the attributes to reload, as the session names them.
|
|
282
|
+
|
|
283
|
+
A rename and an editor follow the attribute a model declares, and neither
|
|
284
|
+
follows a string, so a caller may name either.
|
|
285
|
+
"""
|
|
286
|
+
named = [
|
|
287
|
+
attribute if isinstance(attribute, str) else attribute.key
|
|
288
|
+
for attribute in attributes
|
|
289
|
+
]
|
|
290
|
+
named += list(listed or ())
|
|
291
|
+
return named or None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def every_attribute(model: type[Any]) -> list[str]:
|
|
295
|
+
"""Return every attribute a model declares, relationships included."""
|
|
296
|
+
return list(sa.inspect(model).attrs.keys())
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def related_to(instance: Any) -> list[Any]: # noqa: ANN401 - a model of either API
|
|
300
|
+
"""Return the instances the loaded relationships of this one hold.
|
|
301
|
+
|
|
302
|
+
A relationship read again hands back the instances the session already
|
|
303
|
+
holds, with the values they were loaded with, so a caller that wants the
|
|
304
|
+
rows as they are now expires them.
|
|
305
|
+
"""
|
|
306
|
+
state = sa.inspect(instance)
|
|
307
|
+
found = []
|
|
308
|
+
for name in state.mapper.relationships.keys(): # noqa: SIM118
|
|
309
|
+
if name in state.unloaded:
|
|
310
|
+
continue
|
|
311
|
+
value = state.dict.get(name)
|
|
312
|
+
if value is None:
|
|
313
|
+
continue
|
|
314
|
+
found.extend(value if isinstance(value, (list, set, tuple)) else [value])
|
|
315
|
+
return found
|
|
316
|
+
|
|
317
|
+
|
|
277
318
|
def resolve_alias(model: type[Any], alias: str) -> BaseDatabase[Any, Any]:
|
|
278
319
|
"""Return the database a model knows under that alias.
|
|
279
320
|
|
|
@@ -21,6 +21,9 @@ from sqlakit._model import (
|
|
|
21
21
|
BaseModel,
|
|
22
22
|
BaseSoftDeletes,
|
|
23
23
|
DatabaseDescriptor,
|
|
24
|
+
every_attribute,
|
|
25
|
+
names_of,
|
|
26
|
+
related_to,
|
|
24
27
|
soft_delete_column,
|
|
25
28
|
tables_for,
|
|
26
29
|
)
|
|
@@ -43,6 +46,7 @@ if TYPE_CHECKING:
|
|
|
43
46
|
from collections.abc import AsyncIterator, Iterable, Mapping, Sequence
|
|
44
47
|
|
|
45
48
|
from sqlalchemy.engine import CursorResult, Result, ScalarResult
|
|
49
|
+
from sqlalchemy.orm import InstrumentedAttribute
|
|
46
50
|
from sqlalchemy.sql import Executable
|
|
47
51
|
from sqlalchemy.sql._typing import (
|
|
48
52
|
_ColumnExpressionArgument,
|
|
@@ -541,12 +545,28 @@ class QueryDescriptor(Generic[QueryT]):
|
|
|
541
545
|
|
|
542
546
|
query = QueryDescriptor(AppQuery)
|
|
543
547
|
```
|
|
548
|
+
|
|
549
|
+
Given `Query` itself, as `ModelMixin` does, it reads as a query of the model
|
|
550
|
+
it is read from: `User.query` is a `Query[User]`, so `User.query.get(1)` is
|
|
551
|
+
a `User | None`.
|
|
544
552
|
"""
|
|
545
553
|
|
|
546
554
|
def __init__(self, query_class: type[QueryT]) -> None:
|
|
547
555
|
self.query_class = query_class
|
|
548
556
|
|
|
549
|
-
|
|
557
|
+
# `ClassVar` cannot hold a type variable, so the base declares the query
|
|
558
|
+
# every model has and a read narrows it to the model it is read from.
|
|
559
|
+
@overload
|
|
560
|
+
def __get__(
|
|
561
|
+
self: QueryDescriptor[Query[Any]],
|
|
562
|
+
instance: object | None,
|
|
563
|
+
owner: type[ModelT],
|
|
564
|
+
) -> Query[ModelT]: ...
|
|
565
|
+
|
|
566
|
+
@overload
|
|
567
|
+
def __get__(self, instance: object | None, owner: type[Any]) -> QueryT: ...
|
|
568
|
+
|
|
569
|
+
def __get__(self, instance: object | None, owner: type[Any]) -> Query[Any]:
|
|
550
570
|
return self.query_class(owner, owner.db)
|
|
551
571
|
|
|
552
572
|
|
|
@@ -633,25 +653,45 @@ class ModelMixin(BaseModel[Database]):
|
|
|
633
653
|
|
|
634
654
|
async def refresh(
|
|
635
655
|
self,
|
|
636
|
-
|
|
656
|
+
*attributes: str | InstrumentedAttribute[Any],
|
|
637
657
|
attribute_names: Iterable[str] | None = None,
|
|
658
|
+
with_relationships: bool = False,
|
|
638
659
|
with_for_update: ForUpdateParameter = None,
|
|
639
660
|
) -> None:
|
|
640
661
|
"""Read this instance back from the database.
|
|
641
662
|
|
|
663
|
+
```python
|
|
664
|
+
await user.refresh() # every column, and the relationships already loaded
|
|
665
|
+
await user.refresh(User.team) # and one that was not, though it raises on load
|
|
666
|
+
await user.refresh(with_relationships=True) # every relationship the model has
|
|
667
|
+
```
|
|
668
|
+
|
|
642
669
|
Args:
|
|
643
|
-
|
|
644
|
-
A relationship named here is
|
|
670
|
+
attributes: The attributes to read again, rather than all of them, as
|
|
671
|
+
the model declares them or by name. A relationship named here is
|
|
672
|
+
loaded, which is how a `lazy="raise"` one is read after a refresh.
|
|
673
|
+
attribute_names: The same, for names a caller holds as a list.
|
|
674
|
+
with_relationships: Read every relationship the model declares, loaded
|
|
675
|
+
or not, which costs a statement each. For a test that compares the
|
|
676
|
+
whole instance and would otherwise name them one by one.
|
|
645
677
|
with_for_update: Lock the row while it is read, as
|
|
646
678
|
``Session.refresh`` takes it: `True` for a plain ``FOR UPDATE``,
|
|
647
679
|
or a mapping such as ``{"read": True}``.
|
|
648
680
|
|
|
649
681
|
"""
|
|
682
|
+
names = names_of(attributes, attribute_names)
|
|
683
|
+
if with_relationships:
|
|
684
|
+
names = list(dict.fromkeys([*(names or ()), *every_attribute(type(self))]))
|
|
650
685
|
await self.db.session.refresh(
|
|
651
686
|
self,
|
|
652
|
-
attribute_names=
|
|
687
|
+
attribute_names=names,
|
|
653
688
|
with_for_update=with_for_update,
|
|
654
689
|
)
|
|
690
|
+
if with_relationships:
|
|
691
|
+
# The relationships hand back the instances the session holds, and
|
|
692
|
+
# those carry the values they were loaded with.
|
|
693
|
+
for related in related_to(self):
|
|
694
|
+
self.db.session.expire(related)
|
|
655
695
|
|
|
656
696
|
async def _persist(self) -> None:
|
|
657
697
|
db = self.db
|
|
@@ -22,6 +22,9 @@ from ._model import (
|
|
|
22
22
|
BaseModel,
|
|
23
23
|
BaseSoftDeletes,
|
|
24
24
|
DatabaseDescriptor,
|
|
25
|
+
every_attribute,
|
|
26
|
+
names_of,
|
|
27
|
+
related_to,
|
|
25
28
|
soft_delete_column,
|
|
26
29
|
tables_for,
|
|
27
30
|
)
|
|
@@ -42,6 +45,7 @@ if TYPE_CHECKING:
|
|
|
42
45
|
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
43
46
|
|
|
44
47
|
from sqlalchemy.engine import CursorResult, Result, ScalarResult
|
|
48
|
+
from sqlalchemy.orm import InstrumentedAttribute
|
|
45
49
|
from sqlalchemy.sql import Executable
|
|
46
50
|
from sqlalchemy.sql._typing import (
|
|
47
51
|
_ColumnExpressionArgument,
|
|
@@ -529,12 +533,28 @@ class QueryDescriptor(Generic[QueryT]):
|
|
|
529
533
|
|
|
530
534
|
query = QueryDescriptor(AppQuery)
|
|
531
535
|
```
|
|
536
|
+
|
|
537
|
+
Given `Query` itself, as `ModelMixin` does, it reads as a query of the model
|
|
538
|
+
it is read from: `User.query` is a `Query[User]`, so `User.query.get(1)` is
|
|
539
|
+
a `User | None`.
|
|
532
540
|
"""
|
|
533
541
|
|
|
534
542
|
def __init__(self, query_class: type[QueryT]) -> None:
|
|
535
543
|
self.query_class = query_class
|
|
536
544
|
|
|
537
|
-
|
|
545
|
+
# `ClassVar` cannot hold a type variable, so the base declares the query
|
|
546
|
+
# every model has and a read narrows it to the model it is read from.
|
|
547
|
+
@overload
|
|
548
|
+
def __get__(
|
|
549
|
+
self: QueryDescriptor[Query[Any]],
|
|
550
|
+
instance: object | None,
|
|
551
|
+
owner: type[ModelT],
|
|
552
|
+
) -> Query[ModelT]: ...
|
|
553
|
+
|
|
554
|
+
@overload
|
|
555
|
+
def __get__(self, instance: object | None, owner: type[Any]) -> QueryT: ...
|
|
556
|
+
|
|
557
|
+
def __get__(self, instance: object | None, owner: type[Any]) -> Query[Any]:
|
|
538
558
|
return self.query_class(owner, owner.db)
|
|
539
559
|
|
|
540
560
|
|
|
@@ -620,25 +640,45 @@ class ModelMixin(BaseModel[Database]):
|
|
|
620
640
|
|
|
621
641
|
def refresh(
|
|
622
642
|
self,
|
|
623
|
-
|
|
643
|
+
*attributes: str | InstrumentedAttribute[Any],
|
|
624
644
|
attribute_names: Iterable[str] | None = None,
|
|
645
|
+
with_relationships: bool = False,
|
|
625
646
|
with_for_update: ForUpdateParameter = None,
|
|
626
647
|
) -> None:
|
|
627
648
|
"""Read this instance back from the database.
|
|
628
649
|
|
|
650
|
+
```python
|
|
651
|
+
user.refresh() # every column, and the relationships already loaded
|
|
652
|
+
user.refresh(User.team) # and one that was not, though it raises on load
|
|
653
|
+
user.refresh(with_relationships=True) # every relationship the model has
|
|
654
|
+
```
|
|
655
|
+
|
|
629
656
|
Args:
|
|
630
|
-
|
|
631
|
-
A relationship named here is
|
|
657
|
+
attributes: The attributes to read again, rather than all of them, as
|
|
658
|
+
the model declares them or by name. A relationship named here is
|
|
659
|
+
loaded, which is how a `lazy="raise"` one is read after a refresh.
|
|
660
|
+
attribute_names: The same, for names a caller holds as a list.
|
|
661
|
+
with_relationships: Read every relationship the model declares, loaded
|
|
662
|
+
or not, which costs a statement each. For a test that compares the
|
|
663
|
+
whole instance and would otherwise name them one by one.
|
|
632
664
|
with_for_update: Lock the row while it is read, as
|
|
633
665
|
``Session.refresh`` takes it: `True` for a plain ``FOR UPDATE``,
|
|
634
666
|
or a mapping such as ``{"read": True}``.
|
|
635
667
|
|
|
636
668
|
"""
|
|
669
|
+
names = names_of(attributes, attribute_names)
|
|
670
|
+
if with_relationships:
|
|
671
|
+
names = list(dict.fromkeys([*(names or ()), *every_attribute(type(self))]))
|
|
637
672
|
self.db.session.refresh(
|
|
638
673
|
self,
|
|
639
|
-
attribute_names=
|
|
674
|
+
attribute_names=names,
|
|
640
675
|
with_for_update=with_for_update,
|
|
641
676
|
)
|
|
677
|
+
if with_relationships:
|
|
678
|
+
# The relationships hand back the instances the session holds, and
|
|
679
|
+
# those carry the values they were loaded with.
|
|
680
|
+
for related in related_to(self):
|
|
681
|
+
self.db.session.expire(related)
|
|
642
682
|
|
|
643
683
|
def _persist(self) -> None:
|
|
644
684
|
db = self.db
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|