sqlakit 0.9.0__tar.gz → 0.9.1__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.9.1}/PKG-INFO +1 -1
  2. {sqlakit-0.9.0 → sqlakit-0.9.1}/pyproject.toml +1 -1
  3. {sqlakit-0.9.0 → sqlakit-0.9.1}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_base.py +66 -19
  5. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_model.py +9 -7
  6. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_registry.py +3 -1
  7. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/asyncio/_registry.py +3 -1
  8. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/exceptions.py +11 -3
  9. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/pytest_plugin.py +11 -2
  10. {sqlakit-0.9.0 → sqlakit-0.9.1}/LICENSE +0 -0
  11. {sqlakit-0.9.0 → sqlakit-0.9.1}/README.md +0 -0
  12. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/__init__.py +0 -0
  13. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_cli.py +0 -0
  14. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_db.py +0 -0
  15. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_debugserver.py +0 -0
  16. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_discovery.py +0 -0
  17. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_query.py +0 -0
  18. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_recording.py +0 -0
  19. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_routing.py +0 -0
  20. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/_sql.py +0 -0
  21. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/asyncio/__init__.py +0 -0
  22. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/asyncio/_db.py +0 -0
  23. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/asyncio/orm.py +0 -0
  24. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/asyncio/sql.py +0 -0
  25. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/debugserver.html +0 -0
  26. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/orm.py +0 -0
  27. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/py.typed +0 -0
  28. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/sql.py +0 -0
  29. {sqlakit-0.9.0 → sqlakit-0.9.1}/sqlakit/testing.py +0 -0
  30. {sqlakit-0.9.0 → sqlakit-0.9.1}/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.9.1
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.9.1"
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.9.1"
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,
@@ -703,6 +704,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
703
704
 
704
705
  def __init__(self) -> None:
705
706
  """Leave everything to [`configure`][sqlakit.Databases.configure]."""
707
+ self._default: DatabaseT | None = None
706
708
  self._aliased: dict[str, DatabaseT] = {}
707
709
  self._routers: tuple[Any, ...] = ()
