sqlakit 0.10.0__tar.gz → 0.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {sqlakit-0.10.0 → sqlakit-0.10.2}/PKG-INFO +1 -1
  2. {sqlakit-0.10.0 → sqlakit-0.10.2}/pyproject.toml +1 -1
  3. {sqlakit-0.10.0 → sqlakit-0.10.2}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_base.py +16 -16
  5. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_db.py +8 -8
  6. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_debugserver.py +41 -3
  7. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_discovery.py +1 -1
  8. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_model.py +8 -8
  9. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_query.py +8 -8
  10. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_recording.py +8 -5
  11. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_registry.py +1 -1
  12. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_sql.py +2 -2
  13. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/asyncio/_db.py +8 -8
  14. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/asyncio/_registry.py +2 -2
  15. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/asyncio/orm.py +1 -1
  16. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/asyncio/sql.py +3 -3
  17. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/debugserver.html +42 -42
  18. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/exceptions.py +1 -1
  19. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/orm.py +1 -1
  20. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/pytest_plugin.py +1 -1
  21. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/sql.py +3 -3
  22. {sqlakit-0.10.0 → sqlakit-0.10.2}/LICENSE +0 -0
  23. {sqlakit-0.10.0 → sqlakit-0.10.2}/README.md +0 -0
  24. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/__init__.py +0 -0
  25. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_cli.py +0 -0
  26. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/_routing.py +0 -0
  27. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/asyncio/__init__.py +0 -0
  28. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/py.typed +0 -0
  29. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/testing.py +0 -0
  30. {sqlakit-0.10.0 → sqlakit-0.10.2}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.10.0"
3
+ version = "0.10.2"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.10.0"
3
+ version = "0.10.2"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -193,7 +193,7 @@ class _Outer(Generic[ConnectionT]):
193
193
 
194
194
 
195
195
  class BaseDatabase(Generic[ConnectionT, SessionT]):
196
- """What the sync and async databases share: everything that is not IO.
196
+ """The sync and async databases share this: everything that is not IO.
197
197
 
198
198
  Binding to the context lives here. Opening and closing connections and
199
199
  sessions is left to the subclass.
@@ -578,9 +578,9 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
578
578
  ) -> Iterator[_Outer[ConnectionT] | None]:
579
579
  """Make ``connection`` the outer one for this context. See `_Outer`.
580
580
 
581
- ``None`` leaves this context without an outer transaction at all, which
582
- is what ``autocommit()`` needs: blocks under it must not join a
583
- transaction its own connection is not part of.
581
+ ``None`` leaves this context without an outer transaction at all, as
582
+ ``autocommit()`` needs: blocks under it must not join a transaction
583
+ its own connection is not part of.
584
584
  """
