sqlakit 0.10.8__tar.gz → 0.11.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.10.8 → sqlakit-0.11.0}/PKG-INFO +1 -1
  2. {sqlakit-0.10.8 → sqlakit-0.11.0}/pyproject.toml +1 -1
  3. {sqlakit-0.10.8 → sqlakit-0.11.0}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_base.py +69 -2
  5. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_model.py +1 -1
  6. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/exceptions.py +3 -3
  7. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/pytest_plugin.py +14 -12
  8. {sqlakit-0.10.8 → sqlakit-0.11.0}/LICENSE +0 -0
  9. {sqlakit-0.10.8 → sqlakit-0.11.0}/README.md +0 -0
  10. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/__init__.py +0 -0
  11. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_cli.py +0 -0
  12. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_db.py +0 -0
  13. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_debugserver.py +0 -0
  14. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_discovery.py +0 -0
  15. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_query.py +0 -0
  16. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_recording.py +0 -0
  17. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_registry.py +0 -0
  18. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_routing.py +0 -0
  19. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/_sql.py +0 -0
  20. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/asyncio/__init__.py +0 -0
  21. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/asyncio/_db.py +0 -0
  22. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/asyncio/_registry.py +0 -0
  23. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/asyncio/orm.py +0 -0
  24. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/asyncio/sql.py +0 -0
  25. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/debugserver.html +0 -0
  26. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/orm.py +0 -0
  27. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/py.typed +0 -0
  28. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/sql.py +0 -0
  29. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/testing.py +0 -0
  30. {sqlakit-0.10.8 → sqlakit-0.11.0}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.10.8
3
+ Version: 0.11.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.10.8"
3
+ version = "0.11.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.10.8"
3
+ version = "0.11.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -470,6 +470,29 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
470
470
  """
471
471
  return self._outer.get(None) is not None
472
472
 
473
+ @property
474
+ def aliases(self) -> tuple[str, ...]:
475
+ """The names this database goes by, which for one database is its own.
476
+
477
+ A registry has one for each database it holds. Both carry this, so code
478
+ that takes either does not have to ask which it was given.
479
+ """
480
+ return (self._name,)
481
+
482
+ def __getitem__(self, alias: str) -> Self:
483
+ """Return this database, under the name it carries.
484
+
485
+ Raises:
486
+ UnknownDatabaseError: if the alias is another database's.
487
+
488
+ """
489
+ if alias == self._name:
490
+ return self
491
+ raise UnknownDatabaseError(alias, self.aliases)
492
+
493
+ def __contains__(self, alias: str) -> bool:
494
+ return alias == self._name
495
+
473
496
  def in_session(self) -> bool:
474
497
  """Whether a session is open in the current context.
475
498
 
@@ -910,11 +933,15 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
910
933
  opened with `using()` stands in for the default database.
