sqlakit 0.9.0__tar.gz → 0.10.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.9.0 → sqlakit-0.10.0}/PKG-INFO +1 -1
- {sqlakit-0.9.0 → sqlakit-0.10.0}/pyproject.toml +1 -1
- {sqlakit-0.9.0 → sqlakit-0.10.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_base.py +101 -22
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_model.py +9 -7
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_registry.py +3 -1
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/_registry.py +3 -1
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/debugserver.html +30 -30
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/exceptions.py +11 -3
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/pytest_plugin.py +11 -2
- {sqlakit-0.9.0 → sqlakit-0.10.0}/LICENSE +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/README.md +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/__init__.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_cli.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_debugserver.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_query.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/orm.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/types.py +0 -0
|
@@ -39,6 +39,7 @@ from ._recording import (
|
|
|
39
39
|
from ._routing import Router, as_router
|
|
40
40
|
from .exceptions import (
|
|
41
41
|
DEFAULT_ALIAS,
|
|
42
|
+
REGISTERED_DEFAULT,
|
|
42
43
|
AliasInUseError,
|
|
43
44
|
ConflictingDatabaseUrlError,
|
|
44
45
|
DatabaseAlreadyConfiguredError,
|
|
@@ -206,6 +207,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
206
207
|
engine_args: EngineArgs | None = None,
|
|
207
208
|
session_args: SessionArgs | None = None,
|
|
208
209
|
templates: TemplatesLike | None = None,
|
|
210
|
+
alias: str | None = None,
|
|
209
211
|
**parts: Unpack[UrlParts],
|
|
210
212
|
) -> None:
|
|
211
213
|
"""Build a database on ``url``, or on the parts to make one from.
|
|
@@ -220,8 +222,14 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
220
222
|
)
|
|
221
223
|
|
|
222
224
|
Database(DB_URL, templates="app/sql") # where `sql` reads templates from
|
|
225
|
+
Database(WAREHOUSE_URL, alias="warehouse") # what a recording calls it
|
|
223
226
|
```
|
|
224
227
|
|
|
228
|
+
``alias`` is the name a recorded statement carries, for telling two
|
|
229
|
+
databases apart in a log or on the debug server's page. A registry
|
|
230
|
+
names the databases it holds after the aliases they are registered
|
|
231
|
+
under, so it is worth setting on a database you keep yourself.
|
|
232
|
+
|
|
225
233
|
Raises:
|
|
226
234
|
MissingDatabaseUrlError: if given neither a ``url`` nor the parts to
|
|
227
235
|
build one.
|
|
@@ -258,7 +266,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
258
266
|
self._listening = 0
|
|
259
267
|
self._listened: Any = None
|
|
260
268
|
self._listening_lock = threading.Lock()
|
|
261
|
-
self._name = DEFAULT_ALIAS
|
|
269
|
+
self._name = alias or DEFAULT_ALIAS
|
|
262
270
|
|
|
263
271
|
def __repr__(self) -> str:
|
|
264
272
|
return f"{type(self).__name__}({self.url.render_as_string()!r})"
|
|
@@ -703,6 +711,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
703
711
|
|
|
704
712
|
def __init__(self) -> None:
|
|
705
713
|
"""Leave everything to [`configure`][sqlakit.Databases.configure]."""
|
|
714
|
+
self._default: DatabaseT | None = None
|
|
706
715
|
self._aliased: dict[str, DatabaseT] = {}
|
|
707
716
|
self._routers: tuple[Any, ...] = ()
|
|
708
717
|
self._using: ContextVar[str | None] = ContextVar(
|
|
@@ -717,14 +726,15 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
717
726
|
def __getitem__(self, alias: str) -> Self | DatabaseT:
|
|
718
727
|
"""Return the database configured as ``alias``.
|
|
719
728
|
|
|
720
|
-
``db["default"]`` is
|
|
729
|
+
``db["default"]`` is what the code reaches without an alias: this
|
|
730
|
+
registry, or the database `register` was given for that name.
|
|
721
731
|
|
|
722
732
|
Raises:
|
|
723
733
|
UnknownDatabaseError: if nothing is configured under that alias.
|
|
724
734
|
|
|
725
735
|
"""
|
|
726
736
|
if alias == DEFAULT_ALIAS:
|
|
727
|
-
return self
|
|
737
|
+
return self if self._default is None else self._default
|
|
728
738
|
try:
|
|
729
739
|
return self._aliased[alias]
|
|
730
740
|
except KeyError:
|
|
@@ -744,16 +754,25 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
744
754
|
db.register("shard-7", Database(SHARD_URL))
|
|
745
755
|
```
|
|
746
756
|
|
|
757
|
+
`default` is the alias the code reaches without naming one, and a
|
|
758
|
+
database registered under it stands where `configure` would have built
|
|
759
|
+
one. The registry itself is then a registry alone: reach that database
|
|
760
|
+
as `db["default"]`, or through the models that live on it.
|
|
761
|
+
|
|
747
762
|
The alias has to be free. Replacing one under a name already in use
|
|
748
763
|
would leave the code that holds the old database talking to it.
|
|
749
764
|
|
|
750
765
|
Raises:
|
|
751
766
|
AliasInUseError: if another database holds that alias.
|
|
752
|
-
DefaultAliasError: if
|
|
767
|
+
DefaultAliasError: if `default` is asked for and the registry
|
|
768
|
+
already has one.
|
|
753
769
|
|
|
754
770
|
"""
|
|
755
771
|
if alias == DEFAULT_ALIAS:
|
|
756
|
-
|
|
772
|
+
if self.is_configured:
|
|
773
|
+
raise DefaultAliasError
|
|
774
|
+
self._default = self._named(alias, db)
|
|
775
|
+
return
|
|
757
776
|
if alias in self._aliased:
|
|
758
777
|
raise AliasInUseError(alias)
|
|
759
778
|
self._aliased[alias] = self._named(alias, db)
|
|
@@ -769,6 +788,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
769
788
|
skip_queries_from: Sequence[str | PathLike[str]] = (),
|
|
770
789
|
into: Recording | None = None,
|
|
771
790
|
debugserver: DebugServer | tuple[str, int] | None = None,
|
|
791
|
+
using: str | DatabaseT | Sequence[str | DatabaseT] | None = None,
|
|
772
792
|
) -> Iterator[Recording]:
|
|
773
793
|
"""Record every database this registry has, not the default one alone.
|
|
774
794
|
|
|
@@ -779,11 +799,26 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
779
799
|
record.databases # ("default", "warehouse")
|
|
780
800
|
```
|
|
781
801
|
|
|
782
|
-
Statements say which database ran them.
|
|
783
|
-
|
|
802
|
+
Statements say which database ran them. ``using`` narrows the block to
|
|
803
|
+
the databases named, by alias or in person, as `assert_queries` takes
|
|
804
|
+
them:
|
|
805
|
+
|
|
806
|
+
```python
|
|
807
|
+
with db.recording(using="warehouse"):
|
|
808
|
+
move_the_reports()
|
|
809
|
+
|
|
810
|
+
with db.recording(using=["default", "warehouse"]):
|
|
811
|
+
move_the_reports()
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
`db["warehouse"].recording()` records that one on its own.
|
|
815
|
+
|
|
816
|
+
Raises:
|
|
817
|
+
UnknownDatabaseError: if ``using`` names an alias nothing holds.
|
|
818
|
+
|
|
784
819
|
"""
|
|
785
820
|
together = Recording(label=label) if into is None else into
|
|
786
|
-
databases =
|
|
821
|
+
databases = self._recorded(using)
|
|
787
822
|
with ExitStack() as stack:
|
|
788
823
|
for db in databases:
|
|
789
824
|
stack.enter_context(
|
|
@@ -805,6 +840,15 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
805
840
|
if debugserver is not None:
|
|
806
841
|
send_recording(together, debugserver)
|
|
807
842
|
|
|
843
|
+
def _recorded(
|
|
844
|
+
self, using: str | DatabaseT | Sequence[str | DatabaseT] | None
|
|
845
|
+
) -> tuple[Any, ...]:
|
|
846
|
+
"""Return the databases a recording watches: the ones named, or all of them."""
|
|
847
|
+
if using is None:
|
|
848
|
+
return tuple(self[alias] for alias in self.aliases)
|
|
849
|
+
asked = using if isinstance(using, (list, tuple, set, frozenset)) else (using,)
|
|
850
|
+
return tuple(self[one] if isinstance(one, str) else one for one in asked)
|
|
851
|
+
|
|
808
852
|
@staticmethod
|
|
809
853
|
def _named(alias: str, db: DatabaseT) -> DatabaseT:
|
|
810
854
|
"""Let a database say which alias it answers to, when it is recorded."""
|
|
@@ -888,7 +932,12 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
888
932
|
|
|
889
933
|
@property
|
|
890
934
|
def is_configured(self) -> bool:
|
|
891
|
-
"""Whether
|
|
935
|
+
"""Whether this registry has a default database to reach."""
|
|
936
|
+
return "url" in self.__dict__ or self._default is not None
|
|
937
|
+
|
|
938
|
+
@property
|
|
939
|
+
def _built_its_own(self) -> bool:
|
|
940
|
+
"""Whether the default database is this registry, `configure` having built it."""
|
|
892
941
|
return "url" in self.__dict__
|
|
893
942
|
|
|
894
943
|
@overload
|
|
@@ -963,6 +1012,8 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
963
1012
|
``InvalidDatabaseConfigError``.
|
|
964
1013
|
|
|
965
1014
|
"""
|
|
1015
|
+
if self._default is not None:
|
|
1016
|
+
raise DefaultAliasError
|
|
966
1017
|
if not isinstance(url, Mapping):
|
|
967
1018
|
self._reject_if_connected()
|
|
968
1019
|
super().__init__(url, engine_args, session_args, templates, **parts)
|
|
@@ -997,7 +1048,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
997
1048
|
self.route(*routers)
|
|
998
1049
|
|
|
999
1050
|
def _reject_if_connected(self) -> None:
|
|
1000
|
-
connected = self.
|
|
1051
|
+
connected = self._built_its_own and self._engine is not None
|
|
1001
1052
|
if connected or any(
|
|
1002
1053
|
db._engine is not None # noqa: SLF001
|
|
1003
1054
|
for db in self._aliased.values()
|
|
@@ -1007,19 +1058,47 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1007
1058
|
if not TYPE_CHECKING:
|
|
1008
1059
|
# Hidden from type checkers: seeing it, they would take every attribute
|
|
1009
1060
|
# to exist and stop reporting typos. It is reached when normal lookup
|
|
1010
|
-
# fails, which is what
|
|
1061
|
+
# fails, which is what a registry with no database of its own looks
|
|
1062
|
+
# like, from the outside and from its own methods.
|
|
1011
1063
|
def __getattr__(self, name: str) -> object:
|
|
1012
|
-
# Only the database
|
|
1013
|
-
#
|
|
1014
|
-
# `
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1064
|
+
# Only the database half is worth explaining. Anything else is a
|
|
1065
|
+
# name that does not exist, and saying so is what lets `hasattr`,
|
|
1066
|
+
# `copy` and every library that introspects work.
|
|
1067
|
+
state = self.__dict__
|
|
1068
|
+
if "url" in state:
|
|
1069
|
+
raise AttributeError(name)
|
|
1070
|
+
asked_as_a_database = name in DATABASE_STATE or (
|
|
1071
|
+
not name.startswith("_") and hasattr(type(self), name)
|
|
1072
|
+
)
|
|
1073
|
+
if not asked_as_a_database:
|
|
1074
|
+
raise AttributeError(name)
|
|
1075
|
+
if state.get("_default") is not None:
|
|
1076
|
+
raise DatabaseNotConfiguredError(REGISTERED_DEFAULT) from None
|
|
1077
|
+
raise DatabaseNotConfiguredError from None
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
DATABASE_STATE = frozenset(
|
|
1081
|
+
# What `BaseDatabase.__init__` sets. A registry has these once it has a
|
|
1082
|
+
# database of its own, and reaching for one before then is the question
|
|
1083
|
+
# `DatabaseNotConfiguredError` answers, whichever method asked.
|
|
1084
|
+
{
|
|
1085
|
+
"url",
|
|
1086
|
+
"templates",
|
|
1087
|
+
"engine_args",
|
|
1088
|
+
"session_args",
|
|
1089
|
+
"_sessionmaker",
|
|
1090
|
+
"_engine_lock",
|
|
1091
|
+
"_scope",
|
|
1092
|
+
"_outer",
|
|
1093
|
+
"_recordings",
|
|
1094
|
+
"_stacks",
|
|
1095
|
+
"_skipped",
|
|
1096
|
+
"_listening",
|
|
1097
|
+
"_listened",
|
|
1098
|
+
"_listening_lock",
|
|
1099
|
+
"_name",
|
|
1100
|
+
}
|
|
1101
|
+
)
|
|
1023
1102
|
|
|
1024
1103
|
_CONTROL = frozenset(
|
|
1025
1104
|
# Transaction control is not a query, and which of these reach a cursor
|
|
@@ -138,27 +138,29 @@ 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
|
|
142
|
-
|
|
141
|
+
Without an alias it goes in as the default one, which is where a model
|
|
142
|
+
that names no database lives:
|
|
143
143
|
|
|
144
144
|
```python
|
|
145
145
|
Base.register_db(Database(DB_URL))
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
The registry answers for it either way, so `dbs["default"]`,
|
|
149
|
+
`dbs.transactions()` and `using()` all reach what was registered.
|
|
150
|
+
[`set_db`][sqlakit.orm.ModelMixin.set_db] is the other way to name a
|
|
151
|
+
database, and it leaves the registry out of it.
|
|
152
|
+
|
|
148
153
|
Raises:
|
|
149
154
|
AliasInUseError: if another database holds that alias.
|
|
150
|
-
DefaultAliasError: if the
|
|
155
|
+
DefaultAliasError: if the registry already has a default database.
|
|
151
156
|
|
|
152
157
|
"""
|
|
153
|
-
if alias is None:
|
|
154
|
-
cls.set_db(db)
|
|
155
|
-
return
|
|
156
158
|
if _owns_no_registry(cls):
|
|
157
159
|
# A registry of its own: registering into the importable one would
|
|
158
160
|
# configure it for every model in the process. A class under one
|
|
159
161
|
# that already has its own registers into that one.
|
|
160
162
|
cls.__dbs__ = type(cls.__dbs__)()
|
|
161
|
-
cast("DatabaseRegistry", cls.__dbs__).register(alias, db)
|
|
163
|
+
cast("DatabaseRegistry", cls.__dbs__).register(alias or DEFAULT_ALIAS, db)
|
|
162
164
|
|
|
163
165
|
def update(self, values: Mapping[str, Any]) -> Self:
|
|
164
166
|
"""Set these fields on this instance, and return it.
|
|
@@ -52,8 +52,10 @@ class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
|
52
52
|
|
|
53
53
|
def dispose(self, *, close: bool = True) -> None:
|
|
54
54
|
"""Dispose of every configured database, not just the default one."""
|
|
55
|
-
if self.
|
|
55
|
+
if self._built_its_own:
|
|
56
56
|
super().dispose(close=close)
|
|
57
|
+
elif self._default is not None:
|
|
58
|
+
self._default.dispose(close=close)
|
|
57
59
|
for db in self._aliased.values():
|
|
58
60
|
db.dispose(close=close)
|
|
59
61
|
|
|
@@ -47,8 +47,10 @@ class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
|
47
47
|
|
|
48
48
|
async def dispose(self, *, close: bool = True) -> None:
|
|
49
49
|
"""Dispose of every configured database, not just the default one."""
|
|
50
|
-
if self.
|
|
50
|
+
if self._built_its_own:
|
|
51
51
|
await super().dispose(close=close)
|
|
52
|
+
elif self._default is not None:
|
|
53
|
+
await self._default.dispose(close=close)
|
|
52
54
|
for db in self._aliased.values():
|
|
53
55
|
await db.dispose(close=close)
|
|
54
56
|
|