sqlakit 0.16.0__tar.gz → 0.17.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.17.0}/PKG-INFO +1 -1
- {sqlakit-0.16.0 → sqlakit-0.17.0}/pyproject.toml +1 -1
- {sqlakit-0.16.0 → sqlakit-0.17.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_model.py +41 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/asyncio/orm.py +28 -4
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/orm.py +28 -4
- {sqlakit-0.16.0 → sqlakit-0.17.0}/LICENSE +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/README.md +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/__init__.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_cli.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_debugserver.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_query.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/debugserver.html +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.16.0 → sqlakit-0.17.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,
|
|
@@ -633,25 +637,45 @@ class ModelMixin(BaseModel[Database]):
|
|
|
633
637
|
|
|
634
638
|
async def refresh(
|
|
635
639
|
self,
|
|
636
|
-
|
|
640
|
+
*attributes: str | InstrumentedAttribute[Any],
|
|
637
641
|
attribute_names: Iterable[str] | None = None,
|
|
642
|
+
with_relationships: bool = False,
|
|
638
643
|
with_for_update: ForUpdateParameter = None,
|
|
639
644
|
) -> None:
|
|
640
645
|
"""Read this instance back from the database.
|
|
641
646
|
|
|
647
|
+
```python
|
|
648
|
+
await user.refresh() # every column, and the relationships already loaded
|
|
649
|
+
await user.refresh(User.team) # and one that was not, though it raises on load
|
|
650
|
+
await user.refresh(with_relationships=True) # every relationship the model has
|
|
651
|
+
```
|
|
652
|
+
|
|
642
653
|
Args:
|
|
643
|
-
|
|
644
|
-
A relationship named here is
|
|
654
|
+
attributes: The attributes to read again, rather than all of them, as
|
|
655
|
+
the model declares them or by name. A relationship named here is
|
|
656
|
+
loaded, which is how a `lazy="raise"` one is read after a refresh.
|
|
657
|
+
attribute_names: The same, for names a caller holds as a list.
|
|
658
|
+
with_relationships: Read every relationship the model declares, loaded
|
|
659
|
+
or not, which costs a statement each. For a test that compares the
|
|
660
|
+
whole instance and would otherwise name them one by one.
|
|
645
661
|
with_for_update: Lock the row while it is read, as
|
|
646
662
|
``Session.refresh`` takes it: `True` for a plain ``FOR UPDATE``,
|
|
647
663
|
or a mapping such as ``{"read": True}``.
|
|
648
664
|
|
|
649
665
|
"""
|
|
666
|
+
names = names_of(attributes, attribute_names)
|
|
667
|
+
if with_relationships:
|
|
668
|
+
names = list(dict.fromkeys([*(names or ()), *every_attribute(type(self))]))
|
|
650
669
|
await self.db.session.refresh(
|
|
651
670
|
self,
|
|
652
|
-
attribute_names=
|
|
671
|
+
attribute_names=names,
|
|
653
672
|
with_for_update=with_for_update,
|
|
654
673
|
)
|
|
674
|
+
if with_relationships:
|
|
675
|
+
# The relationships hand back the instances the session holds, and
|
|
676
|
+
# those carry the values they were loaded with.
|
|
677
|
+
for related in related_to(self):
|
|
678
|
+
self.db.session.expire(related)
|
|
655
679
|
|
|
656
680
|
async def _persist(self) -> None:
|
|
657
681
|
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,
|
|
@@ -620,25 +624,45 @@ class ModelMixin(BaseModel[Database]):
|
|
|
620
624
|
|
|
621
625
|
def refresh(
|
|
622
626
|
self,
|
|
623
|
-
|
|
627
|
+
*attributes: str | InstrumentedAttribute[Any],
|
|
624
628
|
attribute_names: Iterable[str] | None = None,
|
|
629
|
+
with_relationships: bool = False,
|
|
625
630
|
with_for_update: ForUpdateParameter = None,
|
|
626
631
|
) -> None:
|
|
627
632
|
"""Read this instance back from the database.
|
|
628
633
|
|
|
634
|
+
```python
|
|
635
|
+
user.refresh() # every column, and the relationships already loaded
|
|
636
|
+
user.refresh(User.team) # and one that was not, though it raises on load
|
|
637
|
+
user.refresh(with_relationships=True) # every relationship the model has
|
|
638
|
+
```
|
|
639
|
+
|
|
629
640
|
Args:
|
|
630
|
-
|
|
631
|
-
A relationship named here is
|
|
641
|
+
attributes: The attributes to read again, rather than all of them, as
|
|
642
|
+
the model declares them or by name. A relationship named here is
|
|
643
|
+
loaded, which is how a `lazy="raise"` one is read after a refresh.
|
|
644
|
+
attribute_names: The same, for names a caller holds as a list.
|
|
645
|
+
with_relationships: Read every relationship the model declares, loaded
|
|
646
|
+
or not, which costs a statement each. For a test that compares the
|
|
647
|
+
whole instance and would otherwise name them one by one.
|
|
632
648
|
with_for_update: Lock the row while it is read, as
|
|
633
649
|
``Session.refresh`` takes it: `True` for a plain ``FOR UPDATE``,
|
|
634
650
|
or a mapping such as ``{"read": True}``.
|
|
635
651
|
|
|
636
652
|
"""
|
|
653
|
+
names = names_of(attributes, attribute_names)
|
|
654
|
+
if with_relationships:
|
|
655
|
+
names = list(dict.fromkeys([*(names or ()), *every_attribute(type(self))]))
|
|
637
656
|
self.db.session.refresh(
|
|
638
657
|
self,
|
|
639
|
-
attribute_names=
|
|
658
|
+
attribute_names=names,
|
|
640
659
|
with_for_update=with_for_update,
|
|
641
660
|
)
|
|
661
|
+
if with_relationships:
|
|
662
|
+
# The relationships hand back the instances the session holds, and
|
|
663
|
+
# those carry the values they were loaded with.
|
|
664
|
+
for related in related_to(self):
|
|
665
|
+
self.db.session.expire(related)
|
|
642
666
|
|
|
643
667
|
def _persist(self) -> None:
|
|
644
668
|
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
|