sqlakit 0.10.9__tar.gz → 0.11.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.10.9 → sqlakit-0.11.1}/PKG-INFO +2 -1
  2. {sqlakit-0.10.9 → sqlakit-0.11.1}/pyproject.toml +8 -5
  3. {sqlakit-0.10.9 → sqlakit-0.11.1}/pyproject.toml.orig +5 -4
  4. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_base.py +66 -1
  5. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_model.py +1 -1
  6. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/exceptions.py +3 -3
  7. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/pytest_plugin.py +14 -14
  8. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/types.py +11 -5
  9. {sqlakit-0.10.9 → sqlakit-0.11.1}/LICENSE +0 -0
  10. {sqlakit-0.10.9 → sqlakit-0.11.1}/README.md +0 -0
  11. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/__init__.py +0 -0
  12. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_cli.py +0 -0
  13. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_db.py +0 -0
  14. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_debugserver.py +0 -0
  15. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_discovery.py +0 -0
  16. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_query.py +0 -0
  17. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_recording.py +0 -0
  18. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_registry.py +0 -0
  19. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_routing.py +0 -0
  20. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/_sql.py +0 -0
  21. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/asyncio/__init__.py +0 -0
  22. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/asyncio/_db.py +0 -0
  23. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/asyncio/_registry.py +0 -0
  24. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/asyncio/orm.py +0 -0
  25. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/asyncio/sql.py +0 -0
  26. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/debugserver.html +0 -0
  27. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/orm.py +0 -0
  28. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/py.typed +0 -0
  29. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/sql.py +0 -0
  30. {sqlakit-0.10.9 → sqlakit-0.11.1}/sqlakit/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.10.9
3
+ Version: 0.11.1
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -19,6 +19,7 @@ Classifier: Topic :: Database
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Dist: sqlalchemy>=2.0.22
22
+ Requires-Dist: typing-extensions>=4.13
22
23
  Requires-Dist: sqlalchemy[asyncio]>=2.0.22 ; extra == 'asyncio'
23
24
  Requires-Dist: sqlparse>=0.6.0 ; extra == 'debug'
24
25
  Requires-Dist: jinja2sql>=0.11.0 ; extra == 'sql'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.10.9"
3
+ version = "0.11.1"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,7 +26,10 @@ classifiers = [
26
26
  "Typing :: Typed",
27
27
  ]
28
28
  requires-python = ">=3.11"
29
- dependencies = ["sqlalchemy>=2.0.22"]
29
+ dependencies = [
30
+ "sqlalchemy>=2.0.22",
31
+ "typing-extensions>=4.13",
32
+ ]
30
33
 
31
34
  [[project.authors]]
32
35
  name = "Anton Ruhlov"
@@ -180,8 +183,8 @@ skip = "./.venv,./site,./uv.lock,./.git,./debugserver/node_modules,./debugserver
180
183
  [dependency-groups]