911
934
  """
912
935
  placement = self._routed(model) or model.__db__
936
+ override = self._using.get()
913
937
  if isinstance(placement, str):
914
- override = self._using.get()
915
938
  if override is not None and placement == DEFAULT_ALIAS:
916
939
  placement = override
917
940
  return self[placement]
941
+ # A model pinned to the database itself follows `using()` as one on the
942
+ # default alias does, when that database is the one being stood in for.
943
+ if override is not None and self[DEFAULT_ALIAS] is placement:
944
+ return self[override]
918
945
  return placement
919
946
 
920
947
  def _routed(self, model: type[Any]) -> str | None:
@@ -940,6 +967,41 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
940
967
  """Whether the default database is this registry, `configure` having built it."""
941
968
  return "url" in self.__dict__
942
969
 
970
+ if not TYPE_CHECKING:
971
+ # Hidden from type checkers, which keep reading these off `Database`
972
+ # and its asyncio twin, signatures and all.
973
+ def _proxy(name: str, *, attribute: bool = False) -> Any: # noqa: ANN401, N805
974
+ """Proxy to the database this registry holds, or call its own.
975
+
976
+ A registry handed a default proxies to that database. One that
977
+ `configure` built calls what it inherits, which `super()` reaches.
978
+ """
979
+
980
+ def reach(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
981
+ held = self._default
982
+ found = (
983
+ getattr(held, name) if held is not None else getattr(super(), name)
984
+ )
985
+ return found if attribute else found(*args, **kwargs)
986
+
987
+ return property(reach) if attribute else reach
988
+
989
+ connection = _proxy("connection", attribute=True)
990
+ engine = _proxy("engine", attribute=True)
991
+ session = _proxy("session", attribute=True)
992
+ sql = _proxy("sql", attribute=True)
993
+ assert_queries = _proxy("assert_queries")
994
+ autocommit = _proxy("autocommit")
995
+ connect = _proxy("connect")
996
+ in_session = _proxy("in_session")
997
+ in_transaction = _proxy("in_transaction")
998
+ ping = _proxy("ping")
999
+ provisioned_tables = _proxy("provisioned_tables")
1000
+ query = _proxy("query")
1001
+ session_factory = _proxy("session_factory")
1002
+ transaction = _proxy("transaction")
1003
+ del _proxy
1004
+
943
1005
  @overload
944
1006
  def configure(
945
1007
  self,
@@ -1061,10 +1123,15 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1061
1123
  # fails, as it does on a registry with no database of its own, from
1062
1124
  # the outside and from its own methods.
1063
1125
  def __getattr__(self, name: str) -> object:
1126
+ # A dunder is the machinery asking, and a container that patched
1127
+ # `__getattribute__` bounces back here until it is answered.
1128
+ if name.startswith("__") and name.endswith("__"):
1129
+ raise AttributeError(name)
1064
1130
  # Only the database half is worth explaining. Anything else is a
1065
1131
  # name that does not exist, and saying so lets `hasattr`,
1066
1132
  # `copy` and every library that introspects work.
1067
- state = self.__dict__
1133
+ # Through `object`, so a patched `__getattribute__` cannot loop.
1134
+ state = object.__getattribute__(self, "__dict__")
1068
1135
  if "url" in state:
1069
1136
  raise AttributeError(name)
1070
1137
  asked_as_a_database = name in DATABASE_STATE or (
@@ -145,7 +145,7 @@ class BaseModel(Generic[DatabaseT]):
145
145
  Base.register_db(Database(DB_URL))
146
146
  ```
147
147
 
148
- The registry answers for it either way, so `dbs["default"]`,
148
+ The registry holds it either way, so `dbs["default"]`,
149
149
  `dbs.transactions()` and `using()` all reach what was registered.
150
150
  [`set_db`][sqlakit.orm.ModelMixin.set_db] is the other way to name a
151
151
  database, and it leaves the registry out of it.
@@ -86,10 +86,10 @@ class RetryNotSupportedError(SQLAKitError, TypeError):
86
86
 
87
87
 
88
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."
89
+ "This registry did not build the default database, it was registered, so "
90
+ "the settings it was built from live on it. Read them from `db['default']`."
91
91
  )
92
- """What a registry says when it is asked for connections it does not have."""
92
+ """What a registry says when asked for settings the database it holds carries."""
93
93
 
94
94
 
95
95
  class DatabaseNotConfiguredError(SQLAKitError, RuntimeError):
@@ -1,25 +1,23 @@
1
1
  """The `db` marker, and the fixtures behind it.
2
2
 
3
- Installed with the library, so a project writes the two lines that say which
4
- database and which tables, and nothing else:
3
+ Installed with the library, so a project says which models it has and nothing
4
+ else:
5
5
 
6
6
  ```python title="conftest.py"
7
7
  import pytest
8
8
 
9
- from app.db import db
10
9
  from app.models import Model
11
10
 
12
11
 
13
12
  @pytest.fixture(scope="session")
14
- def sqlakit_db() -> Databases:
15
- return db
16
-
17
-
18
- @pytest.fixture(scope="session")
19
- def sqlakit_metadata() -> sa.MetaData:
20
- return Model.metadata
13
+ def sqlakit_base() -> type[Model]:
14
+ return Model
21
15
  ```
22
16
 
17
+ The database comes from the base: the registry its models look their aliases
18
+ up in, or the one they were given in person. A project with no model layer
19
+ defines `sqlakit_db` and `sqlakit_metadata` instead.
20
+
23
21
  A test marked `db` runs in a transaction that rolls back, on every database.
24
22
  `using` narrows that to the ones a test works on:
25
23
 
@@ -361,7 +359,9 @@ def _schema_blocks(
361
359
  database to put it on.
362
360
  """
363
361
  if base is not None:
364
- aliases = getattr(db, "aliases", None) or (None,)
362
+ # A database of its own is where the models are pinned, and the alias
363
+ # a registry knows it by is not how the base reaches it.
364
+ aliases = db.aliases if hasattr(db, "transactions") else (None,)
365
365
  return [base.provisioned_tables(alias) for alias in aliases]
366
366
  if metadata is not None:
367
367
  return [db.provisioned_tables(metadata)]
@@ -393,9 +393,11 @@ def _rolled_back(db: Any, using: tuple[Any, ...]) -> list[Any]: # noqa: ANN401
393
393
  a connection to each in the tests that read one.
394
394
  """
395
395
  if not using:
396
+ # A registry opens every database it holds, and one database opens
397
+ # itself. `transactions` is the one a registry has.
396
398
  return [
397
399
  db.transactions(rollback=True)
398
- if len(getattr(db, "aliases", ()) or ()) > 1
400
+ if hasattr(db, "transactions")
399
401
  else db.transaction(rollback=True)
400
402
  ]
401
403
  return [
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