585
585
  outer = (
586
586
  _Outer(
@@ -605,9 +605,9 @@ DatabaseT = TypeVar("DatabaseT", bound="BaseDatabase[Any, Any]")
605
605
  class _Using:
606
606
  """A database standing in for the default one, while a block of it is open.
607
607
 
608
- What `db.using(alias)` returns. Everything a database does, it does; what
609
- it adds is the redirection: for as long as one of its blocks is open, a
610
- model that lives on the default database resolves here instead.
608
+ `db.using(alias)` returns one of these. Everything a database does, it
609
+ does, and it adds the redirection: for as long as one of its blocks is
610
+ open, a model that lives on the default database resolves here instead.
611
611
  """
612
612
 
613
613
  def __init__(
@@ -726,7 +726,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
726
726
  def __getitem__(self, alias: str) -> Self | DatabaseT:
727
727
  """Return the database configured as ``alias``.
728
728
 
729
- ``db["default"]`` is what the code reaches without an alias: this
729
+ ``db["default"]`` is the database the code reaches without an alias: this
730
730
  registry, or the database `register` was given for that name.
731
731
 
732
732
  Raises:
@@ -882,7 +882,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
882
882
  """Say which database a model lives on, for models that do not say it.
883
883
 
884
884
  Each router takes a model and returns an alias, or None to leave the question
885
- to the next one. A dotted path is imported, which is what settings hand over:
885
+ to the next one. A dotted path is imported, since settings carry paths:
886
886
 
887
887
  ```python
888
888
  db.route(lambda model: "warehouse" if is_report(model) else None)
@@ -1058,11 +1058,11 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1058
1058
  if not TYPE_CHECKING:
1059
1059
  # Hidden from type checkers: seeing it, they would take every attribute
1060
1060
  # to exist and stop reporting typos. It is reached when normal lookup
1061
- # fails, which is what a registry with no database of its own looks
1062
- # like, from the outside and from its own methods.
1061
+ # fails, as it does on a registry with no database of its own, from
1062
+ # the outside and from its own methods.
1063
1063
  def __getattr__(self, name: str) -> object:
1064
1064
  # Only the database half is worth explaining. Anything else is a
1065
- # name that does not exist, and saying so is what lets `hasattr`,
1065
+ # name that does not exist, and saying so lets `hasattr`,
1066
1066
  # `copy` and every library that introspects work.
1067
1067
  state = self.__dict__
1068
1068
  if "url" in state:
@@ -1078,9 +1078,9 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
1078
1078
 
1079
1079
 
1080
1080
  DATABASE_STATE = frozenset(
1081
- # What `BaseDatabase.__init__` sets. A registry has these once it has a
1082
- # database of its own, and reaching for one before then is the question
1083
- # `DatabaseNotConfiguredError` answers, whichever method asked.
1081
+ # The attributes `BaseDatabase.__init__` sets. A registry has these once
1082
+ # it has a database of its own, and reaching for one before then raises
1083
+ # `DatabaseNotConfiguredError`, whichever method asked.
1084
1084
  {
1085
1085
  "url",
1086
1086
  "templates",
@@ -1164,7 +1164,7 @@ def lazy_session_class(base: type) -> type:
1164
1164
 
1165
1165
 
1166
1166
  class BaseRetryingTransaction:
1167
- """What ``transaction()`` returns when given ``retry_on``.
1167
+ """``transaction()`` returns this when given ``retry_on``.
1168
1168
 
1169
1169
  A decorator, and deliberately not a context manager: retrying re-runs the
1170
1170
  block, which a ``with`` statement cannot do. Entering one is rejected by
@@ -281,7 +281,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
281
281
  let them reach the database on their own, seeing nothing of this
282
282
  block and surviving its rollback.
283
283
  rollback: Roll back on the way out rather than commit. Implies
284
- ``savepoint``, and is what wraps a test.
284
+ ``savepoint``, and wraps a test.
285
285
  commit_on_error: Exception types whose escape still commits. The
286
286
  exception propagates; what was written before it stays.
287
287
  retry_on: Exception types, or a predicate over the exception, worth
@@ -375,7 +375,7 @@ class Database(BaseDatabase[sa.Connection, Session]):
375
375
  ) -> Iterator[None]:
376
376
  """Create these tables here, and drop them when the block ends.
377
377
 
378
- What a test session opens once, around everything that needs a schema:
378
+ A test session opens this once, around everything that needs a schema:
379
379
 
380
380
  ```python
381
381
  @pytest.fixture(scope="session")
@@ -384,8 +384,8 @@ class Database(BaseDatabase[sa.Connection, Session]):
384
384
  yield
385
385
  ```
386
386
 
387
- Every table of the metadata unless ``tables`` names fewer, which is what a
388
- second database wants.
387
+ Every table of the metadata unless ``tables`` names fewer, as a second
388
+ database wants.
389
389
  """
390
390
  with self.transaction() as connection:
391
391
  metadata.create_all(connection, tables=tables)
@@ -434,10 +434,10 @@ def _owned(
434
434
 
435
435
 
436
436
  class Transaction(ContextDecorator, AbstractContextManager["sa.Connection"]):
437
- """What [`Database.transaction`][sqlakit.Database.transaction] returns.
437
+ """[`Database.transaction`][sqlakit.Database.transaction] returns this.
438
438
 
439
439
  A class rather than a generator, so that it works as a decorator and can be
440
- entered more than once, which is what decorating a function does.
440
+ entered more than once, as decorating a function does.
441
441
  """
442
442
 
443
443
  def __init__(
@@ -595,8 +595,8 @@ class Transaction(ContextDecorator, AbstractContextManager["sa.Connection"]):
595
595
  class RetryingTransaction(BaseRetryingTransaction):
596
596
  """A transaction that runs its block again.
597
597
 
598
- What [`Database.transaction`][sqlakit.Database.transaction] returns when it is
599
- given ``retry_on``.
598
+ [`Database.transaction`][sqlakit.Database.transaction] returns this when
599
+ it is given ``retry_on``.
600
600
  """
601
601
 
602
602
  def __call__(self, func: _FuncT) -> _FuncT:
@@ -57,16 +57,33 @@ class Records:
57
57
  def __init__(self, history: int = HISTORY) -> None:
58
58
  self._lock = threading.Lock()
59
59
  self._records: deque[dict[str, Any]] = deque(maxlen=history)
60
+ self._held: set[str] = set()
60
61
  self._watchers: list[queue.Queue[dict[str, Any]]] = []
61
62
 
62
63
  def add(self, record: dict[str, Any]) -> None:
63
- """Keep a recording, and hand it to every page that is watching."""
64
+ """Keep a recording, and hand it to every page that is watching.
65
+
66
+ A recording is kept once. Several blocks may report one recording,
67
+ and each of them sends it, which is how databases outside a registry
68
+ report under a single label.
69
+ """
64
70
  with self._lock:
71
+ sent = record.get("id")
72
+ if sent is not None:
73
+ if sent in self._held:
74
+ return
75
+ self._held.add(sent)
76
+ if len(self._records) == self._records.maxlen:
77
+ self._forget(self._records[0])
65
78
  self._records.append(record)
66
79
  watchers = list(self._watchers)
67
80
  for watcher in watchers:
68
81
  watcher.put(record)
69
82
 
83
+ def _forget(self, record: dict[str, Any]) -> None:
84
+ """Drop the id of a recording the history no longer holds."""
85
+ self._held.discard(record.get("id", ""))
86
+
70
87
  def all(self) -> list[dict[str, Any]]:
71
88
  """Every recording held, oldest first."""
72
89
  with self._lock:
@@ -76,6 +93,7 @@ class Records:
76
93
  """Forget them."""
77
94
  with self._lock:
78
95
  self._records.clear()
96
+ self._held.clear()
79
97
 
80
98
  def watch(self) -> queue.Queue[dict[str, Any]]:
81
99
  """Return a queue that every later recording arrives on."""
@@ -93,7 +111,7 @@ class Records:
93
111
 
94
112
  @dataclass(frozen=True, slots=True)
95
113
  class DebugServer:
96
- """Where recordings go, and who is sending them.
114
+ """The address recordings go to, and the application sending them.
97
115
 
98
116
  A server watches several applications at once, so a recording says which
99
117
  one it came from:
@@ -126,7 +144,22 @@ class DebugServer:
126
144
  @property
127
145
  def sender(self) -> str:
128
146
  """The name the page groups this application under."""
129
- return self.app or pathlib.Path(sys.argv[0]).name or "python"
147
+ return self.app or _program()
148
+
149
+
150
+ def _program() -> str:
151
+ """Name the running program, for a sender that named no application.
152
+
153
+ `python -m myapp` runs a file called `__main__.py`, which says nothing on
154
+ a page, so the module it was started as is the name instead.
155
+ """
156
+ started = getattr(sys.modules.get("__main__"), "__spec__", None)
157
+ if started is not None and started.name:
158
+ return started.name.removesuffix(".__main__")
159
+ script = pathlib.Path(sys.argv[0])
160
+ if script.name in ("", "__main__.py"):
161
+ return script.parent.name or "python"
162
+ return script.name
130
163
 
131
164
 
132
165
  def as_payload(
@@ -134,6 +167,7 @@ def as_payload(
134
167
  ) -> dict[str, Any]:
135
168
  """Return what travels: the recording, flattened to what a page shows."""
136
169
  return {
170
+ "id": recording.id,
137
171
  "app": app,
138
172
  "tags": list(tags),
139
173
  "label": recording.label,
@@ -160,6 +194,10 @@ def send_recording(recording: Recording, to: DebugServer | tuple[str, int]) -> N
160
194
 
161
195
  A thread of its own does the sending, so the block that recorded pays
162
196
  nothing for a server that is slow, or down, or not there at all.
197
+
198
+ A recording carries an id, and a server keeps the first that arrives under
199
+ it. Two blocks writing into one recording, which is how several databases
200
+ report under a single label, would otherwise show up twice.
163
201
  """
164
202
  server = DebugServer.of(to)
165
203
  body = json.dumps(
@@ -15,7 +15,7 @@ __all__ = ["import_models", "import_string"]
15
15
  def import_string(path: str) -> Any: # noqa: ANN401
16
16
  """Return what a dotted path names, importing what it has to.
17
17
 
18
- What settings hand over instead of the thing itself:
18
+ Settings carry a path instead of the thing itself:
19
19
 
20
20
  ```python
21
21
  db.configure(
@@ -50,7 +50,7 @@ DatabaseT = TypeVar("DatabaseT", bound="BaseDatabase[Any, Any]")
50
50
 
51
51
 
52
52
  class DatabaseSource(Protocol):
53
- """Where an alias is looked up: the importable registry, or a stand-in."""
53
+ """The registry an alias is looked up in: the importable one, or a stand-in."""
54
54
 
55
55
  def __getitem__(self, alias: str) -> BaseDatabase[Any, Any]: ...
56
56
 
@@ -82,7 +82,7 @@ class RegistryDescriptor:
82
82
 
83
83
 
84
84
  class BaseModel(Generic[DatabaseT]):
85
- """What the sync and async models share: everything that is not IO.
85
+ """The sync and async models share this: everything that is not IO.
86
86
 
87
87
  A model works on the database named by ``__db__``: an alias in the
88
88
  importable registry, or a database of its own.
@@ -95,7 +95,7 @@ class BaseModel(Generic[DatabaseT]):
95
95
  """Where an alias in ``__db__`` is looked up."""
96
96
 
97
97
  # Declared, not assigned: the declarative base a model is built on brings
98
- # them, and saying so here is what lets the helpers below read them.
98
+ # them, and saying so here lets the helpers below read them.
99
99
  registry: ClassVar[sa.orm.registry]
100
100
  metadata: ClassVar[sa.MetaData]
101
101
 
@@ -134,9 +134,9 @@ class BaseModel(Generic[DatabaseT]):
134
134
  User(name="ada").save()
135
135
  ```
136
136
 
137
- Which database a model resolves to is still `__db__`, the routers and
138
- the open `using()` block, in that order. A model left on the default
139
- alias follows `using()`, which is what makes the switch above work.
137
+ A model still resolves through `__db__`, the routers and the open
138
+ `using()` block, in that order. A model left on the default alias
139
+ follows `using()`, and that makes the switch above work.
140
140
 
141
141
  Without an alias it goes in as the default one, which is where a model
142
142
  that names no database lives:
@@ -196,7 +196,7 @@ class BaseModel(Generic[DatabaseT]):
196
196
  campaign.set_loaded("thumbnail", None) # known to be empty
197
197
  ```
198
198
 
199
- What a `lazy="raise"` relationship needs when the value is already in hand:
199
+ A `lazy="raise"` relationship needs this when the value is in hand:
200
200
  rows fetched for a whole page at once, a row this block created, or an
201
201
  instance that outlives the session that loaded it.
202
202
 
@@ -293,7 +293,7 @@ def tables_for(
293
293
  ) -> list[sa.Table] | None:
294
294
  """Return the tables of this model's metadata that live on that database.
295
295
 
296
- None when they all do, which is what `create_all` wants for the ordinary
296
+ None when they all do, as `create_all` wants for the ordinary
297
297
  case of one database. Otherwise the tables of the models pointed at it,
298
298
  and the tables that only reference those. An association table belongs
299
299
  with the rows it joins.
@@ -221,7 +221,7 @@ class Page(Generic[ModelT, TotalT]):
221
221
  def with_items(self, items: Sequence[OtherT]) -> Page[OtherT, TotalT]:
222
222
  """Return the page carrying these rows instead, counts unchanged.
223
223
 
224
- What an asynchronous transform needs: `page.with_items(await serialize(...))`.
224
+ An asynchronous transform needs this: `page.with_items(await serialize(...))`.
225
225
 
226
226
  Raises:
227
227
  PageItemsMismatchError: if there is not one item per row.
@@ -385,7 +385,7 @@ class BaseQuery(Generic[ModelT]):
385
385
  def with_select(self, select: sa.Select[Any]) -> Self:
386
386
  """Return a query like this one, over the given select.
387
387
 
388
- This is what every builder is made of. Use it in a method of your own
388
+ Every builder is made of this. Use it in a method of your own
389
389
  when the statement needs SQLAlchemy the builders do not cover.
390
390
 
391
391
  Raises:
@@ -415,7 +415,7 @@ class BaseQuery(Generic[ModelT]):
415
415
  def is_ordered(self) -> bool:
416
416
  """Whether this query carries an ordering.
417
417
 
418
- What a method of your own asks before it adds a default one, since
418
+ A method of your own asks this before it adds a default one, since
419
419
  `page` and `cursor_page` refuse a query with no ordering.
420
420
  """
421
421
  return bool(self._select._order_by_clauses) # noqa: SLF001
@@ -458,7 +458,7 @@ class BaseQuery(Generic[ModelT]):
458
458
  ```
459
459
 
460
460
  Nothing can be added afterwards, and ``__query_filter__`` is not applied:
461
- what the statement selects is what comes back.
461
+ the statement selects the rows that come back.
462
462
  """
463
463
  query = self._copy()
464
464
  query._statement = self._select.from_statement( # noqa: SLF001
@@ -561,7 +561,7 @@ class BaseQuery(Generic[ModelT]):
561
561
  User.query.order_by(request.sort, ignore_case=["name"])
562
562
  ```
563
563
 
564
- Which SQL that becomes is the dialect's to decide, and
564
+ The dialect decides which SQL that becomes, and
565
565
  `CASE_INSENSITIVE_COLLATIONS` names the collation. A cursor cannot page
566
566
  it: use it with `page`.
567
567
 
@@ -948,7 +948,7 @@ class BaseQuery(Generic[ModelT]):
948
948
  def orderable_columns(model: type[Any]) -> Mapping[str, Any]:
949
949
  """Return every mapped column of a model, by name.
950
950
 
951
- What a model orders by when it declares no ``__orderable__``, and what an
951
+ A model orders by these when it declares no ``__orderable__``, and an
952
952
  ``__orderable__`` that adds to the columns rather than replacing them starts
953
953
  from:
954
954
 
@@ -1174,7 +1174,7 @@ def _join_for(
1174
1174
  """Return what to join for a field that is a plain column of another table.
1175
1175
 
1176
1176
  A relationship of the model that reaches that table is the join, condition
1177
- and all, which is what a view with no foreign key needs. Failing that, the
1177
+ and all, as a view with no foreign key needs. Failing that, the
1178
1178
  table itself, and `SQLAlchemy` works the condition out from the key.
1179
1179
 
1180
1180
  An expression may name several tables or none, so it is left alone:
@@ -1409,7 +1409,7 @@ def _from_json(value: Any, type_: sa.types.TypeEngine[Any]) -> Any: # noqa: ANN
1409
1409
 
1410
1410
 
1411
1411
  class _Rows(Protocol[RowT_co]):
1412
- """What the helpers below need of a result: one row, or none."""
1412
+ """The helpers below take one row of a result, or none."""
1413
1413
 
1414
1414
  def one(self) -> RowT_co: ...
1415
1415
 
@@ -4,6 +4,7 @@ import logging
4
4
  import sys
5
5
  import sysconfig
6
6
  import traceback
7
+ import uuid
7
8
  from dataclasses import dataclass, field
8
9
  from itertools import islice
9
10
  from pathlib import Path
@@ -106,7 +107,7 @@ class Statement:
106
107
  class Recording:
107
108
  """The statements of a block, and what they add up to.
108
109
 
109
- What `Database.recording()` hands back:
110
+ `Database.recording()` hands this back:
110
111
 
111
112
  ```python
112
113
  with db.recording() as record:
@@ -118,6 +119,8 @@ class Recording:
118
119
 
119
120
  label: str | None = None
120
121
  statements: list[Statement] = field(default_factory=list)
122
+ id: str = field(default_factory=lambda: uuid.uuid4().hex, repr=False, compare=False)
123
+ """A unique id, so a recording several blocks sent is kept once."""
121
124
 
122
125
  @property
123
126
  def count(self) -> int:
@@ -261,7 +264,7 @@ class Recording:
261
264
  def pretty(self) -> str:
262
265
  """The statements, numbered, each laid out over several lines.
263
266
 
264
- What `print()` shows when the one-line listing has run out of room.
267
+ `print()` shows this when the one-line listing has run out of room.
265
268
  """
266
269
  if not self.statements:
267
270
  return "no queries"
@@ -360,7 +363,7 @@ def check(
360
363
  at_most: int | None,
361
364
  duplicates: bool,
362
365
  ) -> None:
363
- """Fail unless the recording is what the block said it would be.
366
+ """Fail unless the recording matches what the block said.
364
367
 
365
368
  Args:
366
369
  recording: What the block ran.
@@ -402,8 +405,8 @@ def resolved(paths: Sequence[str | PathLike[str]]) -> tuple[str, ...]:
402
405
  def caller_stack(skip: Sequence[str] = (), keep: int = KEEP) -> tuple[str, ...]:
403
406
  """Return the frames of your own code that led to a statement.
404
407
 
405
- Ours and SQLAlchemy's are left out by directory rather than by name: a
406
- project of yours may well live in a path that has our name in it. So is
408
+ This library's and SQLAlchemy's are left out by directory rather than by
409
+ name: a project may well live in a path carrying the library's name. So is
407
410
  generated code, `<string>`: SQLAlchemy builds wrappers that way, and the
408
411
  line numbers lead nowhere.
409
412
 
@@ -37,7 +37,7 @@ class Databases(_DatabaseRegistryMixin[Database], Database):
37
37
  ) -> Iterator[None]:
38
38
  """Open a transaction on every database, not the default one alone.
39
39
 
40
- Where a single database needs `transaction(rollback=True)`, a test harness
40
+ A single database needs `transaction(rollback=True)`. A test harness
41
41
  with several needs this:
42
42
 
43
43
  ```python
@@ -58,7 +58,7 @@ PathLike = str | Path
58
58
 
59
59
 
60
60
  class Templates:
61
- """Where a database's SQL templates live, and how they are rendered.
61
+ """The directory a database's SQL templates live in, and how they render.
62
62
 
63
63
  A path is enough; the object is for the rest:
64
64
 
@@ -173,7 +173,7 @@ class Templates:
173
173
 
174
174
 
175
175
  class BaseSQLQuery(Generic[RowT, DatabaseT]):
176
- """Where the SQL comes from, its context, and what its rows become.
176
+ """The source of the SQL, its context, and the type its rows become.
177
177
 
178
178
  Built by `db.sql(...)`. Nothing can be narrowed: what the SQL selects is
179
179
  what comes back.
@@ -315,7 +315,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
315
315
  let them reach the database on their own, seeing nothing of this
316
316
  block and surviving its rollback.
317
317
  rollback: Roll back on the way out rather than commit. Implies
318
- ``savepoint``, and is what wraps a test.
318
+ ``savepoint``, and wraps a test.
319
319
  commit_on_error: Exception types whose escape still commits. The
320
320
  exception propagates; what was written before it stays.
321
321
  retry_on: Exception types, or a predicate over the exception, worth
@@ -409,7 +409,7 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
409
409
  ) -> AsyncIterator[None]:
410
410
  """Create these tables here, and drop them when the block ends.
411
411
 
412
- What a test session opens once, around everything that needs a schema:
412
+ A test session opens this once, around everything that needs a schema:
413
413
 
414
414
  ```python
415
415
  @pytest.fixture(scope="session")
@@ -418,8 +418,8 @@ class Database(BaseDatabase[AsyncConnection, AsyncSession]):
418
418
  yield
419
419
  ```
420
420
 
421
- Every table of the metadata unless ``tables`` names fewer, which is what a
422
- second database wants.
421
+ Every table of the metadata unless ``tables`` names fewer, as a second
422
+ database wants.
423
423
  """
424
424
  async with self.transaction() as connection:
425
425
  await connection.run_sync(metadata.create_all, tables=tables)
@@ -473,11 +473,11 @@ class Transaction(
473
473
  ):
474
474
  """The transaction an awaited block runs, as an object.
475
475
 
476
- What [`Database.transaction`][sqlakit.asyncio.Database.transaction] returns.
476
+ [`Database.transaction`][sqlakit.asyncio.Database.transaction] returns this.
477
477
 
478
478
  Kept as a class rather than a generator so that it also works as a
479
479
  decorator, and so that a single instance can be entered more than once,
480
- which is what ``@db.transaction()`` on a coroutine function does.
480
+ as ``@db.transaction()`` on a coroutine function does.
481
481
  """
482
482
 
483
483
  def __init__(
@@ -637,8 +637,8 @@ class Transaction(
637
637
  class RetryingTransaction(BaseRetryingTransaction):
638
638
  """A transaction that runs its block again.
639
639
 
640
- What [`Database.transaction`][sqlakit.asyncio.Database.transaction] returns
641
- when it is given ``retry_on``.
640
+ [`Database.transaction`][sqlakit.asyncio.Database.transaction] returns
641
+ this when it is given ``retry_on``.
642
642
  """
643
643
 
644
644
  def __call__(self, func: _FuncT) -> _FuncT:
@@ -37,8 +37,8 @@ class Databases(_DatabaseRegistryMixin[Database], Database):
37
37
  ) -> AsyncIterator[None]:
38
38
  """Open a transaction on every database, not the default one alone.
39
39
 
40
- What a test harness with more than one database needs, in the place a
41
- single database needs `transaction(rollback=True)`.
40
+ A test harness with more than one database needs this, where a single
41
+ database needs `transaction(rollback=True)`.
42
42
  """
43
43
  async with AsyncExitStack() as stack:
44
44
  for alias in self.aliases:
@@ -559,7 +559,7 @@ class ModelMixin(BaseModel[Database]):
559
559
  async def provisioned_tables(cls, alias: str | None = None) -> AsyncIterator[None]:
560
560
  """Create the tables that belong on this model's database, and drop them after.
561
561
 
562
- What a test session opens once, around everything that needs a schema:
562
+ A test session opens this once, around everything that needs a schema:
563
563
 
564
564
  ```python
565
565
  @pytest.fixture(scope="session")
@@ -95,7 +95,7 @@ class SQL:
95
95
 
96
96
  @property
97
97
  def templates(self) -> Templates:
98
- """Where this database looks for its templates."""
98
+ """The directory this database looks in for its templates."""
99
99
  return templates_of(self.db)
100
100
 
101
101
  def check(self) -> None:
@@ -110,7 +110,7 @@ class SQL:
110
110
  class SQLRows(BaseSQLQuery[RowT, "Database"]):
111
111
  """The rows of a SQL template, on the connection of the block it runs in.
112
112
 
113
- What the rows are is settled: reading them is all that is left.
113
+ The rows are settled: reading them is all that is left.
114
114
  """
115
115
 
116
116
  async def all(self) -> Sequence[RowT]:
@@ -196,7 +196,7 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
196
196
  await db.sql("reports/by_team.sql", since=since).typed(TeamReport).all()
197
197
  ```
198
198
 
199
- The type is what one row becomes, and the terminal decides the container.
199
+ The type says what one row becomes, and the terminal decides the container.
200
200
  Anything pydantic can validate works: a model, a dataclass, a
201
201
  `TypedDict`. The type also says how much of the row it takes: one built
202
202
  from columns is given the whole row, and anything else is given the