snake-orm 0.1.0b1__py3-none-any.whl
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.
- snake_orm-0.1.0b1.dist-info/METADATA +366 -0
- snake_orm-0.1.0b1.dist-info/RECORD +198 -0
- snake_orm-0.1.0b1.dist-info/WHEEL +4 -0
- snake_orm-0.1.0b1.dist-info/entry_points.txt +2 -0
- snake_orm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
- snakeorm/__init__.py +214 -0
- snakeorm/advisor.py +109 -0
- snakeorm/cli/__init__.py +3 -0
- snakeorm/cli/app.py +992 -0
- snakeorm/cli/discovery.py +179 -0
- snakeorm/cli/hooks.py +86 -0
- snakeorm/compiler/__init__.py +3 -0
- snakeorm/compiler/compiler.py +330 -0
- snakeorm/connection.py +228 -0
- snakeorm/contrib/__init__.py +17 -0
- snakeorm/contrib/asgi.py +192 -0
- snakeorm/contrib/config.py +264 -0
- snakeorm/contrib/deliver.py +256 -0
- snakeorm/contrib/django.py +245 -0
- snakeorm/contrib/sidecar.py +44 -0
- snakeorm/contrib/wsgi.py +176 -0
- snakeorm/core/__init__.py +1 -0
- snakeorm/core/config.py +173 -0
- snakeorm/core/converters.py +172 -0
- snakeorm/core/exceptions.py +175 -0
- snakeorm/core/placement.py +15 -0
- snakeorm/core/sentinels.py +37 -0
- snakeorm/core/signals.py +114 -0
- snakeorm/debug/__init__.py +42 -0
- snakeorm/debug/assets/css/panel.css +616 -0
- snakeorm/debug/assets/css/snake_orm_app.css +293 -0
- snakeorm/debug/assets/img/icon.svg +5 -0
- snakeorm/debug/assets/js/history.js +425 -0
- snakeorm/debug/assets/js/language.js +283 -0
- snakeorm/debug/assets/js/panel.js +241 -0
- snakeorm/debug/assets/js/snake_orm_app.js +82 -0
- snakeorm/debug/assets/pages/cli.html +28 -0
- snakeorm/debug/assets/pages/config.html +34 -0
- snakeorm/debug/assets/pages/database_first.html +29 -0
- snakeorm/debug/assets/pages/help.html +21 -0
- snakeorm/debug/assets/pages/history.html +27 -0
- snakeorm/debug/capture.py +234 -0
- snakeorm/debug/channel.py +131 -0
- snakeorm/debug/collector.py +144 -0
- snakeorm/debug/config.py +149 -0
- snakeorm/debug/html.py +578 -0
- snakeorm/debug/matrix.py +60 -0
- snakeorm/debug/origin.py +42 -0
- snakeorm/debug/otel/__init__.py +68 -0
- snakeorm/debug/otel/context.py +43 -0
- snakeorm/debug/otel/exporter.py +277 -0
- snakeorm/debug/otel/payload.py +97 -0
- snakeorm/debug/otel/report.py +29 -0
- snakeorm/debug/otel/spans.py +393 -0
- snakeorm/debug/otel/summary.py +59 -0
- snakeorm/debug/record.py +54 -0
- snakeorm/debug/report.py +311 -0
- snakeorm/debug/testing.py +23 -0
- snakeorm/decorators/__init__.py +13 -0
- snakeorm/decorators/abstract.py +26 -0
- snakeorm/decorators/db_first.py +104 -0
- snakeorm/decorators/function.py +28 -0
- snakeorm/decorators/model.py +268 -0
- snakeorm/decorators/polymorphic.py +140 -0
- snakeorm/decorators/result.py +167 -0
- snakeorm/decorators/row.py +90 -0
- snakeorm/decorators/trigger.py +47 -0
- snakeorm/decorators/view.py +149 -0
- snakeorm/dialects/__init__.py +6 -0
- snakeorm/dialects/base.py +373 -0
- snakeorm/dialects/capabilities.py +571 -0
- snakeorm/dialects/literals.py +46 -0
- snakeorm/dialects/matrix.py +384 -0
- snakeorm/dialects/mysql.py +519 -0
- snakeorm/dialects/postgres.py +504 -0
- snakeorm/dialects/sqlite.py +417 -0
- snakeorm/drivers/__init__.py +18 -0
- snakeorm/drivers/asyncbase.py +73 -0
- snakeorm/drivers/asyncdecorators.py +244 -0
- snakeorm/drivers/asyncpool.py +228 -0
- snakeorm/drivers/asyncpsycopg.py +124 -0
- snakeorm/drivers/asyncpymysql.py +50 -0
- snakeorm/drivers/asyncsqlite.py +30 -0
- snakeorm/drivers/base.py +76 -0
- snakeorm/drivers/failures.py +181 -0
- snakeorm/drivers/logging.py +167 -0
- snakeorm/drivers/pool.py +352 -0
- snakeorm/drivers/psycopg.py +188 -0
- snakeorm/drivers/pymysql.py +176 -0
- snakeorm/drivers/savepoints.py +46 -0
- snakeorm/drivers/sqlite.py +170 -0
- snakeorm/drivers/threaded.py +143 -0
- snakeorm/drivers/timeout.py +87 -0
- snakeorm/dto/__init__.py +55 -0
- snakeorm/dto/read.py +231 -0
- snakeorm/dto/region.py +326 -0
- snakeorm/dto/resolve.py +354 -0
- snakeorm/dto/spec.py +201 -0
- snakeorm/expressions/__init__.py +88 -0
- snakeorm/expressions/conditional.py +178 -0
- snakeorm/expressions/expression.py +810 -0
- snakeorm/expressions/functions.py +71 -0
- snakeorm/expressions/keys.py +299 -0
- snakeorm/expressions/paths.py +43 -0
- snakeorm/expressions/scalar.py +293 -0
- snakeorm/expressions/window.py +248 -0
- snakeorm/fields/__init__.py +68 -0
- snakeorm/fields/check.py +79 -0
- snakeorm/fields/column.py +316 -0
- snakeorm/fields/enum.py +64 -0
- snakeorm/fields/index.py +46 -0
- snakeorm/fields/relationship.py +738 -0
- snakeorm/fields/typed.py +736 -0
- snakeorm/helpers/__init__.py +1 -0
- snakeorm/helpers/annotations.py +21 -0
- snakeorm/helpers/inheritance.py +41 -0
- snakeorm/helpers/pyliteral.py +52 -0
- snakeorm/helpers/pytype.py +50 -0
- snakeorm/introspection/__init__.py +13 -0
- snakeorm/introspection/base.py +30 -0
- snakeorm/introspection/drift.py +78 -0
- snakeorm/introspection/models.py +633 -0
- snakeorm/introspection/mysql.py +367 -0
- snakeorm/introspection/postgres.py +397 -0
- snakeorm/introspection/sqlite.py +217 -0
- snakeorm/introspection/unsupported.py +94 -0
- snakeorm/linker/__init__.py +3 -0
- snakeorm/linker/linker.py +521 -0
- snakeorm/metadata/__init__.py +41 -0
- snakeorm/metadata/check.py +37 -0
- snakeorm/metadata/column.py +201 -0
- snakeorm/metadata/enum_storage.py +33 -0
- snakeorm/metadata/fk_action.py +19 -0
- snakeorm/metadata/foreign_key.py +26 -0
- snakeorm/metadata/index.py +43 -0
- snakeorm/metadata/index_method.py +19 -0
- snakeorm/metadata/int_size.py +20 -0
- snakeorm/metadata/json_storage.py +21 -0
- snakeorm/metadata/polymorphic.py +30 -0
- snakeorm/metadata/primary_key.py +23 -0
- snakeorm/metadata/relationship.py +60 -0
- snakeorm/metadata/relationship_kind.py +43 -0
- snakeorm/metadata/routine.py +26 -0
- snakeorm/metadata/server_default.py +20 -0
- snakeorm/metadata/table.py +97 -0
- snakeorm/metadata/table_kind.py +23 -0
- snakeorm/metadata/trigger.py +62 -0
- snakeorm/metadata/type_params.py +348 -0
- snakeorm/migration/__init__.py +88 -0
- snakeorm/migration/asyncrunner.py +168 -0
- snakeorm/migration/autodetect.py +96 -0
- snakeorm/migration/ddl.py +1172 -0
- snakeorm/migration/diff.py +701 -0
- snakeorm/migration/loader.py +77 -0
- snakeorm/migration/operations.py +952 -0
- snakeorm/migration/planner.py +78 -0
- snakeorm/migration/realize.py +420 -0
- snakeorm/migration/renames.py +153 -0
- snakeorm/migration/render.py +1095 -0
- snakeorm/migration/runner.py +314 -0
- snakeorm/migration/squash.py +60 -0
- snakeorm/migration/state.py +107 -0
- snakeorm/model.py +162 -0
- snakeorm/py.typed +0 -0
- snakeorm/query/__init__.py +9 -0
- snakeorm/query/compound.py +288 -0
- snakeorm/query/join_kind.py +19 -0
- snakeorm/query/joined.py +143 -0
- snakeorm/query/query.py +1001 -0
- snakeorm/query/recursive.py +244 -0
- snakeorm/registry/__init__.py +5 -0
- snakeorm/registry/by_module.py +163 -0
- snakeorm/registry/registry.py +166 -0
- snakeorm/session/__init__.py +8 -0
- snakeorm/session/asyncsession.py +638 -0
- snakeorm/session/coercion.py +306 -0
- snakeorm/session/factory.py +29 -0
- snakeorm/session/guards.py +269 -0
- snakeorm/session/isolation.py +23 -0
- snakeorm/session/mapper.py +213 -0
- snakeorm/session/planning.py +815 -0
- snakeorm/session/retry.py +96 -0
- snakeorm/session/session.py +871 -0
- snakeorm/session/shared.py +355 -0
- snakeorm/sql/__init__.py +18 -0
- snakeorm/sql/adapt.py +104 -0
- snakeorm/sql/aggregate.py +232 -0
- snakeorm/sql/condition.py +359 -0
- snakeorm/sql/delete.py +46 -0
- snakeorm/sql/insert.py +149 -0
- snakeorm/sql/joins.py +158 -0
- snakeorm/sql/pk_subquery.py +54 -0
- snakeorm/sql/refs.py +19 -0
- snakeorm/sql/resolver.py +28 -0
- snakeorm/sql/select.py +175 -0
- snakeorm/sql/update.py +82 -0
- snakeorm/sql/value.py +552 -0
- snakeorm/times.py +276 -0
snakeorm/dto/resolve.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Turning declared specs into the SHAPES that get written: annotations, nesting, and write order.
|
|
2
|
+
|
|
3
|
+
The metadata graph has the last word here. A spec carries paths, which are names; what a name MEANS
|
|
4
|
+
— a column of this type, a to-one that may find nothing, a collection — is a question only the
|
|
5
|
+
compiled `SnakeTableInfo` can answer, and asking it is this module's whole job.
|
|
6
|
+
|
|
7
|
+
Three rules, each of which has a way of being quietly wrong:
|
|
8
|
+
|
|
9
|
+
NULLABILITY is accumulated over the WHOLE path and never read off the last step. One nullable key
|
|
10
|
+
anywhere along the way means the value can arrive as `None`, however NOT NULL the column at the end
|
|
11
|
+
is. `editor.username` is `str | None`; so is `author.country.name`, where the only nullable hop is
|
|
12
|
+
in the middle and both ends are required.
|
|
13
|
+
|
|
14
|
+
NESTING resolves to another SPEC, never to the far model's columns. Zero specs for that model is an
|
|
15
|
+
error; two is an error naming both. Picking whichever is bug #14 in another costume — it does not
|
|
16
|
+
fail, it publishes the wrong shape.
|
|
17
|
+
|
|
18
|
+
ORDER is by DEPENDENCY. Python reads a file top to bottom, so a class that mentions another has to
|
|
19
|
+
come after it, whichever order the two declarations were written in. A cycle has no such order, so
|
|
20
|
+
it is named and refused rather than resolved into a file that does not import.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Sequence
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
from snakeorm.core.exceptions import SnakeDtoError
|
|
29
|
+
from snakeorm.dto.spec import SnakeDtoPick, SnakeDtoSpec
|
|
30
|
+
from snakeorm.helpers.pytype import type_annotation
|
|
31
|
+
from snakeorm.metadata import (
|
|
32
|
+
SnakeColumnInfo,
|
|
33
|
+
SnakeRelationshipInfo,
|
|
34
|
+
SnakeRelationshipKind,
|
|
35
|
+
SnakeTableInfo,
|
|
36
|
+
)
|
|
37
|
+
from snakeorm.registry import registry_of
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class SnakeDtoField:
|
|
42
|
+
"""One key of a generated class: its name, its annotation as SOURCE, and the import it needs."""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
annotation: str
|
|
46
|
+
requires: str | None = None
|
|
47
|
+
"""The module that has to be in scope for `annotation` to resolve, or `None` for a builtin.
|
|
48
|
+
|
|
49
|
+
It travels with the field because the generator writes into somebody's file and never writes
|
|
50
|
+
their imports: without this the writer could not refuse a field it cannot spell there.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class SnakeDtoShape:
|
|
56
|
+
"""One class ready to be written: its name and its keys, in the order they go in."""
|
|
57
|
+
|
|
58
|
+
name: str
|
|
59
|
+
fields: tuple[SnakeDtoField, ...]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_all(specs: Sequence[SnakeDtoSpec]) -> tuple[SnakeDtoShape, ...]:
|
|
63
|
+
"""Every spec of one file resolved together, in the order they have to be WRITTEN in.
|
|
64
|
+
|
|
65
|
+
Together and not one at a time, because a nested field points at another spec: a DTO cannot be
|
|
66
|
+
resolved without the set it belongs to.
|
|
67
|
+
"""
|
|
68
|
+
_guard_unique_names(specs)
|
|
69
|
+
by_name = {spec.name: spec for spec in specs}
|
|
70
|
+
shapes: dict[str, SnakeDtoShape] = {}
|
|
71
|
+
needs: dict[str, set[str]] = {}
|
|
72
|
+
for spec in specs:
|
|
73
|
+
fields, depends = _shape_of(spec, specs, by_name)
|
|
74
|
+
shapes[spec.name] = SnakeDtoShape(name=spec.name, fields=fields)
|
|
75
|
+
needs[spec.name] = depends
|
|
76
|
+
return tuple(shapes[name] for name in _write_order(specs, needs))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _guard_unique_names(specs: Sequence[SnakeDtoSpec]) -> None:
|
|
80
|
+
"""Two specs of one name in one file would be two classes of one name: the second wins, silently."""
|
|
81
|
+
seen: set[str] = set()
|
|
82
|
+
for spec in specs:
|
|
83
|
+
if spec.name in seen:
|
|
84
|
+
raise SnakeDtoError(
|
|
85
|
+
f"{spec.name!r} is declared twice in this file. Two classes of one name means the "
|
|
86
|
+
f"second silently replaces the first: give one of them a name of its own."
|
|
87
|
+
)
|
|
88
|
+
seen.add(spec.name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _write_order(
|
|
92
|
+
specs: Sequence[SnakeDtoSpec], needs: dict[str, set[str]]
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
"""Names sorted so that everything a class nests is defined above it. Declaration order breaks ties.
|
|
95
|
+
|
|
96
|
+
A plain depth-first walk with a colour mark, which is what makes the CYCLE reportable: the grey
|
|
97
|
+
set IS the stack, so the names in it when an edge points back are exactly the loop, in order.
|
|
98
|
+
Ties keep the declaration order, so two classes that depend on nothing do not get reshuffled —
|
|
99
|
+
a sort that churned independent classes would rewrite the region for no reason, and the diff is
|
|
100
|
+
the product here.
|
|
101
|
+
"""
|
|
102
|
+
order: list[str] = []
|
|
103
|
+
done: set[str] = set()
|
|
104
|
+
stack: list[str] = []
|
|
105
|
+
|
|
106
|
+
def visit(name: str) -> None:
|
|
107
|
+
if name in done:
|
|
108
|
+
return
|
|
109
|
+
if name in stack:
|
|
110
|
+
loop = [*stack[stack.index(name) :], name]
|
|
111
|
+
raise SnakeDtoError(
|
|
112
|
+
f"these DTOs nest each other in a cycle: {' -> '.join(loop)}. There is no order "
|
|
113
|
+
f"that defines each one before it is mentioned, so nothing was written: break the "
|
|
114
|
+
f"loop by dropping one of the nested fields."
|
|
115
|
+
)
|
|
116
|
+
stack.append(name)
|
|
117
|
+
for nested in sorted(needs.get(name, set())):
|
|
118
|
+
visit(nested)
|
|
119
|
+
stack.pop()
|
|
120
|
+
done.add(name)
|
|
121
|
+
order.append(name)
|
|
122
|
+
|
|
123
|
+
for spec in specs:
|
|
124
|
+
visit(spec.name)
|
|
125
|
+
return order
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _shape_of(
|
|
129
|
+
spec: SnakeDtoSpec,
|
|
130
|
+
specs: Sequence[SnakeDtoSpec],
|
|
131
|
+
by_name: dict[str, SnakeDtoSpec],
|
|
132
|
+
) -> tuple[tuple[SnakeDtoField, ...], set[str]]:
|
|
133
|
+
"""One spec's fields, and the names of the DTOs it nests."""
|
|
134
|
+
table = _table_of(spec.model)
|
|
135
|
+
fields: list[SnakeDtoField] = []
|
|
136
|
+
depends: set[str] = set()
|
|
137
|
+
taken: set[str] = set()
|
|
138
|
+
for pick in _picks(spec, table):
|
|
139
|
+
field, nested = _field_of(pick, spec, table, specs, by_name)
|
|
140
|
+
if field.name in taken:
|
|
141
|
+
raise SnakeDtoError(
|
|
142
|
+
f"{spec.name} would carry the key {field.name!r} twice: two of its fields resolve "
|
|
143
|
+
f"to the same name. A generated key is named after its whole path, so rename the "
|
|
144
|
+
f"column or drop one of the two."
|
|
145
|
+
)
|
|
146
|
+
taken.add(field.name)
|
|
147
|
+
fields.append(field)
|
|
148
|
+
if nested is not None:
|
|
149
|
+
depends.add(nested)
|
|
150
|
+
return tuple(fields), depends
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _picks(spec: SnakeDtoSpec, table: SnakeTableInfo) -> list[SnakeDtoPick]:
|
|
154
|
+
"""What the spec selects: what it named, or every column when it named nothing.
|
|
155
|
+
|
|
156
|
+
Neither switch is every COLUMN and no relationship. Nesting by default would drag an object
|
|
157
|
+
graph into a response nobody asked for, and could not be written at all without a spec for every
|
|
158
|
+
model it reached.
|
|
159
|
+
"""
|
|
160
|
+
if spec.fields is not None:
|
|
161
|
+
return list(spec.fields)
|
|
162
|
+
dropped = {path[0] for path in spec.exclude}
|
|
163
|
+
return [
|
|
164
|
+
SnakeDtoPick(path=(_attribute(column.attr_name, column.name),))
|
|
165
|
+
for column in table.columns
|
|
166
|
+
if _attribute(column.attr_name, column.name) not in dropped
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _field_of(
|
|
171
|
+
pick: SnakeDtoPick,
|
|
172
|
+
spec: SnakeDtoSpec,
|
|
173
|
+
table: SnakeTableInfo,
|
|
174
|
+
specs: Sequence[SnakeDtoSpec],
|
|
175
|
+
by_name: dict[str, SnakeDtoSpec],
|
|
176
|
+
) -> tuple[SnakeDtoField, str | None]:
|
|
177
|
+
"""One selected path resolved into a field, plus the DTO it nests if it nests one."""
|
|
178
|
+
landing, optional = _walk(table, spec.model, pick.path, spec.name)
|
|
179
|
+
name = "_".join(pick.path)
|
|
180
|
+
if isinstance(landing, SnakeRelationshipInfo):
|
|
181
|
+
nested = _nested_name(landing, pick, spec, specs, by_name)
|
|
182
|
+
many = landing.kind is not SnakeRelationshipKind.TO_ONE
|
|
183
|
+
annotation = f"list[{nested}]" if many else nested
|
|
184
|
+
# A collection is never `None`: a parent with no children gets `[]`. What CAN vanish is the
|
|
185
|
+
# collection's owner, when the path crossed a nullable key to reach it.
|
|
186
|
+
if optional:
|
|
187
|
+
annotation = f"{annotation} | None"
|
|
188
|
+
return SnakeDtoField(name=name, annotation=annotation), nested
|
|
189
|
+
spelled, module = type_annotation(landing.python_type)
|
|
190
|
+
nullable = landing.nullable or optional
|
|
191
|
+
return (
|
|
192
|
+
SnakeDtoField(
|
|
193
|
+
name=name,
|
|
194
|
+
annotation=f"{spelled} | None" if nullable else spelled,
|
|
195
|
+
requires=module,
|
|
196
|
+
),
|
|
197
|
+
None,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _nested_name(
|
|
202
|
+
relationship: SnakeRelationshipInfo,
|
|
203
|
+
pick: SnakeDtoPick,
|
|
204
|
+
spec: SnakeDtoSpec,
|
|
205
|
+
specs: Sequence[SnakeDtoSpec],
|
|
206
|
+
by_name: dict[str, SnakeDtoSpec],
|
|
207
|
+
) -> str:
|
|
208
|
+
"""Which DTO a relationship field nests: the one the user named, or the only candidate.
|
|
209
|
+
|
|
210
|
+
"The only" is load-bearing. Two specs over one model and no rule to choose between them is the
|
|
211
|
+
same defect as resolving a model by class name — it produces a valid-looking class describing
|
|
212
|
+
the wrong shape, and nothing anywhere goes red.
|
|
213
|
+
"""
|
|
214
|
+
reg = registry_of(spec.model)
|
|
215
|
+
target = reg.resolve_relationship(relationship)[1]
|
|
216
|
+
if target is None:
|
|
217
|
+
raise SnakeDtoError(
|
|
218
|
+
f"{spec.name} nests {relationship.name!r}, whose target {relationship.target!r} is not "
|
|
219
|
+
f"registered. Import the module that declares it, and call `snake_link()`."
|
|
220
|
+
)
|
|
221
|
+
candidates = [item.name for item in specs if item.model is target]
|
|
222
|
+
if pick.dto is not None:
|
|
223
|
+
named = by_name.get(pick.dto)
|
|
224
|
+
if named is None:
|
|
225
|
+
raise SnakeDtoError(
|
|
226
|
+
f"{spec.name} nests {relationship.name!r} as {pick.dto!r}, and no spec of that "
|
|
227
|
+
f"name is declared in this file. Over {target.__name__} there is: "
|
|
228
|
+
f"{', '.join(candidates) or '(none)'}."
|
|
229
|
+
)
|
|
230
|
+
if named.model is not target:
|
|
231
|
+
raise SnakeDtoError(
|
|
232
|
+
f"{spec.name} nests {relationship.name!r} as {pick.dto!r}, which describes "
|
|
233
|
+
f"{named.model.__name__} and not {target.__name__}. It would type-check and it "
|
|
234
|
+
f"would put another table's fields in this response."
|
|
235
|
+
)
|
|
236
|
+
return named.name
|
|
237
|
+
if not candidates:
|
|
238
|
+
raise SnakeDtoError(
|
|
239
|
+
f"{spec.name} nests {relationship.name!r}, and no spec describes {target.__name__}. A "
|
|
240
|
+
f"relationship is written as the DTO of its model, so declare one: "
|
|
241
|
+
f"`snake_dto({target.__name__}, fields=[...], name=...)`."
|
|
242
|
+
)
|
|
243
|
+
if len(candidates) > 1:
|
|
244
|
+
listed = ", ".join(sorted(candidates))
|
|
245
|
+
raise SnakeDtoError(
|
|
246
|
+
f"{spec.name} nests {relationship.name!r} and {target.__name__} has more than one "
|
|
247
|
+
f"spec ({listed}), so there is no telling which shape this field is. Nothing is "
|
|
248
|
+
f"picked: say which, with "
|
|
249
|
+
f"`({spec.model.__name__}.{relationship.name}, {sorted(candidates)[0]!r})`."
|
|
250
|
+
)
|
|
251
|
+
return candidates[0]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _walk(
|
|
255
|
+
table: SnakeTableInfo, model: type, path: tuple[str, ...], dto: str
|
|
256
|
+
) -> tuple[SnakeColumnInfo | SnakeRelationshipInfo, bool]:
|
|
257
|
+
"""Follows a path across to-ones and returns what it LANDS on, and whether it can find nothing.
|
|
258
|
+
|
|
259
|
+
The landing is a `SnakeColumnInfo` or a `SnakeRelationshipInfo` — a column or a relationship —
|
|
260
|
+
and the caller decides what to do with each. `optional` accumulates over every hop and is never
|
|
261
|
+
reset: that is the correctness of this function in one line.
|
|
262
|
+
"""
|
|
263
|
+
reg = registry_of(model)
|
|
264
|
+
optional = False
|
|
265
|
+
current, owner = table, model
|
|
266
|
+
for index, step in enumerate(path):
|
|
267
|
+
column = current.get_column_by_attr(step)
|
|
268
|
+
relationship = _relationship_named(current, step)
|
|
269
|
+
last = index == len(path) - 1
|
|
270
|
+
if column is not None and last:
|
|
271
|
+
return column, optional
|
|
272
|
+
if relationship is None:
|
|
273
|
+
known = ", ".join(
|
|
274
|
+
[_attribute(item.attr_name, item.name) for item in current.columns]
|
|
275
|
+
+ [item.name for item in current.relationships]
|
|
276
|
+
)
|
|
277
|
+
raise SnakeDtoError(
|
|
278
|
+
f"{dto} selects {'.'.join(path)!r}, and {owner.__name__} has no {step!r}. It has: "
|
|
279
|
+
f"{known}."
|
|
280
|
+
)
|
|
281
|
+
if last:
|
|
282
|
+
# The LAST hop counts too, and forgetting it is the bug this line exists for: a path
|
|
283
|
+
# that ends ON a to-one crosses that key, so `Post.editor` is `EditorDto | None` for
|
|
284
|
+
# exactly the reason `Post.editor.username` is `str | None`. A to-many is never
|
|
285
|
+
# optional by its own key — that key lives on the child, and no children is `[]`.
|
|
286
|
+
if relationship.kind is SnakeRelationshipKind.TO_ONE:
|
|
287
|
+
optional = optional or _to_one_is_optional(current, relationship)
|
|
288
|
+
return relationship, optional
|
|
289
|
+
if relationship.kind is not SnakeRelationshipKind.TO_ONE:
|
|
290
|
+
raise SnakeDtoError(
|
|
291
|
+
f"step {step!r} of {'.'.join(path)!r} in {dto} is a {relationship.kind.value}, and "
|
|
292
|
+
f"a path can only be continued across a to-one. A collection has no single row to "
|
|
293
|
+
f"read the next step off: nest it whole instead."
|
|
294
|
+
)
|
|
295
|
+
optional = optional or _to_one_is_optional(current, relationship)
|
|
296
|
+
target, target_model = reg.resolve_relationship(relationship)
|
|
297
|
+
if target is None or target_model is None:
|
|
298
|
+
raise SnakeDtoError(
|
|
299
|
+
f"step {step!r} of {'.'.join(path)!r} in {dto} points at "
|
|
300
|
+
f"{relationship.target!r}, which is not registered. Import the module that "
|
|
301
|
+
f"declares it, and call `snake_link()`."
|
|
302
|
+
)
|
|
303
|
+
current, owner = target, target_model
|
|
304
|
+
raise SnakeDtoError(f"{dto} selects an empty path, which names nothing.")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _to_one_is_optional(
|
|
308
|
+
table: SnakeTableInfo, relationship: SnakeRelationshipInfo
|
|
309
|
+
) -> bool:
|
|
310
|
+
"""Whether crossing this to-one can find NOTHING. One nullable member of the key is enough.
|
|
311
|
+
|
|
312
|
+
A pair whose local column is not on the table RAISES rather than being skipped. Skipping it and
|
|
313
|
+
letting `any()` fall through to `False` would answer NOT NULL for a key it could not read, and
|
|
314
|
+
that is the one direction this function must never fail in: the DTO promises a value that
|
|
315
|
+
arrives as `None`, the checker believes it, and nothing anywhere goes red.
|
|
316
|
+
|
|
317
|
+
The pair is `(source_column, target_column)` and the source side belongs to the declaring table,
|
|
318
|
+
so this cannot happen while the linker is right. That is precisely why it is worth saying out
|
|
319
|
+
loud — an invariant nobody checks is a defect waiting to be plausible.
|
|
320
|
+
"""
|
|
321
|
+
by_name = {column.name: column for column in table.columns}
|
|
322
|
+
missing = [
|
|
323
|
+
pair[0] for pair in relationship.foreign_key.pairs if pair[0] not in by_name
|
|
324
|
+
]
|
|
325
|
+
if missing:
|
|
326
|
+
raise SnakeDtoError(
|
|
327
|
+
f"the key of {relationship.name!r} reads {', '.join(missing)} off {table.name}, which "
|
|
328
|
+
f"has no such column, so there is no telling whether crossing it can find nothing. "
|
|
329
|
+
f"Nothing was written: this is the graph being inconsistent, not the declaration."
|
|
330
|
+
)
|
|
331
|
+
return any(by_name[pair[0]].nullable for pair in relationship.foreign_key.pairs)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _relationship_named(
|
|
335
|
+
table: SnakeTableInfo, name: str
|
|
336
|
+
) -> SnakeRelationshipInfo | None:
|
|
337
|
+
"""The relationship with that attribute name, or `None`."""
|
|
338
|
+
return next((item for item in table.relationships if item.name == name), None)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _table_of(model: type) -> SnakeTableInfo:
|
|
342
|
+
"""The model's compiled table, asked of the registry the model itself lives in."""
|
|
343
|
+
table = registry_of(model).table_of(model)
|
|
344
|
+
if table is None:
|
|
345
|
+
raise SnakeDtoError(
|
|
346
|
+
f"{getattr(model, '__name__', model)!r} is not a compiled model, so there is no column "
|
|
347
|
+
f"list to read."
|
|
348
|
+
)
|
|
349
|
+
return table
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _attribute(attr_name: str, sql_name: str) -> str:
|
|
353
|
+
"""The column's PYTHON name, which `snake_column(name=...)` can split from its SQL one."""
|
|
354
|
+
return attr_name or sql_name
|
snakeorm/dto/spec.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""`snake_dto(...)`: the DECLARATION of a typed DTO. The class it describes is written by the CLI.
|
|
2
|
+
|
|
3
|
+
This module does one job and stops: it turns a line somebody wrote into a frozen `SnakeDtoSpec`,
|
|
4
|
+
refusing anything it can already tell is wrong. It never reads the graph beyond checking that the
|
|
5
|
+
model was compiled — resolving what a path MEANS is `resolve.py`'s job, and it needs every spec in
|
|
6
|
+
the file at once because a nested field points at another spec.
|
|
7
|
+
|
|
8
|
+
WHY THE FIELDS ARE DESCRIPTORS. `Post.tilte` does not compile; `"tilte"` does. That is the whole
|
|
9
|
+
argument, and it is measured in this package's tests against both mypy and pyright.
|
|
10
|
+
|
|
11
|
+
WHERE THE DECLARATION LIVES, and it is the decision the rest of this package hangs off: inside the
|
|
12
|
+
DTO file's `if TYPE_CHECKING:` block, together with the import of the models. Three properties at
|
|
13
|
+
once, all measured:
|
|
14
|
+
|
|
15
|
+
mypy/pyright over `Post.tilte` inside the block -> error: "type[Post]" has no attribute
|
|
16
|
+
importing the module at runtime -> clean; nothing in the block ever runs
|
|
17
|
+
importing the module at runtime -> does NOT drag in `blog.models`
|
|
18
|
+
|
|
19
|
+
So the checker validates every path, the file costs nothing to import, and the import cycle between
|
|
20
|
+
`models.py` and `dto.py` cannot form. The generator never executes any of it: it READS the block
|
|
21
|
+
with `ast`. The function still works when called, but its runtime behaviour is not the product —
|
|
22
|
+
the type signature is.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import keyword
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Protocol, runtime_checkable
|
|
30
|
+
|
|
31
|
+
from snakeorm.core.exceptions import SnakeDtoError
|
|
32
|
+
from snakeorm.fields.relationship import SnakePathProxy, path_of
|
|
33
|
+
from snakeorm.registry import registry_of
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@runtime_checkable
|
|
37
|
+
class SnakeDtoPath(Protocol):
|
|
38
|
+
"""Anything that knows the path it names: a column expression or a collection.
|
|
39
|
+
|
|
40
|
+
Structural on purpose. `SnakeExpr` and `SnakeCollection` have nothing else in common and there
|
|
41
|
+
is no shared base to name — what this function needs from both is the path, so that is what the
|
|
42
|
+
type asks for. A to-one proxy deliberately does NOT match: its namespace is the target model's,
|
|
43
|
+
so it cannot carry a `path` attribute without shadowing a column of that name.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def path(self) -> tuple[str, ...]: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
SnakeDtoEntry = SnakeDtoPath | type[object]
|
|
51
|
+
"""One thing that can appear in `fields=`.
|
|
52
|
+
|
|
53
|
+
`type[object]` is how a to-one reads to the CHECKER: class access on `SnakeToOne[M]` is typed
|
|
54
|
+
`type[M]`, which is the same overload that makes `Post.author.username` resolve. It lets any class
|
|
55
|
+
through the type check, and that is the price of the navigation being typed at all; a class that
|
|
56
|
+
names no relationship is refused here, by name, at the line that wrote it.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
SnakeDtoSelection = SnakeDtoEntry | tuple[SnakeDtoEntry, str]
|
|
60
|
+
"""An entry, or an entry paired with the NAME of the DTO to nest for it.
|
|
61
|
+
|
|
62
|
+
The pair exists for one case: a model with more than one spec. `Post.author` alone cannot say
|
|
63
|
+
whether it means `AuthorDto` or `AuthorCard`, and picking either would be bug #14 wearing a
|
|
64
|
+
different hat. The disambiguation sits on the line that is ambiguous rather than in a parameter of
|
|
65
|
+
its own, and it stays typed.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class SnakeDtoPick:
|
|
71
|
+
"""One selected field: the path it names, and the DTO to nest if the user named one."""
|
|
72
|
+
|
|
73
|
+
path: tuple[str, ...]
|
|
74
|
+
dto: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class SnakeDtoSpec:
|
|
79
|
+
"""A declared DTO: which model, what it is called, and which of its fields are in.
|
|
80
|
+
|
|
81
|
+
`fields` is `None` when neither switch was given, which means every column. Frozen and hashable
|
|
82
|
+
like every other node this project compiles, so a spec can be a key in the dependency graph the
|
|
83
|
+
renderer sorts.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
model: type
|
|
87
|
+
name: str
|
|
88
|
+
fields: tuple[SnakeDtoPick, ...] | None = None
|
|
89
|
+
exclude: tuple[tuple[str, ...], ...] = ()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def snake_dto(
|
|
93
|
+
model: type,
|
|
94
|
+
*,
|
|
95
|
+
name: str,
|
|
96
|
+
fields: list[SnakeDtoSelection] | None = None,
|
|
97
|
+
exclude: list[SnakeDtoSelection] | None = None,
|
|
98
|
+
) -> SnakeDtoSpec:
|
|
99
|
+
"""Declares a DTO over `model`. Returns the spec, and records it for the generator.
|
|
100
|
+
|
|
101
|
+
Three combinations and one refusal: neither switch means every column, `fields` means exactly
|
|
102
|
+
those, `exclude` means all but those, and both together is an error because they are two answers
|
|
103
|
+
to one question.
|
|
104
|
+
|
|
105
|
+
A relationship in `fields` NESTS — `Post.author` becomes `author: AuthorDto` — and does not
|
|
106
|
+
expand into the author's columns. Expanding would mean a declaration you cannot read without
|
|
107
|
+
opening another model, and the point of writing the shape down is that it is a complete picture
|
|
108
|
+
of what goes out.
|
|
109
|
+
|
|
110
|
+
The spec is returned rather than recorded anywhere: in the file it is written in this line is
|
|
111
|
+
never reached, so a process-wide list of declarations would be empty in production and full in
|
|
112
|
+
tests.
|
|
113
|
+
"""
|
|
114
|
+
return build_spec(
|
|
115
|
+
model=model,
|
|
116
|
+
name=name,
|
|
117
|
+
fields=None if fields is None else tuple(_pick(item, name) for item in fields),
|
|
118
|
+
exclude=None
|
|
119
|
+
if exclude is None
|
|
120
|
+
else tuple(_excluded(item, name) for item in exclude),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def build_spec(
|
|
125
|
+
*,
|
|
126
|
+
model: type,
|
|
127
|
+
name: str,
|
|
128
|
+
fields: tuple[SnakeDtoPick, ...] | None,
|
|
129
|
+
exclude: tuple[tuple[str, ...], ...] | None,
|
|
130
|
+
where: str = "",
|
|
131
|
+
) -> SnakeDtoSpec:
|
|
132
|
+
"""Builds a spec out of already-normalised parts, applying the checks BOTH routes need.
|
|
133
|
+
|
|
134
|
+
Two things produce a spec — this module's typed call, and the AST reader — and they have to
|
|
135
|
+
agree about what a declaration may say. The checks live here so there is ONE answer: two copies
|
|
136
|
+
would eventually differ, and the difference would be a file the generator accepts and the
|
|
137
|
+
checker does not, or the other way round.
|
|
138
|
+
|
|
139
|
+
`where` prefixes the messages with `file:line` when the caller has one. The typed call does not,
|
|
140
|
+
because it fails on the line it was written on and the traceback already says which.
|
|
141
|
+
"""
|
|
142
|
+
prefix = f"{where}: " if where else ""
|
|
143
|
+
if fields is not None and exclude is not None:
|
|
144
|
+
raise SnakeDtoError(
|
|
145
|
+
f"{prefix}{name!r} declares both fields= and exclude=. They are two ways of answering "
|
|
146
|
+
f"the same question and they can disagree: name the fields you want, or the columns "
|
|
147
|
+
f"you do not, never both."
|
|
148
|
+
)
|
|
149
|
+
if not name.isidentifier() or keyword.iskeyword(name):
|
|
150
|
+
raise SnakeDtoError(
|
|
151
|
+
f"{prefix}{name!r} is not a valid class name, and this spec is written out as "
|
|
152
|
+
f"`class {name}(TypedDict)`."
|
|
153
|
+
)
|
|
154
|
+
if registry_of(model).table_of(model) is None:
|
|
155
|
+
raise SnakeDtoError(
|
|
156
|
+
f"{prefix}{getattr(model, '__name__', model)!r} is not a compiled model: nothing "
|
|
157
|
+
f"declared it with @snake_model or @snake_db_first, so there is no column list to read."
|
|
158
|
+
)
|
|
159
|
+
for path in exclude or ():
|
|
160
|
+
if len(path) != 1:
|
|
161
|
+
raise SnakeDtoError(
|
|
162
|
+
f"{prefix}{name!r} excludes {'.'.join(path)!r}, which is not one column of the "
|
|
163
|
+
f"model it describes. `exclude=` prunes this model's own columns; the fields of a "
|
|
164
|
+
f"nested DTO come from ITS spec, so there is nothing to prune from here."
|
|
165
|
+
)
|
|
166
|
+
return SnakeDtoSpec(model=model, name=name, fields=fields, exclude=exclude or ())
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _path_of_entry(entry: SnakeDtoEntry, dto: str) -> tuple[str, ...]:
|
|
170
|
+
"""The path an entry names, whichever of the three kinds of navigation node it is."""
|
|
171
|
+
if isinstance(entry, SnakePathProxy):
|
|
172
|
+
return path_of(entry)
|
|
173
|
+
if isinstance(entry, SnakeDtoPath):
|
|
174
|
+
return entry.path
|
|
175
|
+
raise SnakeDtoError(
|
|
176
|
+
f"{dto} was given {type(entry).__name__} as a field, which names no column or relationship. "
|
|
177
|
+
f"An entry is class access on a model — `Post.title`, `Post.author.username`, "
|
|
178
|
+
f"`Post.author`, `Post.comments` — and nothing else."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _pick(item: SnakeDtoSelection, dto: str) -> SnakeDtoPick:
|
|
183
|
+
"""One `fields=` entry, with the nested DTO name if it was paired with one."""
|
|
184
|
+
if isinstance(item, tuple):
|
|
185
|
+
entry, named = item
|
|
186
|
+
return SnakeDtoPick(path=_path_of_entry(entry, dto), dto=named)
|
|
187
|
+
return SnakeDtoPick(path=_path_of_entry(item, dto))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _excluded(item: SnakeDtoSelection, dto: str) -> tuple[str, ...]:
|
|
191
|
+
"""One `exclude=` entry, which has to be a plain column of the model being described.
|
|
192
|
+
|
|
193
|
+
A path across a relationship reads as if it removed something and removes nothing: the far
|
|
194
|
+
model's columns were never in this DTO to begin with, because a relationship NESTS.
|
|
195
|
+
"""
|
|
196
|
+
if isinstance(item, tuple):
|
|
197
|
+
raise SnakeDtoError(
|
|
198
|
+
f"{dto} pairs an exclusion with a DTO name, and there is nothing to name: "
|
|
199
|
+
f"`exclude=` removes columns, it does not nest anything."
|
|
200
|
+
)
|
|
201
|
+
return _path_of_entry(item, dto)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""SnakeORM's typed expression AST (columns and conditions)."""
|
|
2
|
+
|
|
3
|
+
from snakeorm.expressions.expression import SnakeCast as SnakeCast
|
|
4
|
+
from snakeorm.expressions.scalar import snake_cast as snake_cast
|
|
5
|
+
from snakeorm.expressions.scalar import snake_substring as snake_substring
|
|
6
|
+
from snakeorm.expressions.scalar import snake_replace as snake_replace
|
|
7
|
+
from snakeorm.expressions.scalar import snake_ceil as snake_ceil
|
|
8
|
+
from snakeorm.expressions.scalar import snake_floor as snake_floor
|
|
9
|
+
from snakeorm.expressions.scalar import snake_sqrt as snake_sqrt
|
|
10
|
+
from snakeorm.expressions.scalar import snake_power as snake_power
|
|
11
|
+
from snakeorm.expressions.conditional import SnakeCase as SnakeCase
|
|
12
|
+
from snakeorm.expressions.conditional import SnakeCaseBranch as SnakeCaseBranch
|
|
13
|
+
from snakeorm.expressions.conditional import SnakeCoalesce as SnakeCoalesce
|
|
14
|
+
from snakeorm.expressions.conditional import SnakeNullIf as SnakeNullIf
|
|
15
|
+
from snakeorm.expressions.conditional import snake_case as snake_case
|
|
16
|
+
from snakeorm.expressions.conditional import snake_coalesce as snake_coalesce
|
|
17
|
+
from snakeorm.expressions.conditional import snake_nullif as snake_nullif
|
|
18
|
+
from snakeorm.expressions.expression import SnakeAggFunc as SnakeAggFunc
|
|
19
|
+
from snakeorm.expressions.expression import SnakeAggregate as SnakeAggregate
|
|
20
|
+
from snakeorm.expressions.expression import SnakeAnd as SnakeAnd
|
|
21
|
+
from snakeorm.expressions.expression import SnakeArith as SnakeArith
|
|
22
|
+
from snakeorm.expressions.expression import SnakeJsonGet as SnakeJsonGet
|
|
23
|
+
from snakeorm.expressions.expression import SnakeArithOp as SnakeArithOp
|
|
24
|
+
from snakeorm.expressions.expression import static_type as static_type
|
|
25
|
+
from snakeorm.expressions.expression import SnakeStringAgg as SnakeStringAgg
|
|
26
|
+
from snakeorm.expressions.functions import string_agg as string_agg
|
|
27
|
+
from snakeorm.expressions.window import SNAKE_CURRENT_ROW as SNAKE_CURRENT_ROW
|
|
28
|
+
from snakeorm.expressions.window import SnakeFrame as SnakeFrame
|
|
29
|
+
from snakeorm.expressions.window import SnakeFrameBound as SnakeFrameBound
|
|
30
|
+
from snakeorm.expressions.window import SnakeFrameMode as SnakeFrameMode
|
|
31
|
+
from snakeorm.expressions.window import snake_following as snake_following
|
|
32
|
+
from snakeorm.expressions.window import snake_preceding as snake_preceding
|
|
33
|
+
from snakeorm.expressions.window import snake_range as snake_range
|
|
34
|
+
from snakeorm.expressions.window import snake_rows as snake_rows
|
|
35
|
+
from snakeorm.expressions.scalar import SnakeDateShift as SnakeDateShift
|
|
36
|
+
from snakeorm.expressions.scalar import snake_date_add as snake_date_add
|
|
37
|
+
from snakeorm.expressions.scalar import snake_date_sub as snake_date_sub
|
|
38
|
+
from snakeorm.expressions.expression import SnakeComparison as SnakeComparison
|
|
39
|
+
from snakeorm.expressions.expression import SnakeCondition as SnakeCondition
|
|
40
|
+
from snakeorm.expressions.expression import SnakeExists as SnakeExists
|
|
41
|
+
from snakeorm.expressions.expression import SnakeExistsJoin as SnakeExistsJoin
|
|
42
|
+
from snakeorm.expressions.expression import SnakeExpr as SnakeExpr
|
|
43
|
+
from snakeorm.expressions.expression import SnakeInList as SnakeInList
|
|
44
|
+
from snakeorm.expressions.expression import SnakeInSubquery as SnakeInSubquery
|
|
45
|
+
from snakeorm.expressions.expression import SnakeIsNotNull as SnakeIsNotNull
|
|
46
|
+
from snakeorm.expressions.expression import SnakeIsNull as SnakeIsNull
|
|
47
|
+
from snakeorm.expressions.expression import SnakeLike as SnakeLike
|
|
48
|
+
from snakeorm.expressions.expression import SnakeLock as SnakeLock
|
|
49
|
+
from snakeorm.expressions.expression import SnakeNot as SnakeNot
|
|
50
|
+
from snakeorm.expressions.expression import SnakeNulls as SnakeNulls
|
|
51
|
+
from snakeorm.expressions.expression import SnakeOp as SnakeOp
|
|
52
|
+
from snakeorm.expressions.expression import SnakeOr as SnakeOr
|
|
53
|
+
from snakeorm.expressions.expression import SnakeOrder as SnakeOrder
|
|
54
|
+
from snakeorm.expressions.expression import SnakeSubquery as SnakeSubquery
|
|
55
|
+
from snakeorm.expressions.expression import (
|
|
56
|
+
SnakeSubqueryAggregate as SnakeSubqueryAggregate,
|
|
57
|
+
)
|
|
58
|
+
from snakeorm.expressions.expression import SnakeTupleIn as SnakeTupleIn
|
|
59
|
+
from snakeorm.expressions.expression import SnakeValue as SnakeValue
|
|
60
|
+
from snakeorm.expressions.functions import avg as avg
|
|
61
|
+
from snakeorm.expressions.functions import count as count
|
|
62
|
+
from snakeorm.expressions.functions import max_ as max_
|
|
63
|
+
from snakeorm.expressions.functions import min_ as min_
|
|
64
|
+
from snakeorm.expressions.functions import sum_ as sum_
|
|
65
|
+
from snakeorm.expressions.paths import condition_paths as condition_paths
|
|
66
|
+
from snakeorm.expressions.scalar import SnakeDatePart as SnakeDatePart
|
|
67
|
+
from snakeorm.expressions.scalar import SnakeFunc as SnakeFunc
|
|
68
|
+
from snakeorm.expressions.scalar import SnakeFuncCall as SnakeFuncCall
|
|
69
|
+
from snakeorm.expressions.scalar import snake_abs as snake_abs
|
|
70
|
+
from snakeorm.expressions.scalar import snake_concat as snake_concat
|
|
71
|
+
from snakeorm.expressions.scalar import snake_date_trunc as snake_date_trunc
|
|
72
|
+
from snakeorm.expressions.scalar import snake_extract as snake_extract
|
|
73
|
+
from snakeorm.expressions.scalar import snake_length as snake_length
|
|
74
|
+
from snakeorm.expressions.scalar import snake_lower as snake_lower
|
|
75
|
+
from snakeorm.expressions.scalar import snake_round as snake_round
|
|
76
|
+
from snakeorm.expressions.scalar import snake_trim as snake_trim
|
|
77
|
+
from snakeorm.expressions.scalar import snake_upper as snake_upper
|
|
78
|
+
from snakeorm.expressions.window import SnakeWindow as SnakeWindow
|
|
79
|
+
from snakeorm.expressions.window import dense_rank as dense_rank
|
|
80
|
+
from snakeorm.expressions.window import lag as lag
|
|
81
|
+
from snakeorm.expressions.window import lead as lead
|
|
82
|
+
from snakeorm.expressions.window import rank as rank
|
|
83
|
+
from snakeorm.expressions.window import row_number as row_number
|
|
84
|
+
from snakeorm.expressions.keys import SnakeKey as SnakeKey
|
|
85
|
+
from snakeorm.expressions.keys import SnakeKeys as SnakeKeys
|
|
86
|
+
from snakeorm.expressions.keys import SnakeScalar as SnakeScalar
|
|
87
|
+
from snakeorm.expressions.keys import snake_key as snake_key
|
|
88
|
+
from snakeorm.expressions.keys import snake_keys as snake_keys
|