sqlakit 0.22.10__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.
Files changed (34) hide show
  1. {sqlakit-0.22.10 → sqlakit-0.22.11}/PKG-INFO +1 -1
  2. {sqlakit-0.22.10 → sqlakit-0.22.11}/pyproject.toml +1 -1
  3. {sqlakit-0.22.10 → sqlakit-0.22.11}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_base.py +60 -40
  5. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_db.py +14 -4
  6. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/asyncio/_db.py +16 -4
  7. {sqlakit-0.22.10 → sqlakit-0.22.11}/LICENSE +0 -0
  8. {sqlakit-0.22.10 → sqlakit-0.22.11}/README.md +0 -0
  9. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/__init__.py +0 -0
  10. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_cli.py +0 -0
  11. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_discovery.py +0 -0
  12. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_linters.py +0 -0
  13. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_model.py +0 -0
  14. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_project.py +0 -0
  15. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_pycharm.py +0 -0
  16. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_query.py +0 -0
  17. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_recording.py +0 -0
  18. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_registry.py +0 -0
  19. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_render.py +0 -0
  20. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_routing.py +0 -0
  21. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_sql.py +0 -0
  22. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_sql_formatter.py +0 -0
  23. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/_static.py +0 -0
  24. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/asyncio/__init__.py +0 -0
  25. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/asyncio/_registry.py +0 -0
  26. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/asyncio/orm.py +0 -0
  27. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/asyncio/sql.py +0 -0
  28. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/exceptions.py +0 -0
  29. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/orm.py +0 -0
  30. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/py.typed +0 -0
  31. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/pytest_plugin.py +0 -0
  32. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/sql.py +0 -0
  33. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/testing.py +0 -0
  34. {sqlakit-0.22.10 → sqlakit-0.22.11}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.22.10
3
+ Version: 0.22.11
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.22.10"
3
+ version = "0.22.11"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.22.10"
3
+ version = "0.22.11"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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,9 +643,24 @@ 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
 
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
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
623
664
 
624
665
  def _outer_to_join(self) -> _Outer[ConnectionT] | None:
625
666
  """Return the outer transaction new blocks join, if there is one."""
@@ -816,6 +857,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
816
857
  self._default: DatabaseT | None = None
817
858
  self._aliased: dict[str, DatabaseT] = {}
818
859
  self._overridden: dict[str, DatabaseT] = {}
860
+ self._override = None
819
861
  self._routers: tuple[Any, ...] = ()