181
184
  dev = [
182
185
  "pytest>=9.1.1",
183
- "ty>=0.0.75",
184
- "ruff>=0.16.5",
186
+ "ty>=0.0.79",
187
+ "ruff>=0.16.6",
185
188
  "poethepoet>=0.48.0",
186
189
  "anyio>=4.14.2",
187
190
  "trio>=0.34.0",
@@ -208,5 +211,5 @@ docs = [
208
211
  "mkdocs>=1.6.1,<2",
209
212
  "mkdocs-material>=9.7.7",
210
213
  "mkdocstrings[python]>=1.0.6",
211
- "ruff>=0.16.3",
214
+ "ruff>=0.16.6",
212
215
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.10.9"
3
+ version = "0.11.1"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -23,6 +23,7 @@ classifiers = [
23
23
  requires-python = ">=3.11"
24
24
  dependencies = [
25
25
  "sqlalchemy>=2.0.22",
26
+ "typing-extensions>=4.13",
26
27
  ]
27
28
 
28
29
  [project.scripts]
@@ -60,8 +61,8 @@ default-groups = ["dev", "docs"]
60
61
  [dependency-groups]
61
62
  dev = [
62
63
  "pytest>=9.1.1",
63
- "ty>=0.0.75",
64
- "ruff>=0.16.5",
64
+ "ty>=0.0.79",
65
+ "ruff>=0.16.6",
65
66
  "poethepoet>=0.48.0",
66
67
  "anyio>=4.14.2",
67
68
  "trio>=0.34.0",
@@ -88,7 +89,7 @@ docs = [
88
89
  "mkdocs>=1.6.1,<2",
89
90
  "mkdocs-material>=9.7.7",
90
91
  "mkdocstrings[python]>=1.0.6",
91
- "ruff>=0.16.3",
92
+ "ruff>=0.16.6",
92
93
  ]
93
94
 
94
95
  [tool.pytest.ini_options]
@@ -470,6 +470,32 @@ 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) -> Any: # noqa: ANN401
483
+ """Return this database, under the name it carries.
484
+
485
+ `Any` rather than `Self`, so a registry can narrow it to the databases
486
+ it holds.
487
+
488
+ Raises:
489
+ UnknownDatabaseError: if the alias is another database's.
490
+
491
+ """
492
+ if alias == self._name:
493
+ return self
494
+ raise UnknownDatabaseError(alias, self.aliases)
495
+
496
+ def __contains__(self, alias: str) -> bool:
497
+ return alias == self._name
498
+
473
499
  def in_session(self) -> bool:
474
500
  """Whether a session is open in the current context.
475
501
 
@@ -910,11 +936,15 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
910
936
  opened with `using()` stands in for the default database.
911
937
  """
912
938
  placement = self._routed(model) or model.__db__
939
+ override = self._using.get()
913
940
  if isinstance(placement, str):
914
- override = self._using.get()
915
941
  if override is not None and placement == DEFAULT_ALIAS:
916
942
  placement = override
917
943
  return self[placement]
944
+ # A model pinned to the database itself follows `using()` as one on the
945
+ # default alias does, when that database is the one being stood in for.
946
+ if override is not None and self[DEFAULT_ALIAS] is placement:
947
+ return self[override]
918
948
  return placement
919
949
 
920
950
  def _routed(self, model: type[Any]) -> str | None:
@@ -940,6 +970,41 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
940
970
  """Whether the default database is this registry, `configure` having built it."""
941
971
  return "url" in self.__dict__
942
972
 
973
+ if not TYPE_CHECKING:
974
+ # Hidden from type checkers, which keep reading these off `Database`
975
+ # and its asyncio twin, signatures and all.
976
+ def _proxy(name: str, *, attribute: bool = False) -> Any: # noqa: ANN401, N805
977
+ """Proxy to the database this registry holds, or call its own.
978
+
979
+ A registry handed a default proxies to that database. One that
980
+ `configure` built calls what it inherits, which `super()` reaches.
981
+ """
982
+
983
+ def reach(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
984
+ held = self._default
985
+ found = (
986
+ getattr(held, name) if held is not None else getattr(super(), name)
987
+ )
988
+ return found if attribute else found(*args, **kwargs)
989
+
990
+ return property(reach) if attribute else reach
991
+
992
+ connection = _proxy("connection", attribute=True)
993
+ engine = _proxy("engine", attribute=True)
994
+ session = _proxy("session", attribute=True)
995
+ sql = _proxy("sql", attribute=True)
996
+ assert_queries = _proxy("assert_queries")
997
+ autocommit = _proxy("autocommit")
998
+ connect = _proxy("connect")
999
+ in_session = _proxy("in_session")
1000
+ in_transaction = _proxy("in_transaction")
1001
+ ping = _proxy("ping")
1002
+ provisioned_tables = _proxy("provisioned_tables")
1003
+ query = _proxy("query")
1004
+ session_factory = _proxy("session_factory")
1005
+ transaction = _proxy("transaction")
1006
+ del _proxy
1007
+
943
1008
  @overload
944
1009
  def configure(
945
1010
  self,
@@ -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,11 +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 goes through `transactions`, one alias or many: with one
397
- # it has a database of its own only when `configure` built it.
396
+ # A registry opens every database it holds, and one database opens
397
+ # itself. `transactions` is the one a registry has.
398
398
  return [
399
399
  db.transactions(rollback=True)
400
- if hasattr(db, "aliases")
400
+ if hasattr(db, "transactions")
401
401
  else db.transaction(rollback=True)
402
402
  ]
403
403
  return [
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypedDict
3
+ from typing import TYPE_CHECKING, Any, Literal, TypeAlias
4
+
5
+ from typing_extensions import TypedDict
4
6
 
5
7
  if TYPE_CHECKING:
6
8
  from collections.abc import Callable, Mapping, Sequence
@@ -27,8 +29,11 @@ TemplatesLike: TypeAlias = "str | Path | Sequence[str | Path] | Templates"
27
29
  """Where a database's SQL templates are: a path, several, or the object."""
28
30
 
29
31
 
30
- class EngineArgs(TypedDict, total=False):
31
- """Keyword arguments accepted by [`sqlalchemy.create_engine`](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)."""
32
+ class EngineArgs(TypedDict, total=False, extra_items=Any):
33
+ """Keyword arguments accepted by [`sqlalchemy.create_engine`](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine).
34
+
35
+ The portable ones are listed. A dialect keyword is allowed too.
36
+ """
32
37
 
33
38
  connect_args: dict[str, Any]
34
39
  echo: bool | Literal["debug"]
@@ -61,10 +66,11 @@ class EngineArgs(TypedDict, total=False):
61
66
  use_insertmanyvalues: bool
62
67
 
63
68
 
64
- class SessionArgs(TypedDict, total=False):
69
+ class SessionArgs(TypedDict, total=False, extra_items=Any):
65
70
  """Keyword arguments accepted by `sqlalchemy.orm.sessionmaker`.
66
71
 
67
- No ``bind``: sessions bind to the connection of the surrounding block.
72
+ No ``bind``: sessions bind to the connection of the surrounding block. A
73
+ keyword this list does not have is allowed too.
68
74
  """
69
75
 
70
76
  autobegin: bool
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