sqlakit 0.22.9__tar.gz → 0.22.11__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.9 → sqlakit-0.22.11}/PKG-INFO +1 -1
- {sqlakit-0.22.9 → sqlakit-0.22.11}/pyproject.toml +1 -1
- {sqlakit-0.22.9 → sqlakit-0.22.11}/pyproject.toml.orig +1 -1
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/__init__.py +3 -1
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_base.py +145 -50
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_db.py +37 -0
- sqlakit-0.22.11/sqlakit/_registry.py +215 -0
- sqlakit-0.22.11/sqlakit/asyncio/__init__.py +12 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/asyncio/_db.py +39 -0
- sqlakit-0.22.11/sqlakit/asyncio/_registry.py +189 -0
- sqlakit-0.22.9/sqlakit/_registry.py +0 -71
- sqlakit-0.22.9/sqlakit/asyncio/__init__.py +0 -4
- sqlakit-0.22.9/sqlakit/asyncio/_registry.py +0 -59
- {sqlakit-0.22.9 → sqlakit-0.22.11}/LICENSE +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/README.md +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_cli.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_linters.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_model.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_project.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_pycharm.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_query.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_recording.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_render.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_routing.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_sql.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_sql_formatter.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/_static.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/orm.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/py.typed +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/sql.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/sqlakit/testing.py +0 -0
- {sqlakit-0.22.9 → sqlakit-0.22.11}/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
|
]
|
|
@@ -232,6 +232,9 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
232
232
|
"""
|
|
233
233
|
|
|
234
234
|
_engine: Any = None # narrowed by the subclass
|
|
235
|
+
# The database to work on instead of this one, or None. Every public member
|
|
236
|
+
# checks it first, so a database without one pays one attribute read.
|
|
237
|
+
_override: Any = None
|
|
235
238
|
|
|
236
239
|
def __init__(
|
|
237
240
|
self,
|
|
@@ -299,6 +302,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
299
302
|
self._listened: Any = None
|
|
300
303
|
self._listening_lock = threading.Lock()
|
|
301
304
|
self._name = alias or DEFAULT_ALIAS
|
|
305
|
+
self._override = None
|
|
302
306
|
|
|
303
307
|
def __repr__(self) -> str:
|
|
304
308
|
return f"{type(self).__name__}({self.url.render_as_string()!r})"
|
|
@@ -332,6 +336,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
332
336
|
MissingSessionError: if no connection is bound.
|
|
333
337
|
|
|
334
338
|
"""
|
|
339
|
+
if self._override is not None:
|
|
340
|
+
return self._override.session
|
|
335
341
|
try:
|
|
336
342
|
scope = self._scope.get()
|
|
337
343
|
except LookupError:
|
|
@@ -384,6 +390,18 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
384
390
|
after. `with` is right on either side, awaited or not: it listens, it does
|
|
385
391
|
not run anything.
|
|
386
392
|
"""
|
|
393
|
+
if self._override is not None:
|
|
394
|
+
with self._override.recording(
|
|
395
|
+
label,
|
|
396
|
+
logger=logger,
|
|
397
|
+
echo=echo,
|
|
398
|
+
stacks=stacks,
|
|
399
|
+
skip_queries_from=skip_queries_from,
|
|
400
|
+
into=into,
|
|
401
|
+
send_to=send_to,
|
|
402
|
+
) as recording:
|
|
403
|
+
yield recording
|
|
404
|
+
return
|
|
387
405
|
recording = Recording(label=label) if into is None else into
|
|
388
406
|
self._listen()
|
|
389
407
|
recordings = self._recordings.set((*self._recordings.get(), recording))
|
|
@@ -507,6 +525,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
507
525
|
|
|
508
526
|
False under ``connect()`` and ``autocommit()``, which open none.
|
|
509
527
|
"""
|
|
528
|
+
if self._override is not None:
|
|
529
|
+
return self._override.in_transaction()
|
|
510
530
|
return self._outer.get(None) is not None
|
|
511
531
|
|
|
512
532
|
@property
|
|
@@ -542,6 +562,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
542
562
|
this is False in a block that has only run statements on the
|
|
543
563
|
connection. Reading it opens nothing.
|
|
544
564
|
"""
|
|
565
|
+
if self._override is not None:
|
|
566
|
+
return self._override.in_session()
|
|
545
567
|
scope = self._scope.get(None)
|
|
546
568
|
return scope is not None and scope.session is not None
|
|
547
569
|
|
|
@@ -563,6 +585,10 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
563
585
|
A block opened inside still joins the transaction around it, and rolls
|
|
564
586
|
back with it.
|
|
565
587
|
"""
|
|
588
|
+
if self._override is not None:
|
|
589
|
+
with self._override.unbound():
|
|
590
|
+
yield
|
|
591
|
+
return
|
|
566
592
|
scope = self._scope.get(None)
|
|
567
593
|
if scope is None:
|
|
568
594
|
yield
|
|
@@ -617,6 +643,25 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
|
|
|
617
643
|
outer = self._outer_to_join()
|
|
618
644
|
return outer, savepoint or rollback or bool(outer and outer.savepoint)
|
|
619
645
|
|
|
646
|
+
@contextmanager
|
|
647
|
+
def override(self, db: Self) -> Iterator[Self]:
|
|
648
|
+
"""Use another database in place of this one until the block ends.
|
|
649
|
+
|
|
650
|
+
```python
|
|
651
|
+
with Database(TEST_URL) as test_db, db.override(test_db):
|
|
652
|
+
create_user("ada") # runs on test_db
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Every thread sees it. The database stays open when the block ends, so
|
|
656
|
+
close it yourself.
|
|
657
|
+
"""
|
|
658
|
+
before = self._override
|
|
659
|
+
self._override = None if db is self else db
|
|
660
|
+
try:
|
|
661
|
+
yield db
|
|
662
|
+
finally:
|
|
663
|
+
self._override = before
|
|
664
|
+
|
|
620
665
|
def _outer_to_join(self) -> _Outer[ConnectionT] | None:
|
|
621
666
|
"""Return the outer transaction new blocks join, if there is one."""
|
|
622
667
|
outer = self._outer.get(None)
|
|
@@ -811,15 +856,20 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
811
856
|
"""Leave everything to [`configure`][sqlakit.Databases.configure]."""
|
|
812
857
|
self._default: DatabaseT | None = None
|
|
813
858
|
self._aliased: dict[str, DatabaseT] = {}
|
|
859
|
+
self._overridden: dict[str, DatabaseT] = {}
|
|
860
|
+
self._override = None
|
|
814
861
|
self._routers: tuple[Any, ...] = ()
|
|
815
862
|
self._using: ContextVar[str | None] = ContextVar(
|
|
816
863
|
f"{type(self).__name__}.using", default=None
|
|
817
864
|
)
|
|
818
865
|
|
|
819
866
|
def __repr__(self) -> str:
|
|
820
|
-
if
|
|
867
|
+
if self._built_its_own:
|
|
868
|
+
return super().__repr__()
|
|
869
|
+
held = self._override
|
|
870
|
+
if held is None:
|
|
821
871
|
return f"{type(self).__name__}(unconfigured)"
|
|
822
|
-
return
|
|
872
|
+
return f"{type(self).__name__}({held!r})"
|
|
823
873
|
|
|
824
874
|
def __getitem__(self, alias: str) -> Self | DatabaseT:
|
|
825
875
|
"""Return the database configured as ``alias``.
|
|
@@ -831,6 +881,8 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
831
881
|
UnknownDatabaseError: if nothing is configured under that alias.
|
|
832
882
|
|
|
833
883
|
"""
|
|
884
|
+
if alias in self._overridden:
|
|
885
|
+
return self._overridden[alias]
|
|
834
886
|
if alias == DEFAULT_ALIAS:
|
|
835
887
|
return self if self._default is None else self._default
|
|
836
888
|
try:
|
|
@@ -839,7 +891,11 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
839
891
|
raise UnknownDatabaseError(alias, self.aliases) from None
|
|
840
892
|
|
|
841
893
|
def __contains__(self, alias: str) -> bool:
|
|
842
|
-
return
|
|
894
|
+
return (
|
|
895
|
+
alias == DEFAULT_ALIAS
|
|
896
|
+
or alias in self._aliased
|
|
897
|
+
or alias in self._overridden
|
|
898
|
+
)
|
|
843
899
|
|
|
844
900
|
def register(self, alias: str, db: DatabaseT) -> None:
|
|
845
901
|
"""Put a database already built under an alias.
|
|
@@ -870,6 +926,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
870
926
|
if self.is_configured:
|
|
871
927
|
raise DefaultAliasError
|
|
872
928
|
self._default = self._named(alias, db)
|
|
929
|
+
self._follow_default()
|
|
873
930
|
return
|
|
874
931
|
if alias in self._aliased:
|
|
875
932
|
raise AliasInUseError(alias)
|
|
@@ -961,6 +1018,43 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
961
1018
|
db._name = alias # noqa: SLF001
|
|
962
1019
|
return db
|
|
963
1020
|
|
|
1021
|
+
@contextmanager
|
|
1022
|
+
def override(
|
|
1023
|
+
self,
|
|
1024
|
+
db: DatabaseT,
|
|
1025
|
+
*,
|
|
1026
|
+
alias: str = DEFAULT_ALIAS,
|
|
1027
|
+
) -> Iterator[DatabaseT]:
|
|
1028
|
+
"""Use another database under an alias until the block ends.
|
|
1029
|
+
|
|
1030
|
+
```python
|
|
1031
|
+
with Database(TEST_URL) as test_db, db.override(test_db):
|
|
1032
|
+
create_user("ada") # runs on test_db
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
Every thread sees it. The database stays open when the block ends, so
|
|
1036
|
+
close it yourself.
|
|
1037
|
+
"""
|
|
1038
|
+
before = self._overridden.get(alias)
|
|
1039
|
+
named = getattr(db, "_name", None)
|
|
1040
|
+
self._overridden[alias] = self._named(alias, db)
|
|
1041
|
+
self._follow_default()
|
|
1042
|
+
try:
|
|
1043
|
+
yield db
|
|
1044
|
+
finally:
|
|
1045
|
+
if before is None:
|
|
1046
|
+
del self._overridden[alias]
|
|
1047
|
+
else:
|
|
1048
|
+
self._overridden[alias] = before
|
|
1049
|
+
self._follow_default()
|
|
1050
|
+
if named is not None:
|
|
1051
|
+
db._name = named # noqa: SLF001
|
|
1052
|
+
|
|
1053
|
+
def _follow_default(self) -> None:
|
|
1054
|
+
"""Work on the database the default alias leads to, unless it is this one."""
|
|
1055
|
+
held = self._overridden.get(DEFAULT_ALIAS, self._default)
|
|
1056
|
+
self._override = None if held is self else held
|
|
1057
|
+
|
|
964
1058
|
def using(self, target: str | DatabaseT) -> _Using:
|
|
965
1059
|
"""Return that database, standing in for the default one.
|
|
966
1060
|
|
|
@@ -997,6 +1091,16 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
997
1091
|
UnregisteredDatabaseError: if it holds it under none.
|
|
998
1092
|
|
|
999
1093
|
"""
|
|
1094
|
+
alias = self._held_as(db)
|
|
1095
|
+
if alias is None:
|
|
1096
|
+
raise UnregisteredDatabaseError(self.aliases)
|
|
1097
|
+
return alias
|
|
1098
|
+
|
|
1099
|
+
def _held_as(self, db: object) -> str | None:
|
|
1100
|
+
"""Return the alias this registry holds a database under, or None."""
|
|
1101
|
+
for alias, held in self._overridden.items():
|
|
1102
|
+
if held is db:
|
|
1103
|
+
return alias
|
|
1000
1104
|
# A configured registry is the default database itself, and a
|
|
1001
1105
|
# registered one holds it.
|
|
1002
1106
|
if db is self or db is self._default:
|
|
@@ -1004,7 +1108,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1004
1108
|
for alias, held in self._aliased.items():
|
|
1005
1109
|
if held is db:
|
|
1006
1110
|
return alias
|
|
1007
|
-
|
|
1111
|
+
return None
|
|
1008
1112
|
|
|
1009
1113
|
def route(self, *routers: Router | RouterFunction | str) -> None:
|
|
1010
1114
|
"""Say which database a model lives on, for models that do not say it.
|
|
@@ -1038,16 +1142,17 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1038
1142
|
opened with `using()` stands in for the default database.
|
|
1039
1143
|
"""
|
|
1040
1144
|
placement = self._routed(model) or model.__db__
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1145
|
+
if not isinstance(placement, str):
|
|
1146
|
+
# A model pinned to a database this registry holds follows the
|
|
1147
|
+
# alias it is held under, `using()` and `override()` alike.
|
|
1148
|
+
alias = self._held_as(placement)
|
|
1149
|
+
if alias is None:
|
|
1150
|
+
return placement
|
|
1151
|
+
placement = alias
|
|
1152
|
+
redirected = self._using.get()
|
|
1153
|
+
if redirected is not None and placement == DEFAULT_ALIAS:
|
|
1154
|
+
placement = redirected
|
|
1155
|
+
return self[placement]
|
|
1051
1156
|
|
|
1052
1157
|
def _routed(self, model: type[Any]) -> str | None:
|
|
1053
1158
|
"""Return what the first router says about this model, if anything."""
|
|
@@ -1059,13 +1164,18 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1059
1164
|
|
|
1060
1165
|
@property
|
|
1061
1166
|
def aliases(self) -> tuple[str, ...]:
|
|
1062
|
-
"""The aliases configured, the default one first."""
|
|
1063
|
-
|
|
1167
|
+
"""The aliases configured or overridden, the default one first."""
|
|
1168
|
+
overridden = (
|
|
1169
|
+
alias
|
|
1170
|
+
for alias in self._overridden
|
|
1171
|
+
if alias != DEFAULT_ALIAS and alias not in self._aliased
|
|
1172
|
+
)
|
|
1173
|
+
return (DEFAULT_ALIAS, *self._aliased, *overridden)
|
|
1064
1174
|
|
|
1065
1175
|
@property
|
|
1066
1176
|
def is_configured(self) -> bool:
|
|
1067
1177
|
"""Whether this registry has a default database to reach."""
|
|
1068
|
-
return "url" in self.__dict__ or self.
|
|
1178
|
+
return "url" in self.__dict__ or self._override is not None
|
|
1069
1179
|
|
|
1070
1180
|
@property
|
|
1071
1181
|
def _built_its_own(self) -> bool:
|
|
@@ -1073,39 +1183,13 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
|
|
|
1073
1183
|
return "url" in self.__dict__
|
|
1074
1184
|
|
|
1075
1185
|
if not TYPE_CHECKING:
|
|
1076
|
-
# Hidden from type checkers, which
|
|
1077
|
-
|
|
1078
|
-
def
|
|
1079
|
-
"""
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
"""
|
|
1084
|
-
|
|
1085
|
-
def reach(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
1086
|
-
held = self._default
|
|
1087
|
-
found = (
|
|
1088
|
-
getattr(held, name) if held is not None else getattr(super(), name)
|
|
1089
|
-
)
|
|
1090
|
-
return found if attribute else found(*args, **kwargs)
|
|
1091
|
-
|
|
1092
|
-
return property(reach) if attribute else reach
|
|
1093
|
-
|
|
1094
|
-
connection = _proxy("connection", attribute=True)
|
|
1095
|
-
engine = _proxy("engine", attribute=True)
|
|
1096
|
-
session = _proxy("session", attribute=True)
|
|
1097
|
-
sql = _proxy("sql", attribute=True)
|
|
1098
|
-
assert_queries = _proxy("assert_queries")
|
|
1099
|
-
autocommit = _proxy("autocommit")
|
|
1100
|
-
connect = _proxy("connect")
|
|
1101
|
-
in_session = _proxy("in_session")
|
|
1102
|
-
in_transaction = _proxy("in_transaction")
|
|
1103
|
-
ping = _proxy("ping")
|
|
1104
|
-
provisioned_tables = _proxy("provisioned_tables")
|
|
1105
|
-
query = _proxy("query")
|
|
1106
|
-
session_factory = _proxy("session_factory")
|
|
1107
|
-
transaction = _proxy("transaction")
|
|
1108
|
-
del _proxy
|
|
1186
|
+
# Hidden from type checkers, which read it off `Database`.
|
|
1187
|
+
@property
|
|
1188
|
+
def sql(self) -> Any: # noqa: ANN401
|
|
1189
|
+
"""The templates of the default database, wherever it was built."""
|
|
1190
|
+
if self._built_its_own or self._override is None:
|
|
1191
|
+
return super().sql
|
|
1192
|
+
return self._override.sql
|
|
1109
1193
|
|
|
1110
1194
|
@overload
|
|
1111
1195
|
def configure(
|
|
@@ -1375,6 +1459,17 @@ class BaseRetryingTransaction:
|
|
|
1375
1459
|
raise AssertionError # pragma: no cover
|
|
1376
1460
|
|
|
1377
1461
|
|
|
1462
|
+
def late_bound(source: object, registry: Any) -> Any: # noqa: ANN401
|
|
1463
|
+
"""Return the database ``using=`` points at: the registry, an alias, or a call."""
|
|
1464
|
+
if source is None:
|
|
1465
|
+
return registry
|
|
1466
|
+
if isinstance(source, str):
|
|
1467
|
+
return registry[source]
|
|
1468
|
+
if callable(source):
|
|
1469
|
+
return source()
|
|
1470
|
+
return source
|
|
1471
|
+
|
|
1472
|
+
|
|
1378
1473
|
def retry_matches(exc: BaseException, retry_on: RetryOn) -> bool:
|
|
1379
1474
|
"""Whether ``retry_on`` claims this exception is worth another attempt."""
|
|
1380
1475
|
if isinstance(retry_on, type | tuple):
|
|
@@ -98,6 +98,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
98
98
|
@property
|
|
99
99
|
def engine(self) -> Engine:
|
|
100
100
|
"""The underlying engine, created on first access."""
|
|
101
|
+
if self._override is not None:
|
|
102
|
+
return self._override.engine
|
|
101
103
|
if self._engine is None:
|
|
102
104
|
with self._engine_lock:
|
|
103
105
|
# Two threads reaching this at once would each build one, and
|
|
@@ -122,6 +124,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
122
124
|
MissingConnectionError: if no connection is bound.
|
|
123
125
|
|
|
124
126
|
"""
|
|
127
|
+
if self._override is not None:
|
|
128
|
+
return self._override.connection
|
|
125
129
|
return self._reused(self._current_scope())
|
|
126
130
|
|
|
127
131
|
def _create_session(self, connection: sa.Connection) -> Session:
|
|
@@ -149,6 +153,11 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
149
153
|
@contextmanager
|
|
150
154
|
def connect(self) -> Iterator[sa.Connection]:
|
|
151
155
|
"""Open a connection and bind it, or reuse the one already bound."""
|
|
156
|
+
elsewhere = self._override
|
|
157
|
+
if elsewhere is not None:
|
|
158
|
+
with elsewhere.connect() as connection:
|
|
159
|
+
yield connection
|
|
160
|
+
return
|
|
152
161
|
reuse = self._scope_to_reuse()
|
|
153
162
|
if reuse is not None:
|
|
154
163
|
with self._bound(self._reused(reuse)) as connection:
|
|
@@ -191,6 +200,11 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
191
200
|
@contextmanager
|
|
192
201
|
def _autocommit(self) -> Iterator[sa.Connection]:
|
|
193
202
|
"""Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
|
|
203
|
+
elsewhere = self._override
|
|
204
|
+
if elsewhere is not None:
|
|
205
|
+
with elsewhere.autocommit() as connection:
|
|
206
|
+
yield connection
|
|
207
|
+
return
|
|
194
208
|
outer = self._connection_to_join()
|
|
195
209
|
if outer is not None:
|
|
196
210
|
with self._bound(outer, commit=True) as connection:
|
|
@@ -321,6 +335,11 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
321
335
|
flush, as ``sessionmaker()`` does it. Inside another block it runs on
|
|
322
336
|
the connection already bound.
|
|
323
337
|
"""
|
|
338
|
+
elsewhere = self._override
|
|
339
|
+
if elsewhere is not None:
|
|
340
|
+
with elsewhere.session_factory() as session:
|
|
341
|
+
yield session
|
|
342
|
+
return
|
|
324
343
|
reuse = self._scope_to_reuse()
|
|
325
344
|
if reuse is not None:
|
|
326
345
|
with self._bound(self._reused(reuse)):
|
|
@@ -385,6 +404,10 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
385
404
|
Every table of the metadata unless ``tables`` names fewer, as a second
|
|
386
405
|
database wants.
|
|
387
406
|
"""
|
|
407
|
+
if self._override is not None:
|
|
408
|
+
with self._override.provisioned_tables(metadata, tables=tables):
|
|
409
|
+
yield
|
|
410
|
+
return
|
|
388
411
|
with self.transaction() as connection:
|
|
389
412
|
metadata.create_all(connection, tables=tables)
|
|
390
413
|
try:
|
|
@@ -395,6 +418,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
|
|
|
395
418
|
|
|
396
419
|
def ping(self) -> bool:
|
|
397
420
|
"""Whether the database answers."""
|
|
421
|
+
if self._override is not None:
|
|
422
|
+
return self._override.ping()
|
|
398
423
|
try:
|
|
399
424
|
with self.engine.connect() as connection:
|
|
400
425
|
connection.execute(sa.text("SELECT 1"))
|
|
@@ -472,6 +497,18 @@ class Transaction(ContextDecorator, AbstractContextManager["sa.Connection"]):
|
|
|
472
497
|
|
|
473
498
|
def __enter__(self) -> sa.Connection:
|
|
474
499
|
stack = ExitStack()
|
|
500
|
+
elsewhere = self.db._override # noqa: SLF001
|
|
501
|
+
if elsewhere is not None:
|
|
502
|
+
connection = stack.enter_context(
|
|
503
|
+
elsewhere.transaction(
|
|
504
|
+
savepoint=self.savepoint,
|
|
505
|
+
join_nested=self.join_nested,
|
|
506
|
+
rollback=self.rollback,
|
|
507
|
+
commit_on_error=self.commit_on_error,
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
self._stacks.append(stack)
|
|
511
|
+
return connection
|
|
475
512
|
try:
|
|
476
513
|
outer, savepoint = self.db._plan( # noqa: SLF001
|
|
477
514
|
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
|
|
@@ -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
|
+
]
|
|
@@ -102,6 +102,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
102
102
|
@property
|
|
103
103
|
def engine(self) -> AsyncEngine:
|
|
104
104
|
"""The underlying engine, created on first access."""
|
|
105
|
+
if self._override is not None:
|
|
106
|
+
return self._override.engine
|
|
105
107
|
if self._engine is None:
|
|
106
108
|
with self._engine_lock:
|
|
107
109
|
# Two threads reaching this at once would each build one, and
|
|
@@ -127,6 +129,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
127
129
|
MissingConnectionError: if no connection is bound.
|
|
128
130
|
|
|
129
131
|
"""
|
|
132
|
+
if self._override is not None:
|
|
133
|
+
return self._override.connection
|
|
130
134
|
scope = self._current_scope()
|
|
131
135
|
if scope.connection is None:
|
|
132
136
|
cell = scope.checkout
|
|
@@ -143,6 +147,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
143
147
|
|
|
144
148
|
async def _aconnection(self) -> AsyncConnection:
|
|
145
149
|
"""Return the bound connection, checking it out first if lazy."""
|
|
150
|
+
if self._override is not None:
|
|
151
|
+
return await self._override._aconnection() # noqa: SLF001
|
|
146
152
|
return await self._areused(self._current_scope())
|
|
147
153
|
|
|
148
154
|
def _create_session(self, connection: AsyncConnection) -> AsyncSession:
|
|
@@ -183,6 +189,11 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
183
189
|
@asynccontextmanager
|
|
184
190
|
async def connect(self) -> AsyncIterator[AsyncConnection]:
|
|
185
191
|
"""Open a connection and bind it, or reuse the one already bound."""
|
|
192
|
+
elsewhere = self._override
|
|
193
|
+
if elsewhere is not None:
|
|
194
|
+
async with elsewhere.connect() as connection:
|
|
195
|
+
yield connection
|
|
196
|
+
return
|
|
186
197
|
reuse = self._scope_to_reuse()
|
|
187
198
|
if reuse is not None:
|
|
188
199
|
async with self._bound(await self._areused(reuse)) as connection:
|
|
@@ -225,6 +236,11 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
225
236
|
@asynccontextmanager
|
|
226
237
|
async def _autocommit(self) -> AsyncIterator[AsyncConnection]:
|
|
227
238
|
"""Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
|
|
239
|
+
elsewhere = self._override
|
|
240
|
+
if elsewhere is not None:
|
|
241
|
+
async with elsewhere.autocommit() as connection:
|
|
242
|
+
yield connection
|
|
243
|
+
return
|
|
228
244
|
outer = self._connection_to_join()
|
|
229
245
|
if outer is not None:
|
|
230
246
|
async with self._bound(outer, commit=True) as connection:
|
|
@@ -355,6 +371,11 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
355
371
|
flush, as ``async_sessionmaker()`` does it. Inside another block it
|
|
356
372
|
runs on the connection already bound.
|
|
357
373
|
"""
|
|
374
|
+
elsewhere = self._override
|
|
375
|
+
if elsewhere is not None:
|
|
376
|
+
async with elsewhere.session_factory() as session:
|
|
377
|
+
yield session
|
|
378
|
+
return
|
|
358
379
|
reuse = self._scope_to_reuse()
|
|
359
380
|
if reuse is not None:
|
|
360
381
|
async with self._bound(await self._areused(reuse)):
|
|
@@ -419,6 +440,10 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
419
440
|
Every table of the metadata unless ``tables`` names fewer, as a second
|
|
420
441
|
database wants.
|
|
421
442
|
"""
|
|
443
|
+
if self._override is not None:
|
|
444
|
+
async with self._override.provisioned_tables(metadata, tables=tables):
|
|
445
|
+
yield
|
|
446
|
+
return
|
|
422
447
|
async with self.transaction() as connection:
|
|
423
448
|
await connection.run_sync(metadata.create_all, tables=tables)
|
|
424
449
|
try:
|
|
@@ -429,6 +454,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
|
|
|
429
454
|
|
|
430
455
|
async def ping(self) -> bool:
|
|
431
456
|
"""Whether the database answers."""
|
|
457
|
+
if self._override is not None:
|
|
458
|
+
return await self._override.ping()
|
|
432
459
|
try:
|
|
433
460
|
async with self.engine.connect() as connection:
|
|
434
461
|
await connection.execute(sa.text("SELECT 1"))
|
|
@@ -512,6 +539,18 @@ class Transaction(
|
|
|
512
539
|
|
|
513
540
|
async def __aenter__(self) -> AsyncConnection:
|
|
514
541
|
stack = AsyncExitStack()
|
|
542
|
+
elsewhere = self.db._override # noqa: SLF001
|
|
543
|
+
if elsewhere is not None:
|
|
544
|
+
connection = await stack.enter_async_context(
|
|
545
|
+
elsewhere.transaction(
|
|
546
|
+
savepoint=self.savepoint,
|
|
547
|
+
join_nested=self.join_nested,
|
|
548
|
+
rollback=self.rollback,
|
|
549
|
+
commit_on_error=self.commit_on_error,
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
self._stacks.append(stack)
|
|
553
|
+
return connection
|
|
515
554
|
try:
|
|
516
555
|
outer, savepoint = self.db._plan( # noqa: SLF001
|
|
517
556
|
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
|
|
File without changes
|