820
862
  self._using: ContextVar[str | None] = ContextVar(
821
863
  f"{type(self).__name__}.using", default=None
@@ -824,7 +866,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
824
866
  def __repr__(self) -> str:
825
867
  if self._built_its_own:
826
868
  return super().__repr__()
827
- held = self._stand_in()
869
+ held = self._override
828
870
  if held is None:
829
871
  return f"{type(self).__name__}(unconfigured)"
830
872
  return f"{type(self).__name__}({held!r})"
@@ -884,6 +926,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
884
926
  if self.is_configured:
885
927
  raise DefaultAliasError
886
928
  self._default = self._named(alias, db)
929
+ self._follow_default()
887
930
  return
888
931
  if alias in self._aliased:
889
932
  raise AliasInUseError(alias)
@@ -995,6 +1038,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
995
1038
  before = self._overridden.get(alias)
996
1039
  named = getattr(db, "_name", None)
997
1040
  self._overridden[alias] = self._named(alias, db)
1041
+ self._follow_default()
998
1042
  try:
999
1043
  yield db
1000
1044
  finally:
@@ -1002,13 +1046,14 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1002
1046
  del self._overridden[alias]
1003
1047
  else:
1004
1048
  self._overridden[alias] = before
1049
+ self._follow_default()
1005
1050
  if named is not None:
1006
1051
  db._name = named # noqa: SLF001
1007
1052
 
1008
- def _stand_in(self) -> DatabaseT | None:
1009
- """Return the database the default alias leads to, if not this registry."""
1053
+ def _follow_default(self) -> None:
1054
+ """Work on the database the default alias leads to, unless it is this one."""
1010
1055
  held = self._overridden.get(DEFAULT_ALIAS, self._default)
1011
- return None if held is self else held
1056
+ self._override = None if held is self else held
1012
1057
 
1013
1058
  def using(self, target: str | DatabaseT) -> _Using:
1014
1059
  """Return that database, standing in for the default one.
@@ -1130,7 +1175,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1130
1175
  @property
1131
1176
  def is_configured(self) -> bool:
1132
1177
  """Whether this registry has a default database to reach."""
1133
- return "url" in self.__dict__ or self._stand_in() is not None
1178
+ return "url" in self.__dict__ or self._override is not None
1134
1179
 
1135
1180
  @property
1136
1181
  def _built_its_own(self) -> bool:
@@ -1138,38 +1183,13 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1138
1183
  return "url" in self.__dict__
1139
1184
 
1140
1185
  if not TYPE_CHECKING:
1141
- # Hidden from type checkers, which keep reading these off `Database`
1142
- # and its asyncio twin, signatures and all.
1143
- def _proxy(name: str, *, attribute: bool = False) -> Any: # noqa: ANN401, N805
1144
- """Proxy to the database this registry holds, or call its own.
1145
-
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.
1149
- """
1150
-
1151
- def reach(self: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
1152
- held = self._stand_in()
1153
- found = (
1154
- getattr(held, name) if held is not None else getattr(super(), name)
1155
- )
1156
- return found if attribute else found(*args, **kwargs)
1157
-
1158
- return property(reach) if attribute else reach
1159
-
1160
- connection = _proxy("connection", attribute=True)
1161
- engine = _proxy("engine", attribute=True)
1162
- session = _proxy("session", attribute=True)
1163
- sql = _proxy("sql", attribute=True)
1164
- assert_queries = _proxy("assert_queries")
1165
- in_session = _proxy("in_session")
1166
- in_transaction = _proxy("in_transaction")
1167
- ping = _proxy("ping")
1168
- provisioned_tables = _proxy("provisioned_tables")
1169
- query = _proxy("query")
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.
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
1173
1193
 
1174
1194
  @overload
1175
1195
  def configure(
@@ -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,7 +153,7 @@ 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."""
152
- elsewhere = self._stand_in()
156
+ elsewhere = self._override
153
157
  if elsewhere is not None:
154
158
  with elsewhere.connect() as connection:
155
159
  yield connection
@@ -196,7 +200,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
196
200
  @contextmanager
197
201
  def _autocommit(self) -> Iterator[sa.Connection]:
198
202
  """Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
199
- elsewhere = self._stand_in()
203
+ elsewhere = self._override
200
204
  if elsewhere is not None:
201
205
  with elsewhere.autocommit() as connection:
202
206
  yield connection
@@ -331,7 +335,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
331
335
  flush, as ``sessionmaker()`` does it. Inside another block it runs on
332
336
  the connection already bound.
333
337
  """
334
- elsewhere = self._stand_in()
338
+ elsewhere = self._override
335
339
  if elsewhere is not None:
336
340
  with elsewhere.session_factory() as session:
337
341
  yield session
@@ -400,6 +404,10 @@ class Database(BaseDatabase[sa.Connection, Session]):
400
404
  Every table of the metadata unless ``tables`` names fewer, as a second
401
405
  database wants.
402
406
  """
407
+ if self._override is not None:
408
+ with self._override.provisioned_tables(metadata, tables=tables):
409
+ yield
410
+ return
403
411
  with self.transaction() as connection:
404
412
  metadata.create_all(connection, tables=tables)
405
413
  try:
@@ -410,6 +418,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
410
418
 
411
419
  def ping(self) -> bool:
412
420
  """Whether the database answers."""
421
+ if self._override is not None:
422
+ return self._override.ping()
413
423
  try:
414
424
  with self.engine.connect() as connection:
415
425
  connection.execute(sa.text("SELECT 1"))
@@ -487,7 +497,7 @@ class Transaction(ContextDecorator, AbstractContextManager["sa.Connection"]):
487
497
 
488
498
  def __enter__(self) -> sa.Connection:
489
499
  stack = ExitStack()
490
- elsewhere = self.db._stand_in() # noqa: SLF001
500
+ elsewhere = self.db._override # noqa: SLF001
491
501
  if elsewhere is not None:
492
502
  connection = stack.enter_context(
493
503
  elsewhere.transaction(
@@ -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,7 +189,7 @@ 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."""
186
- elsewhere = self._stand_in()
192
+ elsewhere = self._override
187
193
  if elsewhere is not None:
188
194
  async with elsewhere.connect() as connection:
189
195
  yield connection
@@ -230,7 +236,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
230
236
  @asynccontextmanager
231
237
  async def _autocommit(self) -> AsyncIterator[AsyncConnection]:
232
238
  """Open a connection in ``AUTOCOMMIT`` and bind it, or join the outer one."""
233
- elsewhere = self._stand_in()
239
+ elsewhere = self._override
234
240
  if elsewhere is not None:
235
241
  async with elsewhere.autocommit() as connection:
236
242
  yield connection
@@ -365,7 +371,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
365
371
  flush, as ``async_sessionmaker()`` does it. Inside another block it
366
372
  runs on the connection already bound.
367
373
  """
368
- elsewhere = self._stand_in()
374
+ elsewhere = self._override
369
375
  if elsewhere is not None:
370
376
  async with elsewhere.session_factory() as session:
371
377
  yield session
@@ -434,6 +440,10 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
434
440
  Every table of the metadata unless ``tables`` names fewer, as a second
435
441
  database wants.
436
442
  """
443
+ if self._override is not None:
444
+ async with self._override.provisioned_tables(metadata, tables=tables):
445
+ yield
446
+ return
437
447
  async with self.transaction() as connection:
438
448
  await connection.run_sync(metadata.create_all, tables=tables)
439
449
  try:
@@ -444,6 +454,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
444
454
 
445
455
  async def ping(self) -> bool:
446
456
  """Whether the database answers."""
457
+ if self._override is not None:
458
+ return await self._override.ping()
447
459
  try:
448
460
  async with self.engine.connect() as connection:
449
461
  await connection.execute(sa.text("SELECT 1"))
@@ -527,7 +539,7 @@ class Transaction(
527
539
 
528
540
  async def __aenter__(self) -> AsyncConnection:
529
541
  stack = AsyncExitStack()
530
- elsewhere = self.db._stand_in() # noqa: SLF001
542
+ elsewhere = self.db._override # noqa: SLF001
531
543
  if elsewhere is not None:
532
544
  connection = await stack.enter_async_context(
533
545
  elsewhere.transaction(
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