sqlakit 0.22.8__tar.gz → 0.22.10__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.22.8 → sqlakit-0.22.10}/PKG-INFO +1 -1
- {sqlakit-0.22.8 → sqlakit-0.22.10}/pyproject.toml +1 -1
- {sqlakit-0.22.8 → sqlakit-0.22.10}/pyproject.toml.orig +1 -1
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/__init__.py +3 -1
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_base.py +99 -24
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_db.py +27 -0
- sqlakit-0.22.10/sqlakit/_registry.py +215 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_sql.py +4 -1
- sqlakit-0.22.10/sqlakit/asyncio/__init__.py +12 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/asyncio/_db.py +27 -0
- sqlakit-0.22.10/sqlakit/asyncio/_registry.py +189 -0
- sqlakit-0.22.8/sqlakit/_registry.py +0 -71
- sqlakit-0.22.8/sqlakit/asyncio/__init__.py +0 -4
- sqlakit-0.22.8/sqlakit/asyncio/_registry.py +0 -59
- {sqlakit-0.22.8 → sqlakit-0.22.10}/LICENSE +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/README.md +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_cli.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_linters.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_model.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_project.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_pycharm.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_query.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_recording.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_render.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_routing.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_sql_formatter.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/_static.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/orm.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/py.typed +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/sql.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/testing.py +0 -0
- {sqlakit-0.22.8 → sqlakit-0.22.10}/sqlakit/types.py +0 -0
|
@@ -10,7 +10,7 @@ from ._query import (
|
|
|
10
10
|
orderable_columns,
|
|
11
11
|
)
|
|
12
12
|
from ._recording import Recording, Statement
|
|
13
|
-
from ._registry import Databases, db
|
|
13
|
+
from ._registry import Databases, autocommit, db, transaction
|
|
14
14
|
from ._routing import Router
|
|
15
15
|
from .exceptions import (
|
|
16
16
|
DEFAULT_ALIAS,
|
|
@@ -142,8 +142,10 @@ __all__ = [
|
|
|
142
142
|
"UnregisteredDatabaseError",
|
|
143
143
|
"UrlParts",
|
|
144
144
|
"ValidationArgs",
|
|
145
|
+
"autocommit",
|
|
145
146
|
"db",
|
|
146
147
|
"import_models",
|
|
147
148
|
"import_string",
|
|
148
149
|
"orderable_columns",
|
|
150
|
+
"transaction",
|
|
149
151
|
]
|
|
@@ -617,6 +617,10 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
617
617
|
outer = self._outer_to_join()
|
|
618
618
|
return outer, savepoint or rollback or bool(outer and outer.savepoint)
|
|
619
619
|
|
|
620
|
+
def _stand_in(self) -> Any: # noqa: ANN401
|
|
621
|
+
"""Return the database to open blocks on instead of this one, or None."""
|
|
622
|
+
return None
|
|
623
|
+
|
|
620
624
|
def _outer_to_join(self) -> _Outer[ConnectionT] | None:
|
|
621
625
|
"""Return the outer transaction new blocks join, if there is one."""
|
|
622
626
|
outer = self._outer.get(None)
|
|
@@ -811,15 +815,19 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
811
815
|
"""Leave everything to [`configure`][sqlakit.Databases.configure]."""
|
|
812
816
|
self._default: DatabaseT | None = None
|
|
813
817
|
self._aliased: dict[str, DatabaseT] = {}
|
|
818
|
+
self._overridden: dict[str, DatabaseT] = {}
|
|
814
819
|
self._routers: tuple[Any, ...] = ()
|
|
815
820
|
self._using: ContextVar[str | None] = ContextVar(
|
|
816
821
|
f"{type(self).__name__}.using", default=None
|
|
817
822
|
)
|
|
818
823
|
|
|
819
824
|
def __repr__(self) -> str:
|
|
820
|
-
if
|
|
825
|
+
if self._built_its_own:
|
|
826
|
+
return super().__repr__()
|
|
827
|
+
held = self._stand_in()
|
|
828
|
+
if held is None:
|
|
821
829
|
return f"{type(self).__name__}(unconfigured)"
|
|
822
|
-
return
|
|
830
|
+
return f"{type(self).__name__}({held!r})"
|
|
823
831
|
|
|
824
832
|
def __getitem__(self, alias: str) -> Self | DatabaseT:
|
|
825
833
|
"""Return the database configured as ``alias``.
|
|
@@ -831,6 +839,8 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
831
839
|
UnknownDatabaseError: if nothing is configured under that alias.
|
|
832
840
|
|
|
833
841
|
"""
|
|
842
|
+
if alias in self._overridden:
|
|
843
|
+
return self._overridden[alias]
|
|
834
844
|
if alias == DEFAULT_ALIAS:
|
|
835
845
|
return self if self._default is None else self._default
|
|
836
846
|
try:
|
|
@@ -839,7 +849,11 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
839
849
|
raise UnknownDatabaseError(alias, self.aliases) from None
|
|
840
850
|
|
|
841
851
|
def __contains__(self, alias: str) -> bool:
|
|
842
|
-
return
|
|
852
|
+
return (
|
|
853
|
+
alias == DEFAULT_ALIAS
|
|
854
|
+
or alias in self._aliased
|
|
855
|
+
or alias in self._overridden
|
|
856
|
+
)
|
|
843
857
|
|
|
844
858
|
def register(self, alias: str, db: DatabaseT) -> None:
|
|
845
859
|
"""Put a database already built under an alias.
|
|
@@ -961,6 +975,41 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
961
975
|
db._name = alias # noqa: SLF001
|
|
962
976
|
return db
|
|
963
977
|
|
|
978
|
+
@contextmanager
|
|
979
|
+
def override(
|
|
980
|
+
self,
|
|
981
|
+
db: DatabaseT,
|
|
982
|
+
*,
|
|
983
|
+
alias: str = DEFAULT_ALIAS,
|
|
984
|
+
) -> Iterator[DatabaseT]:
|
|
985
|
+
"""Use another database under an alias until the block ends.
|
|
986
|
+
|
|
987
|
+
```python
|
|
988
|
+
with Database(TEST_URL) as test_db, db.override(test_db):
|
|
989
|
+
create_user("ada") # runs on test_db
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
Every thread sees it. The database stays open when the block ends, so
|
|
993
|
+
close it yourself.
|
|
994
|
+
"""
|
|
995
|
+
before = self._overridden.get(alias)
|
|
996
|
+
named = getattr(db, "_name", None)
|
|
997
|
+
self._overridden[alias] = self._named(alias, db)
|
|
998
|
+
try:
|
|
999
|
+
yield db
|
|
1000
|
+
finally:
|
|
1001
|
+
if before is None:
|
|
1002
|
+
del self._overridden[alias]
|
|
1003
|
+
else:
|
|
1004
|
+
self._overridden[alias] = before
|
|
1005
|
+
if named is not None:
|
|
1006
|
+
db._name = named # noqa: SLF001
|
|
1007
|
+
|
|
1008
|
+
def _stand_in(self) -> DatabaseT | None:
|
|
1009
|
+
"""Return the database the default alias leads to, if not this registry."""
|
|
1010
|
+
held = self._overridden.get(DEFAULT_ALIAS, self._default)
|
|
1011
|
+
return None if held is self else held
|
|
1012
|
+
|
|
964
1013
|
def using(self, target: str | DatabaseT) -> _Using:
|
|
965
1014
|
"""Return that database, standing in for the default one.
|
|
966
1015
|
|
|
@@ -997,6 +1046,16 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
997
1046
|
UnregisteredDatabaseError: if it holds it under none.
|
|
998
1047
|
|
|
999
1048
|
"""
|
|
1049
|
+
alias = self._held_as(db)
|
|
1050
|
+
if alias is None:
|
|
1051
|
+
raise UnregisteredDatabaseError(self.aliases)
|
|
1052
|
+
return alias
|
|
1053
|
+
|
|
1054
|
+
def _held_as(self, db: object) -> str | None:
|
|
1055
|
+
"""Return the alias this registry holds a database under, or None."""
|
|
1056
|
+
for alias, held in self._overridden.items():
|
|
1057
|
+
if held is db:
|
|
1058
|
+
return alias
|
|
1000
1059
|
# A configured registry is the default database itself, and a
|
|
1001
1060
|
# registered one holds it.
|
|
1002
1061
|
if db is self or db is self._default:
|
|
@@ -1004,7 +1063,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1004
1063
|
for alias, held in self._aliased.items():
|
|
1005
1064
|
if held is db:
|
|
1006
1065
|
return alias
|
|
1007
|
-
|
|
1066
|
+
return None
|
|
1008
1067
|
|
|
1009
1068
|
def route(self, *routers: Router | RouterFunction | str) -> None:
|
|
1010
1069
|
"""Say which database a model lives on, for models that do not say it.
|
|
@@ -1038,16 +1097,17 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1038
1097
|
opened with `using()` stands in for the default database.
|
|
1039
1098
|
"""
|
|
1040
1099
|
placement = self._routed(model) or model.__db__
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1100
|
+
if not isinstance(placement, str):
|
|
1101
|
+
# A model pinned to a database this registry holds follows the
|
|
1102
|
+
# alias it is held under, `using()` and `override()` alike.
|
|
1103
|
+
alias = self._held_as(placement)
|
|
1104
|
+
if alias is None:
|
|
1105
|
+
return placement
|
|
1106
|
+
placement = alias
|
|
1107
|
+
redirected = self._using.get()
|
|
1108
|
+
if redirected is not None and placement == DEFAULT_ALIAS:
|
|
1109
|
+
placement = redirected
|
|
1110
|
+
return self[placement]
|
|
1051
1111
|
|
|
1052
1112
|
def _routed(self, model: type[Any]) -> str | None:
|
|
1053
1113
|
"""Return what the first router says about this model, if anything."""
|
|
@@ -1059,13 +1119,18 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1059
1119
|
|
|
1060
1120
|
@property
|
|
1061
1121
|
def aliases(self) -> tuple[str, ...]:
|
|
1062
|
-
"""The aliases configured, the default one first."""
|
|
1063
|
-
|
|
1122
|
+
"""The aliases configured or overridden, the default one first."""
|
|
1123
|
+
overridden = (
|
|
1124
|
+
alias
|
|
1125
|
+
for alias in self._overridden
|
|
1126
|
+
if alias != DEFAULT_ALIAS and alias not in self._aliased
|
|
1127
|
+
)
|
|
1128
|
+
return (DEFAULT_ALIAS, *self._aliased, *overridden)
|
|
1064
1129
|
|
|
1065
1130
|
@property
|
|
1066
1131
|
def is_configured(self) -> bool:
|
|
1067
1132
|
"""Whether this registry has a default database to reach."""
|
|
1068
|
-
return "url" in self.__dict__ or self.
|
|
1133
|
+
return "url" in self.__dict__ or self._stand_in() is not None
|
|
1069
1134
|
|
|
1070
1135
|
@property
|
|
1071
1136
|
def _built_its_own(self) -> bool:
|
|
@@ -1078,12 +1143,13 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1078
1143
|
def _proxy(name: str, *, attribute: bool = False) -> Any: # noqa: ANN401, N805
|
|
1079
1144
|
"""Proxy to the database this registry holds, or call its own.
|
|
1080
1145
|
|
|
1081
|
-
A registry handed a default
|
|
1082
|
-
`configure` built calls what it inherits,
|
|
1146
|
+
A registry handed a default, or overridden for a block, proxies to
|
|
1147
|
+
that database. One that `configure` built calls what it inherits,
|
|
1148
|
+
which `super()` reaches.
|
|
1083
1149
|
"""
|
|
1084
1150
|
|
|
1085
1151
|
def reach(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
1086
|
-
held = self.
|
|
1152
|
+
held = self._stand_in()
|
|
1087
1153
|
found = (
|
|
1088
1154
|
getattr(held, name) if held is not None else getattr(super(), name)
|
|
1089
1155
|
)
|
|
@@ -1096,16 +1162,14 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1096
1162
|
session = _proxy("session", attribute=True)
|
|
1097
1163
|
sql = _proxy("sql", attribute=True)
|
|
1098
1164
|
assert_queries = _proxy("assert_queries")
|
|
1099
|
-
autocommit = _proxy("autocommit")
|
|
1100
|
-
connect = _proxy("connect")
|
|
1101
1165
|
in_session = _proxy("in_session")
|
|
1102
1166
|
in_transaction = _proxy("in_transaction")
|
|
1103
1167
|
ping = _proxy("ping")
|
|
1104
1168
|
provisioned_tables = _proxy("provisioned_tables")
|
|
1105
1169
|
query = _proxy("query")
|
|
1106
|
-
session_factory = _proxy("session_factory")
|
|
1107
|
-
transaction = _proxy("transaction")
|
|
1108
1170
|
del _proxy
|
|
1171
|
+
# `connect`, `autocommit`, `session_factory` and `transaction` are not
|
|
1172
|
+
# proxied: the ones inherited look `_stand_in` up as the block opens.
|
|
1109
1173
|
|
|
1110
1174
|
@overload
|
|
1111
1175
|
def configure(
|
|
@@ -1375,6 +1439,17 @@ class BaseRetryingTransaction:
|
|
|
1375
1439
|
raise AssertionError # pragma: no cover
|
|
1376
1440
|
|
|
1377
1441
|
|
|
1442
|
+
def late_bound(source: object, registry: Any) -> Any: # noqa: ANN401
|
|
1443
|
+
"""Return the database ``using=`` points at: the registry, an alias, or a call."""
|
|
1444
|
+
if source is None:
|
|
1445
|
+
return registry
|
|
1446
|
+
if isinstance(source, str):
|
|
1447
|
+
return registry[source]
|
|
1448
|
+
if callable(source):
|
|
1449
|
+
return source()
|
|
1450
|
+
return source
|
|
1451
|
+
|
|
1452
|
+
|
|
1378
1453
|
def retry_matches(exc: BaseException, retry_on: RetryOn) -> bool:
|
|
1379
1454
|
"""Whether ``retry_on`` claims this exception is worth another attempt."""
|
|
1380
1455
|
if isinstance(retry_on, type | tuple):
|
|
@@ -149,6 +149,11 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
149
149
|
@contextmanager
|
|
150
150
|
def connect(self) -> Iterator[sa.Connection]:
|
|
151
151
|
"""Open a connection and bind it, or reuse the one already bound."""
|
|
152
|
+
elsewhere = self._stand_in()
|
|
153
|
+
if elsewhere is not None:
|
|
154
|
+
with elsewhere.connect() as connection:
|
|
155
|
+
yield connection
|
|
156
|
+
return
|
|
152
157
|
reuse = self._scope_to_reuse()
|
|
153
158
|
if reuse is not None:
|
|
154
159
|
with self._bound(self._reused(reuse)) as connection:
|
|
@@ -191,6 +196,11 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
191
196
|
@contextmanager
|
|
192
197
|
def _autocommit(self) -> Iterator[sa.Connection]:
|
|
193
198
|
"""Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
|
|
199
|
+
elsewhere = self._stand_in()
|
|
200
|
+
if elsewhere is not None:
|
|
201
|
+
with elsewhere.autocommit() as connection:
|
|
202
|
+
yield connection
|
|
203
|
+
return
|
|
194
204
|
outer = self._connection_to_join()
|
|
195
205
|
if outer is not None:
|
|
196
206
|
with self._bound(outer, commit=True) as connection:
|
|
@@ -321,6 +331,11 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
321
331
|
flush, as ``sessionmaker()`` does it. Inside another block it runs on
|
|
322
332
|
the connection already bound.
|
|
323
333
|
"""
|
|
334
|
+
elsewhere = self._stand_in()
|
|
335
|
+
if elsewhere is not None:
|
|
336
|
+
with elsewhere.session_factory() as session:
|
|
337
|
+
yield session
|
|
338
|
+
return
|
|
324
339
|
reuse = self._scope_to_reuse()
|
|
325
340
|
if reuse is not None:
|
|
326
341
|
with self._bound(self._reused(reuse)):
|
|
@@ -472,6 +487,18 @@ class Transaction(ContextDecorator, AbstractContextManager["sa.Connection"]):
|
|
|
472
487
|
|
|
473
488
|
def __enter__(self) -> sa.Connection:
|
|
474
489
|
stack = ExitStack()
|
|
490
|
+
elsewhere = self.db._stand_in() # noqa: SLF001
|
|
491
|
+
if elsewhere is not None:
|
|
492
|
+
connection = stack.enter_context(
|
|
493
|
+
elsewhere.transaction(
|
|
494
|
+
savepoint=self.savepoint,
|
|
495
|
+
join_nested=self.join_nested,
|
|
496
|
+
rollback=self.rollback,
|
|
497
|
+
commit_on_error=self.commit_on_error,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
self._stacks.append(stack)
|
|
501
|
+
return connection
|
|
475
502
|
try:
|
|
476
503
|
outer, savepoint = self.db._plan( # noqa: SLF001
|
|
477
504
|
savepoint=self.savepoint,
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from contextlib import ExitStack, contextmanager
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
|
|
6
|
+
|
|
7
|
+
from ._base import _DatabaseRegistryMixin, default_backoff, late_bound
|
|
8
|
+
from ._db import Database, RetryingTransaction
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable, Iterator
|
|
12
|
+
|
|
13
|
+
from ._base import RetryOn
|
|
14
|
+
from ._db import Transaction
|
|
15
|
+
|
|
16
|
+
__all__ = ["Databases", "autocommit", "db", "transaction"]
|
|
17
|
+
|
|
18
|
+
_FuncT = TypeVar("_FuncT", bound="Callable[..., Any]")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
22
|
+
"""The databases an application talks to, and the default one among them.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from sqlakit import db
|
|
26
|
+
|
|
27
|
+
db.configure(DB_URL)
|
|
28
|
+
|
|
29
|
+
db.session # the default database
|
|
30
|
+
db["replica"].session # another, if one was configured
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use it when one database is enough and passing a handle around is not worth
|
|
34
|
+
it. `Database(url)` is the alternative.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_database_class = Database
|
|
38
|
+
|
|
39
|
+
@contextmanager
|
|
40
|
+
def transactions(
|
|
41
|
+
self,
|
|
42
|
+
**arguments: Any, # noqa: ANN401
|
|
43
|
+
) -> Iterator[None]:
|
|
44
|
+
"""Open a transaction on every database, not the default one alone.
|
|
45
|
+
|
|
46
|
+
A single database needs `transaction(rollback=True)`. A test harness
|
|
47
|
+
with several needs this:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
with db.transactions(rollback=True):
|
|
51
|
+
yield
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
with ExitStack() as stack:
|
|
55
|
+
for alias in self.aliases:
|
|
56
|
+
stack.enter_context(self[alias].transaction(**arguments))
|
|
57
|
+
yield
|
|
58
|
+
|
|
59
|
+
def dispose(self, *, close: bool = True) -> None:
|
|
60
|
+
"""Dispose of every configured database, not just the default one."""
|
|
61
|
+
if self._built_its_own:
|
|
62
|
+
super().dispose(close=close)
|
|
63
|
+
elif self._default is not None:
|
|
64
|
+
self._default.dispose(close=close)
|
|
65
|
+
for db in self._aliased.values():
|
|
66
|
+
db.dispose(close=close)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
db = Databases()
|
|
70
|
+
"""The importable registry: one [`Databases`][sqlakit.Databases] for the process.
|
|
71
|
+
|
|
72
|
+
`db.configure(url)` fills it, and every module then reaches the same connections
|
|
73
|
+
by importing it. Reconfiguring is allowed until something connects, after which
|
|
74
|
+
it raises
|
|
75
|
+
[`DatabaseAlreadyConfiguredError`][sqlakit.DatabaseAlreadyConfiguredError]
|
|
76
|
+
and `dispose()` has to come first.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@overload
|
|
81
|
+
def transaction(func: _FuncT, /) -> _FuncT: ...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@overload
|
|
85
|
+
def transaction(
|
|
86
|
+
*,
|
|
87
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
88
|
+
savepoint: bool = False,
|
|
89
|
+
join_nested: bool = True,
|
|
90
|
+
rollback: bool = False,
|
|
91
|
+
commit_on_error: type[BaseException]
|
|
92
|
+
| tuple[type[BaseException], ...]
|
|
93
|
+
| None = None,
|
|
94
|
+
retry_on: None = None,
|
|
95
|
+
max_retries: int = 3,
|
|
96
|
+
backoff: Callable[[int], float] = default_backoff,
|
|
97
|
+
) -> Callable[[_FuncT], _FuncT]: ...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@overload
|
|
101
|
+
def transaction(
|
|
102
|
+
*,
|
|
103
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
104
|
+
savepoint: bool = False,
|
|
105
|
+
join_nested: bool = True,
|
|
106
|
+
rollback: bool = False,
|
|
107
|
+
commit_on_error: type[BaseException]
|
|
108
|
+
| tuple[type[BaseException], ...]
|
|
109
|
+
| None = None,
|
|
110
|
+
retry_on: RetryOn,
|
|
111
|
+
max_retries: int = 3,
|
|
112
|
+
backoff: Callable[[int], float] = default_backoff,
|
|
113
|
+
) -> RetryingTransaction: ...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def transaction( # noqa: PLR0913 (all keyword-only, as on `Database`)
|
|
117
|
+
func: _FuncT | None = None,
|
|
118
|
+
/,
|
|
119
|
+
*,
|
|
120
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
121
|
+
savepoint: bool = False,
|
|
122
|
+
join_nested: bool = True,
|
|
123
|
+
rollback: bool = False,
|
|
124
|
+
commit_on_error: type[BaseException]
|
|
125
|
+
| tuple[type[BaseException], ...]
|
|
126
|
+
| None = None,
|
|
127
|
+
retry_on: RetryOn | None = None,
|
|
128
|
+
max_retries: int = 3,
|
|
129
|
+
backoff: Callable[[int], float] = default_backoff,
|
|
130
|
+
) -> _FuncT | Callable[[_FuncT], _FuncT] | RetryingTransaction:
|
|
131
|
+
"""Like `Database.transaction`, but find the database on every call.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
@transaction
|
|
135
|
+
def import_users() -> None: ...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@transaction(using="replica")
|
|
139
|
+
def rebuild_report() -> None: ...
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@transaction(using=lambda: container.resolve(Database))
|
|
143
|
+
def create_user(name: str) -> None: ...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
``using`` is an alias in [`sqlakit.db`][sqlakit.db], or a function that
|
|
147
|
+
returns the database. Without it, the default database of `sqlakit.db`.
|
|
148
|
+
The other arguments are those of `Database.transaction`.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def opened() -> Transaction:
|
|
152
|
+
found: Database = late_bound(using, db)
|
|
153
|
+
return found.transaction(
|
|
154
|
+
savepoint=savepoint,
|
|
155
|
+
join_nested=join_nested,
|
|
156
|
+
rollback=rollback,
|
|
157
|
+
commit_on_error=commit_on_error,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
decorate: Callable[[_FuncT], _FuncT] | RetryingTransaction = (
|
|
161
|
+
_opening(opened)
|
|
162
|
+
if retry_on is None
|
|
163
|
+
else RetryingTransaction(
|
|
164
|
+
opened, retry_on=retry_on, max_retries=max_retries, backoff=backoff
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
if func is not None:
|
|
168
|
+
return decorate(func)
|
|
169
|
+
return decorate
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@overload
|
|
173
|
+
def autocommit(func: _FuncT, /) -> _FuncT: ...
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@overload
|
|
177
|
+
def autocommit(
|
|
178
|
+
*,
|
|
179
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
180
|
+
) -> Callable[[_FuncT], _FuncT]: ...
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def autocommit(
|
|
184
|
+
func: _FuncT | None = None,
|
|
185
|
+
/,
|
|
186
|
+
*,
|
|
187
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
188
|
+
) -> _FuncT | Callable[[_FuncT], _FuncT]:
|
|
189
|
+
"""Like `Database.autocommit`, but find the database on every call.
|
|
190
|
+
|
|
191
|
+
``using`` works as in [`transaction`][sqlakit.transaction].
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def opened() -> Any: # noqa: ANN401
|
|
195
|
+
found: Database = late_bound(using, db)
|
|
196
|
+
return found.autocommit()
|
|
197
|
+
|
|
198
|
+
decorate: Callable[[_FuncT], _FuncT] = _opening(opened)
|
|
199
|
+
if func is not None:
|
|
200
|
+
return decorate(func)
|
|
201
|
+
return decorate
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _opening(opened: Callable[[], Any]) -> Callable[[_FuncT], _FuncT]:
|
|
205
|
+
"""Return a decorator that runs the function inside `opened()`."""
|
|
206
|
+
|
|
207
|
+
def decorate(func: _FuncT) -> _FuncT:
|
|
208
|
+
@functools.wraps(func)
|
|
209
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
210
|
+
with opened():
|
|
211
|
+
return func(*args, **kwargs)
|
|
212
|
+
|
|
213
|
+
return cast("_FuncT", wrapper)
|
|
214
|
+
|
|
215
|
+
return decorate
|
|
@@ -613,6 +613,9 @@ def sql_macros(path: Path | str, source: str | None = None) -> list[SqlMacro]:
|
|
|
613
613
|
problem = f"its arguments in {path.name}:{line} name one twice: {params}"
|
|
614
614
|
raise MacroDefinitionError(name, problem, line=line)
|
|
615
615
|
named = offset + statement.start("name")
|
|
616
|
+
# The body may start below its `SELECT`, where its lines count from.
|
|
617
|
+
body = statement.group("body")
|
|
618
|
+
body_at = offset + statement.start("body") + len(body) - len(body.lstrip())
|
|
616
619
|
found.append(
|
|
617
620
|
SqlMacro(
|
|
618
621
|
name,
|
|
@@ -621,7 +624,7 @@ def sql_macros(path: Path | str, source: str | None = None) -> list[SqlMacro]:
|
|
|
621
624
|
doc=" ".join(one for one in doc if one),
|
|
622
625
|
path=path,
|
|
623
626
|
line=line,
|
|
624
|
-
body_line=
|
|
627
|
+
body_line=source.count("\n", 0, body_at) + 1,
|
|
625
628
|
name_at=(
|
|
626
629
|
source.count("\n", 0, named) + 1,
|
|
627
630
|
named - (source.rfind("\n", 0, named) + 1),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ._db import Database, RetryingTransaction, Transaction
|
|
2
|
+
from ._registry import Databases, autocommit, db, transaction
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Database",
|
|
6
|
+
"Databases",
|
|
7
|
+
"RetryingTransaction",
|
|
8
|
+
"Transaction",
|
|
9
|
+
"autocommit",
|
|
10
|
+
"db",
|
|
11
|
+
"transaction",
|
|
12
|
+
]
|
|
@@ -183,6 +183,11 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
183
183
|
@asynccontextmanager
|
|
184
184
|
async def connect(self) -> AsyncIterator[AsyncConnection]:
|
|
185
185
|
"""Open a connection and bind it, or reuse the one already bound."""
|
|
186
|
+
elsewhere = self._stand_in()
|
|
187
|
+
if elsewhere is not None:
|
|
188
|
+
async with elsewhere.connect() as connection:
|
|
189
|
+
yield connection
|
|
190
|
+
return
|
|
186
191
|
reuse = self._scope_to_reuse()
|
|
187
192
|
if reuse is not None:
|
|
188
193
|
async with self._bound(await self._areused(reuse)) as connection:
|
|
@@ -225,6 +230,11 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
225
230
|
@asynccontextmanager
|
|
226
231
|
async def _autocommit(self) -> AsyncIterator[AsyncConnection]:
|
|
227
232
|
"""Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
|
|
233
|
+
elsewhere = self._stand_in()
|
|
234
|
+
if elsewhere is not None:
|
|
235
|
+
async with elsewhere.autocommit() as connection:
|
|
236
|
+
yield connection
|
|
237
|
+
return
|
|
228
238
|
outer = self._connection_to_join()
|
|
229
239
|
if outer is not None:
|
|
230
240
|
async with self._bound(outer, commit=True) as connection:
|
|
@@ -355,6 +365,11 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
355
365
|
flush, as ``async_sessionmaker()`` does it. Inside another block it
|
|
356
366
|
runs on the connection already bound.
|
|
357
367
|
"""
|
|
368
|
+
elsewhere = self._stand_in()
|
|
369
|
+
if elsewhere is not None:
|
|
370
|
+
async with elsewhere.session_factory() as session:
|
|
371
|
+
yield session
|
|
372
|
+
return
|
|
358
373
|
reuse = self._scope_to_reuse()
|
|
359
374
|
if reuse is not None:
|
|
360
375
|
async with self._bound(await self._areused(reuse)):
|
|
@@ -512,6 +527,18 @@ class Transaction(
|
|
|
512
527
|
|
|
513
528
|
async def __aenter__(self) -> AsyncConnection:
|
|
514
529
|
stack = AsyncExitStack()
|
|
530
|
+
elsewhere = self.db._stand_in() # noqa: SLF001
|
|
531
|
+
if elsewhere is not None:
|
|
532
|
+
connection = await stack.enter_async_context(
|
|
533
|
+
elsewhere.transaction(
|
|
534
|
+
savepoint=self.savepoint,
|
|
535
|
+
join_nested=self.join_nested,
|
|
536
|
+
rollback=self.rollback,
|
|
537
|
+
commit_on_error=self.commit_on_error,
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
self._stacks.append(stack)
|
|
541
|
+
return connection
|
|
515
542
|
try:
|
|
516
543
|
outer, savepoint = self.db._plan( # noqa: SLF001
|
|
517
544
|
savepoint=self.savepoint,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
|
|
6
|
+
|
|
7
|
+
from sqlakit._base import _DatabaseRegistryMixin, default_backoff, late_bound
|
|
8
|
+
|
|
9
|
+
from ._db import Database, RetryingTransaction
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import AsyncIterator, Callable
|
|
13
|
+
|
|
14
|
+
from sqlakit._base import RetryOn
|
|
15
|
+
|
|
16
|
+
from ._db import Transaction
|
|
17
|
+
|
|
18
|
+
__all__ = ["Databases", "autocommit", "db", "transaction"]
|
|
19
|
+
|
|
20
|
+
_FuncT = TypeVar("_FuncT", bound="Callable[..., Any]")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
24
|
+
"""The databases an application talks to, and the default one among them.
|
|
25
|
+
|
|
26
|
+
The asyncio counterpart of [`sqlakit.Databases`][sqlakit.Databases].
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from sqlakit.asyncio import db
|
|
30
|
+
|
|
31
|
+
db.configure(DB_URL)
|
|
32
|
+
|
|
33
|
+
db.session # the default database
|
|
34
|
+
db["replica"].session # another, if one was configured
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_database_class = Database
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def transactions(
|
|
42
|
+
self,
|
|
43
|
+
**arguments: Any, # noqa: ANN401
|
|
44
|
+
) -> AsyncIterator[None]:
|
|
45
|
+
"""Open a transaction on every database, not the default one alone.
|
|
46
|
+
|
|
47
|
+
A test harness with more than one database needs this, where a single
|
|
48
|
+
database needs `transaction(rollback=True)`.
|
|
49
|
+
"""
|
|
50
|
+
async with AsyncExitStack() as stack:
|
|
51
|
+
for alias in self.aliases:
|
|
52
|
+
await stack.enter_async_context(self[alias].transaction(**arguments))
|
|
53
|
+
yield
|
|
54
|
+
|
|
55
|
+
async def dispose(self, *, close: bool = True) -> None:
|
|
56
|
+
"""Dispose of every configured database, not just the default one."""
|
|
57
|
+
if self._built_its_own:
|
|
58
|
+
await super().dispose(close=close)
|
|
59
|
+
elif self._default is not None:
|
|
60
|
+
await self._default.dispose(close=close)
|
|
61
|
+
for db in self._aliased.values():
|
|
62
|
+
await db.dispose(close=close)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
db = Databases()
|
|
66
|
+
"""The database an application talks to. Configure it once, use it anywhere."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def transaction(func: _FuncT, /) -> _FuncT: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
def transaction(
|
|
75
|
+
*,
|
|
76
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
77
|
+
savepoint: bool = False,
|
|
78
|
+
join_nested: bool = True,
|
|
79
|
+
rollback: bool = False,
|
|
80
|
+
commit_on_error: type[BaseException]
|
|
81
|
+
| tuple[type[BaseException], ...]
|
|
82
|
+
| None = None,
|
|
83
|
+
retry_on: None = None,
|
|
84
|
+
max_retries: int = 3,
|
|
85
|
+
backoff: Callable[[int], float] = default_backoff,
|
|
86
|
+
) -> Callable[[_FuncT], _FuncT]: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@overload
|
|
90
|
+
def transaction(
|
|
91
|
+
*,
|
|
92
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
93
|
+
savepoint: bool = False,
|
|
94
|
+
join_nested: bool = True,
|
|
95
|
+
rollback: bool = False,
|
|
96
|
+
commit_on_error: type[BaseException]
|
|
97
|
+
| tuple[type[BaseException], ...]
|
|
98
|
+
| None = None,
|
|
99
|
+
retry_on: RetryOn,
|
|
100
|
+
max_retries: int = 3,
|
|
101
|
+
backoff: Callable[[int], float] = default_backoff,
|
|
102
|
+
) -> RetryingTransaction: ...
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def transaction( # noqa: PLR0913 (all keyword-only, as on `Database`)
|
|
106
|
+
func: _FuncT | None = None,
|
|
107
|
+
/,
|
|
108
|
+
*,
|
|
109
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
110
|
+
savepoint: bool = False,
|
|
111
|
+
join_nested: bool = True,
|
|
112
|
+
rollback: bool = False,
|
|
113
|
+
commit_on_error: type[BaseException]
|
|
114
|
+
| tuple[type[BaseException], ...]
|
|
115
|
+
| None = None,
|
|
116
|
+
retry_on: RetryOn | None = None,
|
|
117
|
+
max_retries: int = 3,
|
|
118
|
+
backoff: Callable[[int], float] = default_backoff,
|
|
119
|
+
) -> _FuncT | Callable[[_FuncT], _FuncT] | RetryingTransaction:
|
|
120
|
+
"""Like `Database.transaction`, but find the database on every call.
|
|
121
|
+
|
|
122
|
+
The asyncio version of [`sqlakit.transaction`][sqlakit.transaction].
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def opened() -> Transaction:
|
|
126
|
+
found: Database = late_bound(using, db)
|
|
127
|
+
return found.transaction(
|
|
128
|
+
savepoint=savepoint,
|
|
129
|
+
join_nested=join_nested,
|
|
130
|
+
rollback=rollback,
|
|
131
|
+
commit_on_error=commit_on_error,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
decorate: Callable[[_FuncT], _FuncT] | RetryingTransaction = (
|
|
135
|
+
_opening(opened)
|
|
136
|
+
if retry_on is None
|
|
137
|
+
else RetryingTransaction(
|
|
138
|
+
opened, retry_on=retry_on, max_retries=max_retries, backoff=backoff
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
if func is not None:
|
|
142
|
+
return decorate(func)
|
|
143
|
+
return decorate
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@overload
|
|
147
|
+
def autocommit(func: _FuncT, /) -> _FuncT: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@overload
|
|
151
|
+
def autocommit(
|
|
152
|
+
*,
|
|
153
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
154
|
+
) -> Callable[[_FuncT], _FuncT]: ...
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def autocommit(
|
|
158
|
+
func: _FuncT | None = None,
|
|
159
|
+
/,
|
|
160
|
+
*,
|
|
161
|
+
using: str | Database | Callable[[], Database] | None = None,
|
|
162
|
+
) -> _FuncT | Callable[[_FuncT], _FuncT]:
|
|
163
|
+
"""Like `Database.autocommit`, but find the database on every call.
|
|
164
|
+
|
|
165
|
+
The asyncio version of [`sqlakit.autocommit`][sqlakit.autocommit].
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def opened() -> Any: # noqa: ANN401
|
|
169
|
+
found: Database = late_bound(using, db)
|
|
170
|
+
return found.autocommit()
|
|
171
|
+
|
|
172
|
+
decorate: Callable[[_FuncT], _FuncT] = _opening(opened)
|
|
173
|
+
if func is not None:
|
|
174
|
+
return decorate(func)
|
|
175
|
+
return decorate
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _opening(opened: Callable[[], Any]) -> Callable[[_FuncT], _FuncT]:
|
|
179
|
+
"""Return a decorator that runs the function inside `opened()`."""
|
|
180
|
+
|
|
181
|
+
def decorate(func: _FuncT) -> _FuncT:
|
|
182
|
+
@functools.wraps(func)
|
|
183
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
184
|
+
async with opened():
|
|
185
|
+
return await func(*args, **kwargs)
|
|
186
|
+
|
|
187
|
+
return cast("_FuncT", wrapper)
|
|
188
|
+
|
|
189
|
+
return decorate
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from contextlib import ExitStack, contextmanager
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
-
|
|
6
|
-
from ._base import _DatabaseRegistryMixin
|
|
7
|
-
from ._db import Database
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from collections.abc import Iterator
|
|
11
|
-
|
|
12
|
-
__all__ = ["Databases", "db"]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
16
|
-
"""The databases an application talks to, and the default one among them.
|
|
17
|
-
|
|
18
|
-
```python
|
|
19
|
-
from sqlakit import db
|
|
20
|
-
|
|
21
|
-
db.configure(DB_URL)
|
|
22
|
-
|
|
23
|
-
db.session # the default database
|
|
24
|
-
db["replica"].session # another, if one was configured
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Use it when one database is enough and passing a handle around is not worth
|
|
28
|
-
it. `Database(url)` is the alternative.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
_database_class = Database
|
|
32
|
-
|
|
33
|
-
@contextmanager
|
|
34
|
-
def transactions(
|
|
35
|
-
self,
|
|
36
|
-
**arguments: Any, # noqa: ANN401
|
|
37
|
-
) -> Iterator[None]:
|
|
38
|
-
"""Open a transaction on every database, not the default one alone.
|
|
39
|
-
|
|
40
|
-
A single database needs `transaction(rollback=True)`. A test harness
|
|
41
|
-
with several needs this:
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
with db.transactions(rollback=True):
|
|
45
|
-
yield
|
|
46
|
-
```
|
|
47
|
-
"""
|
|
48
|
-
with ExitStack() as stack:
|
|
49
|
-
for alias in self.aliases:
|
|
50
|
-
stack.enter_context(self[alias].transaction(**arguments))
|
|
51
|
-
yield
|
|
52
|
-
|
|
53
|
-
def dispose(self, *, close: bool = True) -> None:
|
|
54
|
-
"""Dispose of every configured database, not just the default one."""
|
|
55
|
-
if self._built_its_own:
|
|
56
|
-
super().dispose(close=close)
|
|
57
|
-
elif self._default is not None:
|
|
58
|
-
self._default.dispose(close=close)
|
|
59
|
-
for db in self._aliased.values():
|
|
60
|
-
db.dispose(close=close)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
db = Databases()
|
|
64
|
-
"""The importable registry: one [`Databases`][sqlakit.Databases] for the process.
|
|
65
|
-
|
|
66
|
-
`db.configure(url)` fills it, and every module then reaches the same connections
|
|
67
|
-
by importing it. Reconfiguring is allowed until something connects, after which
|
|
68
|
-
it raises
|
|
69
|
-
[`DatabaseAlreadyConfiguredError`][sqlakit.DatabaseAlreadyConfiguredError]
|
|
70
|
-
and `dispose()` has to come first.
|
|
71
|
-
"""
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from contextlib import AsyncExitStack, asynccontextmanager
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
-
|
|
6
|
-
from sqlakit._base import _DatabaseRegistryMixin
|
|
7
|
-
|
|
8
|
-
from ._db import Database
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from collections.abc import AsyncIterator
|
|
12
|
-
|
|
13
|
-
__all__ = ["Databases", "db"]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Databases(_DatabaseRegistryMixin[Database], Database):
|
|
17
|
-
"""The databases an application talks to, and the default one among them.
|
|
18
|
-
|
|
19
|
-
The asyncio counterpart of [`sqlakit.Databases`][sqlakit.Databases].
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
from sqlakit.asyncio import db
|
|
23
|
-
|
|
24
|
-
db.configure(DB_URL)
|
|
25
|
-
|
|
26
|
-
db.session # the default database
|
|
27
|
-
db["replica"].session # another, if one was configured
|
|
28
|
-
```
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
_database_class = Database
|
|
32
|
-
|
|
33
|
-
@asynccontextmanager
|
|
34
|
-
async def transactions(
|
|
35
|
-
self,
|
|
36
|
-
**arguments: Any, # noqa: ANN401
|
|
37
|
-
) -> AsyncIterator[None]:
|
|
38
|
-
"""Open a transaction on every database, not the default one alone.
|
|
39
|
-
|
|
40
|
-
A test harness with more than one database needs this, where a single
|
|
41
|
-
database needs `transaction(rollback=True)`.
|
|
42
|
-
"""
|
|
43
|
-
async with AsyncExitStack() as stack:
|
|
44
|
-
for alias in self.aliases:
|
|
45
|
-
await stack.enter_async_context(self[alias].transaction(**arguments))
|
|
46
|
-
yield
|
|
47
|
-
|
|
48
|
-
async def dispose(self, *, close: bool = True) -> None:
|
|
49
|
-
"""Dispose of every configured database, not just the default one."""
|
|
50
|
-
if self._built_its_own:
|
|
51
|
-
await super().dispose(close=close)
|
|
52
|
-
elif self._default is not None:
|
|
53
|
-
await self._default.dispose(close=close)
|
|
54
|
-
for db in self._aliased.values():
|
|
55
|
-
await db.dispose(close=close)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
db = Databases()
|
|
59
|
-
"""The database an application talks to. Configure it once, use it anywhere."""
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|