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.
Files changed (30) hide show
  1. {sqlakit-0.9.0 → sqlakit-0.10.0}/PKG-INFO +1 -1
  2. {sqlakit-0.9.0 → sqlakit-0.10.0}/pyproject.toml +1 -1
  3. {sqlakit-0.9.0 → sqlakit-0.10.0}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_base.py +101 -22
  5. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_model.py +9 -7
  6. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_registry.py +3 -1
  7. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/_registry.py +3 -1
  8. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/debugserver.html +30 -30
  9. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/exceptions.py +11 -3
  10. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/pytest_plugin.py +11 -2
  11. {sqlakit-0.9.0 → sqlakit-0.10.0}/LICENSE +0 -0
  12. {sqlakit-0.9.0 → sqlakit-0.10.0}/README.md +0 -0
  13. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/__init__.py +0 -0
  14. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_cli.py +0 -0
  15. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_db.py +0 -0
  16. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_debugserver.py +0 -0
  17. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_discovery.py +0 -0
  18. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_query.py +0 -0
  19. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_recording.py +0 -0
  20. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_routing.py +0 -0
  21. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/_sql.py +0 -0
  22. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/__init__.py +0 -0
  23. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/_db.py +0 -0
  24. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/orm.py +0 -0
  25. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/asyncio/sql.py +0 -0
  26. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/orm.py +0 -0
  27. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/py.typed +0 -0
  28. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/sql.py +0 -0
  29. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/testing.py +0 -0
  30. {sqlakit-0.9.0 → sqlakit-0.10.0}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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 this one: what the code reaches without an alias.
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 the alias is `default`, which this registry is.
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
- raise DefaultAliasError
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. `db["warehouse"].recording()` records
783
- that one on its own.
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 = (self, *self._aliased.values())
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 [`configure`][sqlakit.Databases.configure] has been called."""
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.is_configured and self._engine is not None
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 an unconfigured database looks like.
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's own attributes are worth explaining. Anything
1013
- # else is a name that does not exist, and saying so is what lets
1014
- # `hasattr`, `copy` and every library that introspects work.
1015
- if (
1016
- not name.startswith("_")
1017
- and hasattr(type(self), name)
1018
- and not self.is_configured
1019
- ):
1020
- raise DatabaseNotConfiguredError from None
1021
- raise AttributeError(name)
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 points the model at that database, the same as
142
- [`set_db`][sqlakit.orm.ModelMixin.set_db], for an application with one:
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 alias is `default`.
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.is_configured:
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.is_configured:
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