sqlakit 0.12.0__tar.gz → 0.13.0__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.12.0 → sqlakit-0.13.0}/PKG-INFO +2 -2
- {sqlakit-0.12.0 → sqlakit-0.13.0}/pyproject.toml +4 -4
- {sqlakit-0.12.0 → sqlakit-0.13.0}/pyproject.toml.orig +4 -4
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/__init__.py +2 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_sql.py +63 -15
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/asyncio/sql.py +19 -4
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/pytest_plugin.py +12 -1
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/sql.py +17 -3
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/types.py +17 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/LICENSE +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/README.md +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_cli.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_debugserver.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_model.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_query.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/debugserver.html +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/orm.py +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.12.0 → sqlakit-0.13.0}/sqlakit/testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlakit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: A toolkit for SQLAlchemy applications.
|
|
5
5
|
Keywords: sqlalchemy,database,orm,sql,asyncio
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -22,7 +22,7 @@ Requires-Dist: sqlalchemy>=2.0.22
|
|
|
22
22
|
Requires-Dist: typing-extensions>=4.13
|
|
23
23
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.22 ; extra == 'asyncio'
|
|
24
24
|
Requires-Dist: sqlparse>=0.6.0 ; extra == 'debug'
|
|
25
|
-
Requires-Dist: jinja2sql>=0.
|
|
25
|
+
Requires-Dist: jinja2sql>=0.12.0 ; extra == 'sql'
|
|
26
26
|
Requires-Python: >=3.11
|
|
27
27
|
Project-URL: Repository, https://github.com/antonrh/sqlakit
|
|
28
28
|
Project-URL: Documentation, https://github.com/antonrh/sqlakit
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "A toolkit for SQLAlchemy applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -47,7 +47,7 @@ Documentation = "https://github.com/antonrh/sqlakit"
|
|
|
47
47
|
|
|
48
48
|
[project.optional-dependencies]
|
|
49
49
|
asyncio = ["sqlalchemy[asyncio]>=2.0.22"]
|
|
50
|
-
sql = ["jinja2sql>=0.
|
|
50
|
+
sql = ["jinja2sql>=0.12.0"]
|
|
51
51
|
debug = ["sqlparse>=0.6.0"]
|
|
52
52
|
|
|
53
53
|
[build-system]
|
|
@@ -183,7 +183,7 @@ skip = "./.venv,./site,./uv.lock,./.git,./debugserver/node_modules,./debugserver
|
|
|
183
183
|
[dependency-groups]
|
|
184
184
|
dev = [
|
|
185
185
|
"pytest>=9.1.1",
|
|
186
|
-
"ty>=0.0.
|
|
186
|
+
"ty>=0.0.80",
|
|
187
187
|
"ruff>=0.16.6",
|
|
188
188
|
"poethepoet>=0.48.0",
|
|
189
189
|
"anyio>=4.14.2",
|
|
@@ -194,7 +194,7 @@ dev = [
|
|
|
194
194
|
"httpx>=0.28.1",
|
|
195
195
|
"httpx2>=2.10.0",
|
|
196
196
|
"sqlmodel>=0.0.39",
|
|
197
|
-
"jinja2sql>=0.
|
|
197
|
+
"jinja2sql>=0.12.0",
|
|
198
198
|
"pydantic>=2.13.4",
|
|
199
199
|
"sqlparse>=0.6.0",
|
|
200
200
|
"rich>=15.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "A toolkit for SQLAlchemy applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -41,7 +41,7 @@ asyncio = [
|
|
|
41
41
|
"sqlalchemy[asyncio]>=2.0.22",
|
|
42
42
|
]
|
|
43
43
|
sql = [
|
|
44
|
-
"jinja2sql>=0.
|
|
44
|
+
"jinja2sql>=0.12.0",
|
|
45
45
|
]
|
|
46
46
|
debug = [
|
|
47
47
|
"sqlparse>=0.6.0",
|
|
@@ -61,7 +61,7 @@ default-groups = ["dev", "docs"]
|
|
|
61
61
|
[dependency-groups]
|
|
62
62
|
dev = [
|
|
63
63
|
"pytest>=9.1.1",
|
|
64
|
-
"ty>=0.0.
|
|
64
|
+
"ty>=0.0.80",
|
|
65
65
|
"ruff>=0.16.6",
|
|
66
66
|
"poethepoet>=0.48.0",
|
|
67
67
|
"anyio>=4.14.2",
|
|
@@ -72,7 +72,7 @@ dev = [
|
|
|
72
72
|
"httpx>=0.28.1",
|
|
73
73
|
"httpx2>=2.10.0",
|
|
74
74
|
"sqlmodel>=0.0.39",
|
|
75
|
-
"jinja2sql>=0.
|
|
75
|
+
"jinja2sql>=0.12.0",
|
|
76
76
|
"pydantic>=2.13.4",
|
|
77
77
|
"sqlparse>=0.6.0",
|
|
78
78
|
"rich>=15.0.0",
|
|
@@ -60,6 +60,7 @@ from .types import (
|
|
|
60
60
|
SessionArgs,
|
|
61
61
|
TemplatesLike,
|
|
62
62
|
UrlParts,
|
|
63
|
+
ValidationArgs,
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
__all__ = [
|
|
@@ -122,6 +123,7 @@ __all__ = [
|
|
|
122
123
|
"UnknownOrderFieldError",
|
|
123
124
|
"UnorderedPageError",
|
|
124
125
|
"UrlParts",
|
|
126
|
+
"ValidationArgs",
|
|
125
127
|
"db",
|
|
126
128
|
"import_models",
|
|
127
129
|
"import_string",
|
|
@@ -42,7 +42,13 @@ else:
|
|
|
42
42
|
except ImportError: # pragma: no cover - pydantic is installed in CI
|
|
43
43
|
BaseModel = TypeAdapter = None
|
|
44
44
|
|
|
45
|
-
__all__ = [
|
|
45
|
+
__all__ = [
|
|
46
|
+
"BaseSQLQuery",
|
|
47
|
+
"Filter",
|
|
48
|
+
"Templates",
|
|
49
|
+
"require_pydantic",
|
|
50
|
+
"templates_of",
|
|
51
|
+
]
|
|
46
52
|
|
|
47
53
|
_preparer: ContextVar[Any] = ContextVar("sqlakit.identifier_preparer")
|
|
48
54
|
"""The preparer of the database a template is rendering for."""
|
|
@@ -57,6 +63,38 @@ PathLike = str | Path
|
|
|
57
63
|
"""Where templates are looked for: one directory, or several."""
|
|
58
64
|
|
|
59
65
|
|
|
66
|
+
class Filter:
|
|
67
|
+
"""A template filter, registered the way jinja2sql registers one.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
Templates("app/sql", filters={"in_span": Filter(in_span, bind=True)})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
``bind=True`` calls the filter with a jinja2sql `Binder` as its first
|
|
74
|
+
argument, so a filter writing SQL of its own binds the values through it:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
def in_span(binder, span):
|
|
78
|
+
start, end = span
|
|
79
|
+
return binder.raw(
|
|
80
|
+
f"BETWEEN {binder.bind('span', start)} AND {binder.bind('span', end)}"
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A plain function needs none of this and goes in as it is: whatever it
|
|
85
|
+
returns is bound as one more value of the statement.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
__slots__ = ("bind", "func")
|
|
89
|
+
|
|
90
|
+
def __init__(self, func: Callable[..., Any], *, bind: bool = False) -> None:
|
|
91
|
+
self.func = func
|
|
92
|
+
self.bind = bind
|
|
93
|
+
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return f"{type(self).__name__}({self.func!r}, bind={self.bind})"
|
|
96
|
+
|
|
97
|
+
|
|
60
98
|
class Templates:
|
|
61
99
|
"""The directory a database's SQL templates live in, and how they render.
|
|
62
100
|
|
|
@@ -70,6 +108,10 @@ class Templates:
|
|
|
70
108
|
development server wants and a production one does not. ``filters`` and
|
|
71
109
|
``globals`` are handed to the Jinja environment, and are refused if they have
|
|
72
110
|
to be awaited: rendering makes a string, in both APIs.
|
|
111
|
+
|
|
112
|
+
A filter is a plain function, whose return value is bound as one more value
|
|
113
|
+
of the statement. `Filter(func, bind=True)` registers one that writes SQL of
|
|
114
|
+
its own instead, and is handed a binder for the values inside it.
|
|
73
115
|
"""
|
|
74
116
|
|
|
75
117
|
def __init__(
|
|
@@ -77,7 +119,7 @@ class Templates:
|
|
|
77
119
|
path: PathLike | Sequence[PathLike] = (),
|
|
78
120
|
*,
|
|
79
121
|
auto_reload: bool = False,
|
|
80
|
-
filters: Mapping[str, Callable[..., Any]] | None = None,
|
|
122
|
+
filters: Mapping[str, Callable[..., Any] | Filter] | None = None,
|
|
81
123
|
globals: Mapping[str, Any] | None = None, # noqa: A002
|
|
82
124
|
) -> None:
|
|
83
125
|
self.paths = (
|
|
@@ -87,7 +129,8 @@ class Templates:
|
|
|
87
129
|
self.filters = dict(filters or {})
|
|
88
130
|
self.globals = dict(globals or {})
|
|
89
131
|
for name, value in (*self.filters.items(), *self.globals.items()):
|
|
90
|
-
if
|
|
132
|
+
called = value.func if isinstance(value, Filter) else value
|
|
133
|
+
if iscoroutinefunction(called):
|
|
91
134
|
raise AsyncFilterError(name)
|
|
92
135
|
|
|
93
136
|
def __repr__(self) -> str:
|
|
@@ -109,17 +152,15 @@ class Templates:
|
|
|
109
152
|
autoescape=True,
|
|
110
153
|
)
|
|
111
154
|
environment.globals.update(self.globals)
|
|
112
|
-
#
|
|
113
|
-
# which reads `:name` and nothing else. The driver's own style is
|
|
114
|
-
# SQLAlchemy's business, and a template that picked one would be wrong
|
|
115
|
-
# on the next database.
|
|
155
|
+
# Named parameters: `text()` reads `:name` and nothing else.
|
|
116
156
|
renderer = jinja2sql(environment, param_style=_placeholder)
|
|
117
|
-
# Ours
|
|
118
|
-
# knows both how it quotes and when it has to, which is the difference
|
|
119
|
-
# between `"name"` and `name` on Oracle.
|
|
157
|
+
# Ours quotes through the dialect's preparer, jinja2sql's through one char.
|
|
120
158
|
renderer.register_filter("identifier", _identifier)
|
|
121
159
|
for name, filter_ in self.filters.items():
|
|
122
|
-
|
|
160
|
+
if isinstance(filter_, Filter):
|
|
161
|
+
renderer.register_filter(name, filter_.func, bind=filter_.bind)
|
|
162
|
+
else:
|
|
163
|
+
renderer.register_filter(name, filter_)
|
|
123
164
|
return renderer
|
|
124
165
|
|
|
125
166
|
def render(
|
|
@@ -152,8 +193,7 @@ class Templates:
|
|
|
152
193
|
raise TemplateNotFoundError(source, self.paths) from None
|
|
153
194
|
finally:
|
|
154
195
|
_preparer.reset(token)
|
|
155
|
-
#
|
|
156
|
-
# style would hand back a sequence.
|
|
196
|
+
# Named parameters come back as a mapping, positional ones as a sequence.
|
|
157
197
|
return sql, cast("Mapping[str, Any]", params)
|
|
158
198
|
|
|
159
199
|
def check(self) -> None:
|
|
@@ -188,6 +228,7 @@ class BaseSQLQuery(Generic[RowT, DatabaseT]):
|
|
|
188
228
|
inline: bool = False,
|
|
189
229
|
type_: type[Any] | None = None,
|
|
190
230
|
scalar: bool = False,
|
|
231
|
+
validation: Mapping[str, Any] | None = None,
|
|
191
232
|
) -> None:
|
|
192
233
|
self.db = db
|
|
193
234
|
self.source = source
|
|
@@ -195,6 +236,7 @@ class BaseSQLQuery(Generic[RowT, DatabaseT]):
|
|
|
195
236
|
self.inline = inline
|
|
196
237
|
self.type = type_
|
|
197
238
|
self.scalar = scalar
|
|
239
|
+
self.validation = dict(validation or {})
|
|
198
240
|
|
|
199
241
|
def __repr__(self) -> str:
|
|
200
242
|
return f"{type(self).__name__}({self.source!r})"
|
|
@@ -237,6 +279,7 @@ class BaseSQLQuery(Generic[RowT, DatabaseT]):
|
|
|
237
279
|
"inline": self.inline,
|
|
238
280
|
"type_": self.type,
|
|
239
281
|
"scalar": self.scalar,
|
|
282
|
+
"validation": self.validation,
|
|
240
283
|
**changes,
|
|
241
284
|
}
|
|
242
285
|
return query(self.db, self.source, self.context, **arguments)
|
|
@@ -245,12 +288,17 @@ class BaseSQLQuery(Generic[RowT, DatabaseT]):
|
|
|
245
288
|
if self.type is None:
|
|
246
289
|
return rows
|
|
247
290
|
adapter = _adapter(self.type)
|
|
248
|
-
return [
|
|
291
|
+
return [
|
|
292
|
+
adapter.validate_python(_as_python(row, self.type), **self.validation)
|
|
293
|
+
for row in rows
|
|
294
|
+
]
|
|
249
295
|
|
|
250
296
|
def _shaped_one(self, row: Any) -> Any: # noqa: ANN401
|
|
251
297
|
if self.type is None or row is None:
|
|
252
298
|
return row
|
|
253
|
-
return _adapter(self.type).validate_python(
|
|
299
|
+
return _adapter(self.type).validate_python(
|
|
300
|
+
_as_python(row, self.type), **self.validation
|
|
301
|
+
)
|
|
254
302
|
|
|
255
303
|
def _executable(self, *, size: int | None = None) -> Executable:
|
|
256
304
|
if size is None:
|
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
4
4
|
|
|
5
5
|
import sqlalchemy as sa
|
|
6
|
+
from typing_extensions import Unpack
|
|
6
7
|
|
|
7
8
|
from sqlakit._sql import (
|
|
8
9
|
BaseSQLQuery,
|
|
10
|
+
Filter,
|
|
9
11
|
Templates,
|
|
10
12
|
require_pydantic,
|
|
11
13
|
templates_of,
|
|
@@ -18,9 +20,11 @@ if TYPE_CHECKING:
|
|
|
18
20
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
19
21
|
from sqlalchemy.sql import Executable
|
|
20
22
|
|
|
23
|
+
from sqlakit.types import ValidationArgs
|
|
24
|
+
|
|
21
25
|
from ._db import Database
|
|
22
26
|
|
|
23
|
-
__all__ = ["SQL", "SQLQuery", "SQLRows", "Templates"]
|
|
27
|
+
__all__ = ["SQL", "Filter", "SQLQuery", "SQLRows", "Templates"]
|
|
24
28
|
|
|
25
29
|
RowT = TypeVar("RowT")
|
|
26
30
|
OtherT = TypeVar("OtherT")
|
|
@@ -189,11 +193,13 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
|
|
|
189
193
|
same way and carry no further say, so each is asked once.
|
|
190
194
|
"""
|
|
191
195
|
|
|
192
|
-
def typed(
|
|
196
|
+
def typed(
|
|
197
|
+
self, type_: type[OtherT], /, **validation: Unpack[ValidationArgs]
|
|
198
|
+
) -> SQLRows[OtherT]:
|
|
193
199
|
"""Read the rows as this type, one row at a time.
|
|
194
200
|
|
|
195
201
|
```python
|
|
196
|
-
|
|
202
|
+
db.sql("reports/by_team.sql", since=since).typed(TeamReport).all()
|
|
197
203
|
```
|
|
198
204
|
|
|
199
205
|
The type says what one row becomes, and the terminal decides the container.
|
|
@@ -202,12 +208,21 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
|
|
|
202
208
|
from columns is given the whole row, and anything else is given the
|
|
203
209
|
first column, so `SELECT count(*)` with `typed(int)` reads as an `int`.
|
|
204
210
|
|
|
211
|
+
Keyword arguments go to pydantic's `validate_python`, `context` and
|
|
212
|
+
`strict` among them:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
db.sql("reports/by_team.sql").typed(TeamReport, context={"tz": tz}).all()
|
|
216
|
+
```
|
|
217
|
+
|
|
205
218
|
Raises:
|
|
206
219
|
MissingDependencyError: if pydantic is not installed.
|
|
207
220
|
|
|
208
221
|
"""
|
|
209
222
|
require_pydantic()
|
|
210
|
-
return cast(
|
|
223
|
+
return cast(
|
|
224
|
+
"SQLRows[OtherT]", self._as(SQLRows, type_=type_, validation=validation)
|
|
225
|
+
)
|
|
211
226
|
|
|
212
227
|
def scalars(self) -> SQLRows[Any]:
|
|
213
228
|
"""Read the first column of each row instead of whole rows.
|
|
@@ -286,6 +286,12 @@ def _sqlakit_transaction(
|
|
|
286
286
|
) -> Iterator[None]:
|
|
287
287
|
with ExitStack() as stack:
|
|
288
288
|
for block in _rolled_back(sqlakit_db, _asked_for(request)):
|
|
289
|
+
if hasattr(block, "__aenter__"):
|
|
290
|
+
pytest.fail(
|
|
291
|
+
f"`{request.node.name}` runs on an async database, whose "
|
|
292
|
+
"blocks are awaited. Write the test as `async def`.",
|
|
293
|
+
pytrace=False,
|
|
294
|
+
)
|
|
289
295
|
stack.enter_context(block)
|
|
290
296
|
if _hidden(request):
|
|
291
297
|
stack.enter_context(sqlakit_db.unbound())
|
|
@@ -301,7 +307,12 @@ async def _sqlakit_async_transaction(
|
|
|
301
307
|
) -> AsyncIterator[None]:
|
|
302
308
|
async with AsyncExitStack() as stack:
|
|
303
309
|
for block in _rolled_back(sqlakit_db, _asked_for(request)):
|
|
304
|
-
|
|
310
|
+
# A synchronous database under a test that awaits something else,
|
|
311
|
+
# a handler it runs in a worker thread among them.
|
|
312
|
+
if hasattr(block, "__aenter__"):
|
|
313
|
+
await stack.enter_async_context(block)
|
|
314
|
+
else:
|
|
315
|
+
stack.enter_context(block)
|
|
305
316
|
if _hidden(request):
|
|
306
317
|
stack.enter_context(sqlakit_db.unbound())
|
|
307
318
|
with _reported(request, sqlakit_db):
|
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
4
4
|
|
|
5
5
|
import sqlalchemy as sa
|
|
6
|
+
from typing_extensions import Unpack
|
|
6
7
|
|
|
7
8
|
from ._sql import (
|
|
8
9
|
BaseSQLQuery,
|
|
10
|
+
Filter,
|
|
9
11
|
Templates,
|
|
10
12
|
require_pydantic,
|
|
11
13
|
templates_of,
|
|
@@ -18,8 +20,9 @@ if TYPE_CHECKING:
|
|
|
18
20
|
from sqlalchemy.sql import Executable
|
|
19
21
|
|
|
20
22
|
from ._db import Database
|
|
23
|
+
from .types import ValidationArgs
|
|
21
24
|
|
|
22
|
-
__all__ = ["SQL", "SQLQuery", "SQLRows", "Templates"]
|
|
25
|
+
__all__ = ["SQL", "Filter", "SQLQuery", "SQLRows", "Templates"]
|
|
23
26
|
|
|
24
27
|
RowT = TypeVar("RowT")
|
|
25
28
|
OtherT = TypeVar("OtherT")
|
|
@@ -178,7 +181,9 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
|
|
|
178
181
|
same way and carry no further say, so each is asked once.
|
|
179
182
|
"""
|
|
180
183
|
|
|
181
|
-
def typed(
|
|
184
|
+
def typed(
|
|
185
|
+
self, type_: type[OtherT], /, **validation: Unpack[ValidationArgs]
|
|
186
|
+
) -> SQLRows[OtherT]:
|
|
182
187
|
"""Read the rows as this type, one row at a time.
|
|
183
188
|
|
|
184
189
|
```python
|
|
@@ -191,12 +196,21 @@ class SQLQuery(SQLRows[sa.Row[Any]]):
|
|
|
191
196
|
from columns is given the whole row, and anything else is given the
|
|
192
197
|
first column, so `SELECT count(*)` with `typed(int)` reads as an `int`.
|
|
193
198
|
|
|
199
|
+
Keyword arguments go to pydantic's `validate_python`, `context` and
|
|
200
|
+
`strict` among them:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
db.sql("reports/by_team.sql").typed(TeamReport, context={"tz": tz}).all()
|
|
204
|
+
```
|
|
205
|
+
|
|
194
206
|
Raises:
|
|
195
207
|
MissingDependencyError: if pydantic is not installed.
|
|
196
208
|
|
|
197
209
|
"""
|
|
198
210
|
require_pydantic()
|
|
199
|
-
return cast(
|
|
211
|
+
return cast(
|
|
212
|
+
"SQLRows[OtherT]", self._as(SQLRows, type_=type_, validation=validation)
|
|
213
|
+
)
|
|
200
214
|
|
|
201
215
|
def scalars(self) -> SQLRows[Any]:
|
|
202
216
|
"""Read the first column of each row instead of whole rows.
|
|
@@ -21,6 +21,7 @@ __all__ = [
|
|
|
21
21
|
"SessionArgs",
|
|
22
22
|
"TemplatesLike",
|
|
23
23
|
"UrlParts",
|
|
24
|
+
"ValidationArgs",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
# Quoted, so importing this module never reaches `Templates` and the
|
|
@@ -66,6 +67,22 @@ class EngineArgs(TypedDict, total=False, extra_items=Any):
|
|
|
66
67
|
use_insertmanyvalues: bool
|
|
67
68
|
|
|
68
69
|
|
|
70
|
+
class ValidationArgs(TypedDict, total=False, extra_items=Any):
|
|
71
|
+
"""Keyword arguments accepted by [`pydantic.TypeAdapter.validate_python`](https://docs.pydantic.dev/latest/api/type_adapter/#pydantic.type_adapter.TypeAdapter.validate_python).
|
|
72
|
+
|
|
73
|
+
`typed()` hands them to pydantic for every row it reads. A keyword a later
|
|
74
|
+
pydantic adds is allowed too.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
strict: bool | None
|
|
78
|
+
extra: Literal["allow", "ignore", "forbid"] | None
|
|
79
|
+
from_attributes: bool | None
|
|
80
|
+
context: Any
|
|
81
|
+
experimental_allow_partial: bool | Literal["off", "on", "trailing-strings"]
|
|
82
|
+
by_alias: bool | None
|
|
83
|
+
by_name: bool | None
|
|
84
|
+
|
|
85
|
+
|
|
69
86
|
class SessionArgs(TypedDict, total=False, extra_items=Any):
|
|
70
87
|
"""Keyword arguments accepted by `sqlalchemy.orm.sessionmaker`.
|
|
71
88
|
|
|
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
|