708
710
  self._using: ContextVar[str | None] = ContextVar(
@@ -717,14 +719,15 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
717
719
  def __getitem__(self, alias: str) -> Self | DatabaseT:
718
720
  """Return the database configured as ``alias``.
719
721
 
720
- ``db["default"]`` is this one: what the code reaches without an alias.
722
+ ``db["default"]`` is what the code reaches without an alias: this
723
+ registry, or the database `register` was given for that name.
721
724
 
722
725
  Raises:
723
726
  UnknownDatabaseError: if nothing is configured under that alias.
724
727
 
725
728
  """
726
729
  if alias == DEFAULT_ALIAS:
727
- return self
730
+ return self if self._default is None else self._default
728
731
  try:
729
732
  return self._aliased[alias]
730
733
  except KeyError:
@@ -744,16 +747,25 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
744
747
  db.register("shard-7", Database(SHARD_URL))
745
748
  ```
746
749
 
750
+ `default` is the alias the code reaches without naming one, and a
751
+ database registered under it stands where `configure` would have built
752
+ one. The registry itself is then a registry alone: reach that database
753
+ as `db["default"]`, or through the models that live on it.
754
+
747
755
  The alias has to be free. Replacing one under a name already in use
748
756
  would leave the code that holds the old database talking to it.
749
757
 
750
758
  Raises:
751
759
  AliasInUseError: if another database holds that alias.
752
- DefaultAliasError: if the alias is `default`, which this registry is.
760
+ DefaultAliasError: if `default` is asked for and the registry
761
+ already has one.
753
762
 
754
763
  """
755
764
  if alias == DEFAULT_ALIAS:
756
- raise DefaultAliasError
765
+ if self.is_configured:
766
+ raise DefaultAliasError
767
+ self._default = self._named(alias, db)
768
+ return
757
769
  if alias in self._aliased:
758
770
  raise AliasInUseError(alias)
759
771
  self._aliased[alias] = self._named(alias, db)
@@ -783,7 +795,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
783
795
  that one on its own.
784
796
  """
785
797
  together = Recording(label=label) if into is None else into
786
- databases = (self, *self._aliased.values())
798
+ databases = tuple(self[alias] for alias in self.aliases)
787
799
  with ExitStack() as stack:
788
800
  for db in databases:
789
801
  stack.enter_context(
@@ -888,7 +900,12 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
888
900
 
889
901
  @property
890
902
  def is_configured(self) -> bool:
891
- """Whether [`configure`][sqlakit.Databases.configure] has been called."""
903
+ """Whether this registry has a default database to reach."""
904
+ return "url" in self.__dict__ or self._default is not None
905
+
906
+ @property
907
+ def _built_its_own(self) -> bool:
908
+ """Whether the default database is this registry, `configure` having built it."""
892
909
  return "url" in self.__dict__
893
910
 
894
911
  @overload
@@ -963,6 +980,8 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
963
980
  ``InvalidDatabaseConfigError``.
964
981
 
965
982
  """
983
+ if self._default is not None:
984
+ raise DefaultAliasError
966
985
  if not isinstance(url, Mapping):
967
986
  self._reject_if_connected()
968
987
  super().__init__(url, engine_args, session_args, templates, **parts)
@@ -997,7 +1016,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
997
1016
  self.route(*routers)
998
1017
 
999
1018
  def _reject_if_connected(self) -> None:
1000
- connected = self.is_configured and self._engine is not None
1019
+ connected = self._built_its_own and self._engine is not None
1001
1020
  if connected or any(
1002
1021
  db._engine is not None # noqa: SLF001
1003
1022
  for db in self._aliased.values()
@@ -1007,19 +1026,47 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1007
1026
  if not TYPE_CHECKING:
1008
1027
  # Hidden from type checkers: seeing it, they would take every attribute
1009
1028
  # to exist and stop reporting typos. It is reached when normal lookup
1010
- # fails, which is what an unconfigured database looks like.
1029
+ # fails, which is what a registry with no database of its own looks
1030
+ # like, from the outside and from its own methods.
1011
1031
  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
-
1032
+ # Only the database half is worth explaining. Anything else is a
1033
+ # name that does not exist, and saying so is what lets `hasattr`,
1034
+ # `copy` and every library that introspects work.
1035
+ state = self.__dict__
1036
+ if "url" in state:
1037
+ raise AttributeError(name)
1038
+ asked_as_a_database = name in DATABASE_STATE or (
1039
+ not name.startswith("_") and hasattr(type(self), name)
1040
+ )
1041
+ if not asked_as_a_database:
1042
+ raise AttributeError(name)
1043
+ if state.get("_default") is not None:
1044
+ raise DatabaseNotConfiguredError(REGISTERED_DEFAULT) from None
1045
+ raise DatabaseNotConfiguredError from None
1046
+
1047
+
1048
+ DATABASE_STATE = frozenset(
1049
+ # What `BaseDatabase.__init__` sets. A registry has these once it has a
1050
+ # database of its own, and reaching for one before then is the question
1051
+ # `DatabaseNotConfiguredError` answers, whichever method asked.
1052
+ {
1053
+ "url",
1054
+ "templates",
1055
+ "engine_args",
1056
+ "session_args",
1057
+ "_sessionmaker",
1058
+ "_engine_lock",
1059
+ "_scope",
1060
+ "_outer",
1061
+ "_recordings",
1062
+ "_stacks",
1063
+ "_skipped",
1064
+ "_listening",
1065
+ "_listened",
1066
+ "_listening_lock",
1067
+ "_name",
1068
+ }
1069
+ )
1023
1070
 
1024
1071
  _CONTROL = frozenset(
1025
1072
  # 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
 
@@ -85,6 +85,13 @@ class RetryNotSupportedError(SQLAKitError, TypeError):
85
85
  super().__init__(message)
86
86
 
87
87
 
88
+ REGISTERED_DEFAULT = (
89
+ "This registry did not build the default database, it was registered. "
90
+ "Reach that one as `db['default']`, or through the models that live on it."
91
+ )
92
+ """What a registry says when it is asked for connections it does not have."""
93
+
94
+
88
95
  class DatabaseNotConfiguredError(SQLAKitError, RuntimeError):
89
96
  """Raised when the importable database is used before it has a URL."""
90
97
 
@@ -123,12 +130,13 @@ class AliasInUseError(SQLAKitError, ValueError):
123
130
 
124
131
 
125
132
  class DefaultAliasError(SQLAKitError, ValueError):
126
- """Raised when registering a database as the default one."""
133
+ """Raised when a registry is given a second default database."""
127
134
 
128
135
  def __init__(self) -> None:
129
136
  super().__init__(
130
- f"`{DEFAULT_ALIAS}` is the registry itself. Configure it with "
131
- "`configure()`, and register the others under their own names."
137
+ f"This registry already has a `{DEFAULT_ALIAS}` database. "
138
+ "`configure()` builds one and `register()` hands one over, and a "
139
+ "registry keeps whichever came first."
132
140
  )
133
141
 
134
142
 
@@ -172,8 +172,8 @@ def _before_the_test_s_own(item: pytest.Function) -> int:
172
172
  def sqlakit_db(sqlakit_base: Any) -> Any: # noqa: ANN401
173
173
  """Return the database the marked tests run on.
174
174
 
175
- The one the models live on, which is the importable registry unless they
176
- were given a `Database` of their own. Override it for a project with no
175
+ The one the models live on: their registry, which knows every alias, or the
176
+ `Database` they were given in person. Override it for a project with no
177
177
  model layer.
178
178
  """
179
179
  if sqlakit_base is None:
@@ -185,6 +185,15 @@ def sqlakit_db(sqlakit_base: Any) -> Any: # noqa: ANN401
185
185
  pytrace=False,
186
186
  )
187
187
  return importable_db
188
+ registry = getattr(sqlakit_base, "dbs", None)
189
+ if (
190
+ isinstance(getattr(sqlakit_base, "__db__", None), str)
191
+ and registry is not None
192
+ and registry.is_configured
193
+ ):
194
+ # The models look their aliases up there, so the schema and the
195
+ # transactions cover every one of them, not the default alone.
196
+ return registry
188
197
  database = getattr(sqlakit_base, "db", None)
189
198
  if database is None:
190
199
  pytest.fail(
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