overture-schema-codegen 0.1.1.dev0__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.
- overture/schema/codegen/__init__.py +1 -0
- overture/schema/codegen/cli.py +228 -0
- overture/schema/codegen/extraction/__init__.py +0 -0
- overture/schema/codegen/extraction/docstring.py +46 -0
- overture/schema/codegen/extraction/enum_extraction.py +40 -0
- overture/schema/codegen/extraction/examples.py +367 -0
- overture/schema/codegen/extraction/field.py +172 -0
- overture/schema/codegen/extraction/field_constraints.py +185 -0
- overture/schema/codegen/extraction/field_walk.py +275 -0
- overture/schema/codegen/extraction/length_constraints.py +49 -0
- overture/schema/codegen/extraction/literal_alternatives.py +26 -0
- overture/schema/codegen/extraction/model_constraints.py +252 -0
- overture/schema/codegen/extraction/model_extraction.py +240 -0
- overture/schema/codegen/extraction/newtype_extraction.py +73 -0
- overture/schema/codegen/extraction/numeric_extraction.py +74 -0
- overture/schema/codegen/extraction/pydantic_extraction.py +33 -0
- overture/schema/codegen/extraction/specs.py +295 -0
- overture/schema/codegen/extraction/type_analyzer.py +693 -0
- overture/schema/codegen/extraction/type_registry.py +137 -0
- overture/schema/codegen/extraction/union_extraction.py +270 -0
- overture/schema/codegen/layout/__init__.py +0 -0
- overture/schema/codegen/layout/module_layout.py +139 -0
- overture/schema/codegen/layout/type_collection.py +122 -0
- overture/schema/codegen/markdown/__init__.py +0 -0
- overture/schema/codegen/markdown/link_computation.py +70 -0
- overture/schema/codegen/markdown/path_assignment.py +114 -0
- overture/schema/codegen/markdown/pipeline.py +198 -0
- overture/schema/codegen/markdown/renderer.py +641 -0
- overture/schema/codegen/markdown/reverse_references.py +169 -0
- overture/schema/codegen/markdown/templates/_used_by.md.jinja2 +10 -0
- overture/schema/codegen/markdown/templates/enum.md.jinja2 +13 -0
- overture/schema/codegen/markdown/templates/feature.md.jinja2 +45 -0
- overture/schema/codegen/markdown/templates/geometric.md.jinja2 +11 -0
- overture/schema/codegen/markdown/templates/newtype.md.jinja2 +17 -0
- overture/schema/codegen/markdown/templates/numeric.md.jinja2 +27 -0
- overture/schema/codegen/markdown/templates/pydantic_type.md.jinja2 +8 -0
- overture/schema/codegen/markdown/type_format.py +383 -0
- overture/schema/codegen/py.typed +0 -0
- overture/schema/codegen/pyspark/__init__.py +1 -0
- overture/schema/codegen/pyspark/_primitive_fill.py +23 -0
- overture/schema/codegen/pyspark/_render_common.py +477 -0
- overture/schema/codegen/pyspark/check_builder.py +961 -0
- overture/schema/codegen/pyspark/check_ir.py +223 -0
- overture/schema/codegen/pyspark/constraint_dispatch.py +753 -0
- overture/schema/codegen/pyspark/pipeline.py +220 -0
- overture/schema/codegen/pyspark/renderer.py +816 -0
- overture/schema/codegen/pyspark/schema_builder.py +187 -0
- overture/schema/codegen/pyspark/templates/_check_function.py.jinja2 +10 -0
- overture/schema/codegen/pyspark/templates/model_module.py.jinja2 +83 -0
- overture/schema/codegen/pyspark/templates/test_module.py.jinja2 +129 -0
- overture/schema/codegen/pyspark/test_data/__init__.py +9 -0
- overture/schema/codegen/pyspark/test_data/base_row.py +835 -0
- overture/schema/codegen/pyspark/test_data/constraint_values.py +203 -0
- overture/schema/codegen/pyspark/test_data/invalid_value.py +105 -0
- overture/schema/codegen/pyspark/test_data/scaffold.py +390 -0
- overture/schema/codegen/pyspark/test_renderer.py +708 -0
- overture/schema/codegen/spec_discovery.py +66 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/METADATA +13 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/RECORD +61 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/WHEEL +4 -0
- overture_schema_codegen-0.1.1.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
"""Render Check / ModelCheck IR into complete Python modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
from overture.schema.system.field_path import (
|
|
11
|
+
ArraySegment,
|
|
12
|
+
Direct,
|
|
13
|
+
FieldPath,
|
|
14
|
+
Iterated,
|
|
15
|
+
MapProjection,
|
|
16
|
+
MapSegment,
|
|
17
|
+
)
|
|
18
|
+
from overture.schema.system.geometric import GeometryType
|
|
19
|
+
|
|
20
|
+
from ._render_common import (
|
|
21
|
+
FieldCheckRow,
|
|
22
|
+
ModelCheckRow,
|
|
23
|
+
field_check_rows,
|
|
24
|
+
jinja_env,
|
|
25
|
+
map_runtime_helper,
|
|
26
|
+
model_check_rows,
|
|
27
|
+
py_literal,
|
|
28
|
+
sanitize_field_name,
|
|
29
|
+
schema_const_name,
|
|
30
|
+
tuple_literal,
|
|
31
|
+
)
|
|
32
|
+
from .check_ir import (
|
|
33
|
+
Check,
|
|
34
|
+
ColumnGuard,
|
|
35
|
+
ElementGuard,
|
|
36
|
+
ModelCheck,
|
|
37
|
+
)
|
|
38
|
+
from .constraint_dispatch import (
|
|
39
|
+
ExpressionDescriptor,
|
|
40
|
+
FieldEq,
|
|
41
|
+
ForbidIf,
|
|
42
|
+
MinFieldsSet,
|
|
43
|
+
RadioGroup,
|
|
44
|
+
RequireAnyOf,
|
|
45
|
+
RequireAnyTrue,
|
|
46
|
+
RequireIf,
|
|
47
|
+
model_constraint_function,
|
|
48
|
+
require_field_eq,
|
|
49
|
+
)
|
|
50
|
+
from .schema_builder import SHARED_TYPE_REFS, SchemaField
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"render_model_module",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Descriptor function names that resolve to helpers from the
|
|
57
|
+
# `column_patterns` runtime module (rather than `constraint_expressions`).
|
|
58
|
+
# Used to route imports to the correct module. Distinct from
|
|
59
|
+
# `_render_common.COLUMN_LEVEL_FUNCTIONS`, which classifies checks that
|
|
60
|
+
# emit one Check per field rather than per array element.
|
|
61
|
+
_COLUMN_PATTERN_HELPERS = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"array_check",
|
|
64
|
+
"nested_array_check",
|
|
65
|
+
"map_keys_check",
|
|
66
|
+
"map_values_check",
|
|
67
|
+
"check_struct_unique",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
_SHARED_STRUCT_REFS = frozenset(SHARED_TYPE_REFS.values())
|
|
72
|
+
|
|
73
|
+
_SPARK_TYPES = frozenset(
|
|
74
|
+
{
|
|
75
|
+
"ArrayType",
|
|
76
|
+
"BinaryType",
|
|
77
|
+
"BooleanType",
|
|
78
|
+
"ByteType",
|
|
79
|
+
"DateType",
|
|
80
|
+
"DoubleType",
|
|
81
|
+
"FloatType",
|
|
82
|
+
"IntegerType",
|
|
83
|
+
"LongType",
|
|
84
|
+
"MapType",
|
|
85
|
+
"ShortType",
|
|
86
|
+
"StringType",
|
|
87
|
+
"StructField",
|
|
88
|
+
"StructType",
|
|
89
|
+
"TimestampType",
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _render_condition_desc(parsed: FieldEq) -> str:
|
|
95
|
+
"""Render a parsed condition to a human-readable error-message description."""
|
|
96
|
+
display = repr(
|
|
97
|
+
parsed.value.value if isinstance(parsed.value, Enum) else parsed.value
|
|
98
|
+
)
|
|
99
|
+
op = "!=" if parsed.negated else "="
|
|
100
|
+
return f"{parsed.field_name} {op} {display}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _render_condition(
|
|
104
|
+
parsed: FieldEq,
|
|
105
|
+
*,
|
|
106
|
+
in_array: bool = False,
|
|
107
|
+
struct_path: tuple[str, ...] = (),
|
|
108
|
+
var: str = "el",
|
|
109
|
+
column_prefix: tuple[str, ...] = (),
|
|
110
|
+
) -> str:
|
|
111
|
+
"""Render a parsed condition to a PySpark Column expression string.
|
|
112
|
+
|
|
113
|
+
`struct_path` is the leaf the constrained model was reached at; the
|
|
114
|
+
condition field lives beside the target field on that same model, so
|
|
115
|
+
its reference must navigate the same leaf (e.g. `el["inner"]["subtype"]`,
|
|
116
|
+
not `el["subtype"]`). `column_prefix` plays the same role for a
|
|
117
|
+
struct-nested (non-iterated) model, qualifying the condition to
|
|
118
|
+
`F.col("details.subtype")`.
|
|
119
|
+
"""
|
|
120
|
+
ref = _render_field_ref(
|
|
121
|
+
parsed.field_name,
|
|
122
|
+
in_array=in_array,
|
|
123
|
+
struct_path=struct_path,
|
|
124
|
+
var=var,
|
|
125
|
+
column_prefix=column_prefix,
|
|
126
|
+
)
|
|
127
|
+
op = "!=" if parsed.negated else "=="
|
|
128
|
+
# A bare `== True` / `== False` -- from any boolean condition, whether
|
|
129
|
+
# require_if/forbid_if or require_any_true -- trips ruff's E712, which would
|
|
130
|
+
# rewrite the comparison away; wrap the boolean in `F.lit(...)` so the Column
|
|
131
|
+
# comparison survives post-generation ruff intact.
|
|
132
|
+
value_src = (
|
|
133
|
+
f"F.lit({py_literal(parsed.value)})"
|
|
134
|
+
if isinstance(parsed.value, bool)
|
|
135
|
+
else py_literal(parsed.value)
|
|
136
|
+
)
|
|
137
|
+
return f"{ref} {op} {value_src}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _render_field_ref(
|
|
141
|
+
field_name: str,
|
|
142
|
+
*,
|
|
143
|
+
in_array: bool,
|
|
144
|
+
struct_path: tuple[str, ...] = (),
|
|
145
|
+
var: str = "el",
|
|
146
|
+
column_prefix: tuple[str, ...] = (),
|
|
147
|
+
) -> str:
|
|
148
|
+
"""Render a field reference as F.col("x"), el["x"], or el["struct"]["x"].
|
|
149
|
+
|
|
150
|
+
`F.col` accepts dotted names directly so the top-level form keeps
|
|
151
|
+
`field_name` intact. `column_prefix` names the struct segments the model
|
|
152
|
+
was reached through (a struct-nested model constraint at
|
|
153
|
+
`Direct('details')`), so its fields resolve to `F.col("details.foo")`; it
|
|
154
|
+
is empty for a row-root constraint. The in-array form descends a struct via
|
|
155
|
+
`el[...]`, which requires the dotted name to be split into segments before
|
|
156
|
+
applying `struct_path` and the field's own parts.
|
|
157
|
+
"""
|
|
158
|
+
if not in_array:
|
|
159
|
+
qualified = ".".join((*column_prefix, field_name))
|
|
160
|
+
return f'F.col("{qualified}")'
|
|
161
|
+
parts = (*struct_path, *field_name.split("."))
|
|
162
|
+
return _element_accessor(var, parts)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _geometry_type_literal(g: GeometryType) -> str:
|
|
166
|
+
"""Spell out `GeometryType.NAME` as valid Python source.
|
|
167
|
+
|
|
168
|
+
`repr(g)` yields `<GeometryType.POINT: 'point'>`, which is not a valid
|
|
169
|
+
expression.
|
|
170
|
+
"""
|
|
171
|
+
return f"GeometryType.{g.name}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Check functions whose first positional arg is a list of allowed values.
|
|
175
|
+
# Descriptors store the values as a tuple for hashability; the renderer
|
|
176
|
+
# unwraps that one position to a list literal so the generated call matches
|
|
177
|
+
# the runtime signature.
|
|
178
|
+
_LIST_FIRST_ARG_FUNCTIONS = frozenset({"check_enum"})
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _render_arg(arg: object) -> str:
|
|
182
|
+
"""Render a descriptor arg as a valid Python expression string."""
|
|
183
|
+
if isinstance(arg, GeometryType):
|
|
184
|
+
return _geometry_type_literal(arg)
|
|
185
|
+
return py_literal(arg)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _render_expr_call(
|
|
189
|
+
desc: ExpressionDescriptor,
|
|
190
|
+
col_expr: str,
|
|
191
|
+
) -> str:
|
|
192
|
+
"""Render a single ExpressionDescriptor call with col injected."""
|
|
193
|
+
parts = [col_expr]
|
|
194
|
+
for idx, arg in enumerate(desc.args):
|
|
195
|
+
if (
|
|
196
|
+
idx == 0
|
|
197
|
+
and desc.function in _LIST_FIRST_ARG_FUNCTIONS
|
|
198
|
+
and isinstance(arg, tuple)
|
|
199
|
+
):
|
|
200
|
+
parts.append(py_literal(list(arg)))
|
|
201
|
+
else:
|
|
202
|
+
parts.append(_render_arg(arg))
|
|
203
|
+
for k, v in desc.kwargs:
|
|
204
|
+
parts.append(f"{k}={py_literal(v)}")
|
|
205
|
+
if desc.check_nan is not None:
|
|
206
|
+
parts.append(f"check_nan={py_literal(desc.check_nan)}")
|
|
207
|
+
if desc.label is not None:
|
|
208
|
+
parts.append(f"label={py_literal(desc.label)}")
|
|
209
|
+
call = f"{desc.function}({', '.join(parts)})"
|
|
210
|
+
if desc.allow_literals:
|
|
211
|
+
literals = py_literal(list(desc.allow_literals))
|
|
212
|
+
return f"except_literals({col_expr}, {call}, {literals})"
|
|
213
|
+
return call
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _element_accessor(var: str, path: tuple[str, ...]) -> str:
|
|
217
|
+
"""Build bracket-notation accessor like `el["foo"]["bar"]`."""
|
|
218
|
+
return var + "".join(f'["{p}"]' for p in path)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _iter_var_name(idx: int, total: int) -> str:
|
|
222
|
+
"""Lambda variable name at iteration depth `idx` (0..total-1) of `total`.
|
|
223
|
+
|
|
224
|
+
Single-iteration cases (`total == 1`) return `"el"` from the first
|
|
225
|
+
branch; the innermost frame of a nested iteration uses `"inner"`,
|
|
226
|
+
intermediate frames `"el2"`, `"el3"`, ...
|
|
227
|
+
"""
|
|
228
|
+
if idx == 0:
|
|
229
|
+
return "el"
|
|
230
|
+
if idx == total - 1:
|
|
231
|
+
return "inner"
|
|
232
|
+
return f"el{idx + 1}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _wrap_element_gate(body: str, var: str, gate_parts: tuple[str, ...]) -> str:
|
|
236
|
+
"""Wrap a lambda body in F.when(var[gate].isNotNull(), ...) for nullable parent gating."""
|
|
237
|
+
gate_accessor = _element_accessor(var, gate_parts)
|
|
238
|
+
return f"F.when({gate_accessor}.isNotNull(), {body})"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _map_iter_var(projection: MapProjection) -> str:
|
|
242
|
+
"""Lambda variable name for a map projection: `k` for keys, `v` for values."""
|
|
243
|
+
return "k" if projection is MapProjection.KEY else "v"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(frozen=True, slots=True)
|
|
247
|
+
class RenderFrame:
|
|
248
|
+
"""One iteration frame enriched with its lambda var and runtime helper.
|
|
249
|
+
|
|
250
|
+
Attributes
|
|
251
|
+
----------
|
|
252
|
+
prefix_structs
|
|
253
|
+
Struct segment names between the previous iterating segment (or the
|
|
254
|
+
start of the path) and this one. For the outermost frame this is the
|
|
255
|
+
column struct prefix; joined with `segment.name` it forms the frame's
|
|
256
|
+
`F.col(...)` column. For an inner named frame it is the descent from
|
|
257
|
+
the previous element; for an anonymous frame it is empty.
|
|
258
|
+
segment
|
|
259
|
+
The iterating segment (`ArraySegment` or `MapSegment`) this frame
|
|
260
|
+
iterates. Anonymous when the parent element is itself the container.
|
|
261
|
+
is_innermost
|
|
262
|
+
Whether this is the leaf-most iteration (the base runtime helper is
|
|
263
|
+
used; outer frames use the `nested_` flattening helper).
|
|
264
|
+
var_name
|
|
265
|
+
The lambda parameter name (`el` / `el2` / `inner` for arrays,
|
|
266
|
+
`k` / `v` for maps).
|
|
267
|
+
helper_name
|
|
268
|
+
The `column_patterns` helper this frame calls.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
prefix_structs: tuple[str, ...]
|
|
272
|
+
segment: ArraySegment | MapSegment
|
|
273
|
+
is_innermost: bool
|
|
274
|
+
var_name: str
|
|
275
|
+
helper_name: str
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def descent(self) -> tuple[str, ...]:
|
|
279
|
+
"""Struct accessor from the previous element to this container.
|
|
280
|
+
|
|
281
|
+
Empty for an anonymous frame (the parent element already IS this
|
|
282
|
+
container); `prefix_structs + segment.name` for a named frame.
|
|
283
|
+
"""
|
|
284
|
+
if self.segment.is_anonymous:
|
|
285
|
+
return ()
|
|
286
|
+
return (*self.prefix_structs, self.segment.name)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def column(self) -> str:
|
|
290
|
+
"""Dotted `F.col(...)` name for the outermost frame."""
|
|
291
|
+
return ".".join((*self.prefix_structs, self.segment.name))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _render_frames(target: Iterated) -> tuple[RenderFrame, ...]:
|
|
295
|
+
"""Enrich each iteration of *target* with its lambda var and runtime helper.
|
|
296
|
+
|
|
297
|
+
One `RenderFrame` per iteration -- every iterating segment, named and
|
|
298
|
+
anonymous, since each is its own `array_check` / `map_*_check` call.
|
|
299
|
+
Built once and consumed by the fold, `_pattern_imports_for`, and the
|
|
300
|
+
model-constraint context so var and helper names never drift (a hazard
|
|
301
|
+
with mixed nesting where two map frames both want `v`).
|
|
302
|
+
|
|
303
|
+
Array frames use `el` / `el2` / `inner` (indexed by overall iteration
|
|
304
|
+
position) with `array_check` (innermost) or `nested_array_check`; map
|
|
305
|
+
frames use `k` / `v` with `map_{keys,values}_check` (innermost) or the
|
|
306
|
+
`nested_map_*` flattening variant.
|
|
307
|
+
"""
|
|
308
|
+
raw: list[tuple[tuple[str, ...], ArraySegment | MapSegment]] = []
|
|
309
|
+
prefix: list[str] = []
|
|
310
|
+
for seg in target.segments:
|
|
311
|
+
if isinstance(seg, (ArraySegment, MapSegment)):
|
|
312
|
+
raw.append((tuple(prefix), seg))
|
|
313
|
+
prefix = []
|
|
314
|
+
else:
|
|
315
|
+
prefix.append(seg.name)
|
|
316
|
+
total = len(raw)
|
|
317
|
+
frames: list[RenderFrame] = []
|
|
318
|
+
for i, (prefix_structs, seg) in enumerate(raw):
|
|
319
|
+
is_innermost = i == total - 1
|
|
320
|
+
if isinstance(seg, ArraySegment):
|
|
321
|
+
var = _iter_var_name(i, total)
|
|
322
|
+
helper = "array_check" if is_innermost else "nested_array_check"
|
|
323
|
+
else:
|
|
324
|
+
var = _map_iter_var(seg.projection)
|
|
325
|
+
helper = map_runtime_helper(seg.projection, flatten=not is_innermost)
|
|
326
|
+
frames.append(
|
|
327
|
+
RenderFrame(
|
|
328
|
+
prefix_structs=prefix_structs,
|
|
329
|
+
segment=seg,
|
|
330
|
+
is_innermost=is_innermost,
|
|
331
|
+
var_name=var,
|
|
332
|
+
helper_name=helper,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
return tuple(frames)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _wrap_in_iteration(
|
|
339
|
+
frames: tuple[RenderFrame, ...],
|
|
340
|
+
body: str,
|
|
341
|
+
*,
|
|
342
|
+
gate_parts: tuple[str, ...] = (),
|
|
343
|
+
) -> str:
|
|
344
|
+
"""Fold *frames* outermost->innermost into nested iteration helper calls.
|
|
345
|
+
|
|
346
|
+
The outermost frame targets its `column` (an `F.col` string); each inner
|
|
347
|
+
frame targets an element accessor built from the outer frame's var and the
|
|
348
|
+
inner frame's `descent`. The innermost frame carries `body`. `gate_parts`,
|
|
349
|
+
when set, wraps the OUTERMOST frame's body in a nullable-parent element
|
|
350
|
+
gate (`element_relative_gate` is relative to the outer array element);
|
|
351
|
+
element guards are applied to `body` at the innermost var by the caller,
|
|
352
|
+
the two wrap points staying distinct.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def build(i: int, accessor: str) -> str:
|
|
356
|
+
frame = frames[i]
|
|
357
|
+
if frame.is_innermost:
|
|
358
|
+
inner = body
|
|
359
|
+
else:
|
|
360
|
+
child = frames[i + 1]
|
|
361
|
+
inner = build(i + 1, _element_accessor(frame.var_name, child.descent))
|
|
362
|
+
if i == 0 and gate_parts:
|
|
363
|
+
inner = _wrap_element_gate(inner, frame.var_name, gate_parts)
|
|
364
|
+
return f"{frame.helper_name}({accessor}, lambda {frame.var_name}: {inner})"
|
|
365
|
+
|
|
366
|
+
return build(0, f'"{frames[0].column}"')
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _render_iterated_check_expr(
|
|
370
|
+
target: Iterated,
|
|
371
|
+
desc: ExpressionDescriptor,
|
|
372
|
+
*,
|
|
373
|
+
element_guards: tuple[ElementGuard, ...] = (),
|
|
374
|
+
gate_parts: tuple[str, ...] = (),
|
|
375
|
+
) -> str:
|
|
376
|
+
"""Render an `Iterated` target to a nested iteration-fold expression.
|
|
377
|
+
|
|
378
|
+
Element guards are applied at the innermost iteration variable. This
|
|
379
|
+
assumes each guard's discriminator lives on the same struct level as
|
|
380
|
+
the leaf accessor -- true today because `ElementGuard`s only arise from
|
|
381
|
+
a union variant whose discriminator field is the immediately enclosing
|
|
382
|
+
array element. A future case where a check is reached through further
|
|
383
|
+
iteration *inside* a discriminated union element would need per-guard
|
|
384
|
+
depth info to apply the guard at the correct frame.
|
|
385
|
+
"""
|
|
386
|
+
frames = _render_frames(target)
|
|
387
|
+
innermost_var = frames[-1].var_name
|
|
388
|
+
leaf_accessor = _element_accessor(innermost_var, target.leaf)
|
|
389
|
+
body = _render_expr_call(desc, leaf_accessor)
|
|
390
|
+
|
|
391
|
+
for guard in reversed(element_guards):
|
|
392
|
+
body = _render_variant_expr(
|
|
393
|
+
body, guard.values, guard.discriminator, in_array=True, var=innermost_var
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return _wrap_in_iteration(frames, body, gate_parts=gate_parts)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _render_variant_expr(
|
|
400
|
+
inner_expr: str,
|
|
401
|
+
variant_values: tuple[str, ...],
|
|
402
|
+
discriminator_field: str,
|
|
403
|
+
*,
|
|
404
|
+
in_array: bool = False,
|
|
405
|
+
var: str = "el",
|
|
406
|
+
) -> str:
|
|
407
|
+
"""Wrap an expression in F.when(...).isin() gating for union variant fields."""
|
|
408
|
+
values_repr = py_literal(list(variant_values))
|
|
409
|
+
disc_ref = (
|
|
410
|
+
f'{var}["{discriminator_field}"]'
|
|
411
|
+
if in_array
|
|
412
|
+
else f'F.col("{discriminator_field}")'
|
|
413
|
+
)
|
|
414
|
+
return f"F.when({disc_ref}.isin({values_repr}), {inner_expr})"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _render_column_gate(expr: str, gate: FieldPath) -> str:
|
|
418
|
+
"""Wrap an expression in F.when(gate.isNotNull(), ...) for nullable parent gating."""
|
|
419
|
+
return f'F.when(F.col("{gate}").isNotNull(), {expr})'
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _model_check_func_name(check: ModelCheck, idx: int) -> str:
|
|
423
|
+
"""Build the private function name for a model-constraint check.
|
|
424
|
+
|
|
425
|
+
An `Iterated` (array/map) target prefixes the column path -- using the
|
|
426
|
+
full encoded `FieldPath` when the check is reached via inner iteration or
|
|
427
|
+
leaf struct navigation, otherwise the outer column name alone -- so
|
|
428
|
+
collisions across nested contexts get distinct identifiers. Row-root
|
|
429
|
+
(`Direct`) targets emit `_{fn}_{idx}_check`.
|
|
430
|
+
"""
|
|
431
|
+
fn = model_constraint_function(check.descriptor)
|
|
432
|
+
target = check.target
|
|
433
|
+
if isinstance(target, Iterated):
|
|
434
|
+
has_nested_path = bool(target.iter_struct_paths) or bool(target.leaf)
|
|
435
|
+
prefix_source = str(target) if has_nested_path else target.outer_column
|
|
436
|
+
prefix = sanitize_field_name(prefix_source)
|
|
437
|
+
return f"_{prefix}_{fn}_{idx}_check"
|
|
438
|
+
return f"_{fn}_{idx}_check"
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _check_shape_token(target: FieldPath) -> str:
|
|
442
|
+
"""Token naming the runtime `CheckShape` member for a target path.
|
|
443
|
+
|
|
444
|
+
Mirrors the member names of `overture.schema.pyspark.check.CheckShape`;
|
|
445
|
+
the check-function template prefixes `CheckShape.` to the result. An
|
|
446
|
+
`Iterated` target renders to an `array<string>` expression (array
|
|
447
|
+
iteration, or a map helper iterating the projected keys/values), a
|
|
448
|
+
`Direct` target to a nullable string.
|
|
449
|
+
"""
|
|
450
|
+
return "ARRAY" if isinstance(target, Iterated) else "SCALAR"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _render_check_expr(check: Check, descriptor_idx: int) -> str:
|
|
454
|
+
"""Render the PySpark expression for one descriptor of `check`."""
|
|
455
|
+
desc = check.descriptors[descriptor_idx]
|
|
456
|
+
column_guards = tuple(g for g in check.guards if isinstance(g, ColumnGuard))
|
|
457
|
+
element_guards = tuple(g for g in check.guards if isinstance(g, ElementGuard))
|
|
458
|
+
|
|
459
|
+
match check.target:
|
|
460
|
+
case Direct():
|
|
461
|
+
expr = _render_expr_call(desc, f'F.col("{check.target}")')
|
|
462
|
+
if desc.gate:
|
|
463
|
+
expr = _render_column_gate(expr, desc.gate)
|
|
464
|
+
case Iterated():
|
|
465
|
+
gate_parts: tuple[str, ...] = ()
|
|
466
|
+
if desc.gate is not None:
|
|
467
|
+
# check_builder zeros the nullable gate when descending into
|
|
468
|
+
# any iterated container (see `_recurse_into_model`), so a
|
|
469
|
+
# gate paired with an Iterated target should never occur
|
|
470
|
+
# today. If it does, the column-level fallback below would
|
|
471
|
+
# silently hide a codegen bug -- raise instead.
|
|
472
|
+
element_relative = check.target.element_relative_gate(desc.gate)
|
|
473
|
+
if element_relative is None:
|
|
474
|
+
raise AssertionError(
|
|
475
|
+
f"Iterated target with column-level gate is not "
|
|
476
|
+
f"produced by check_builder (gate={desc.gate!r}, "
|
|
477
|
+
f"target={check.target!r})"
|
|
478
|
+
)
|
|
479
|
+
gate_parts = element_relative
|
|
480
|
+
expr = _render_iterated_check_expr(
|
|
481
|
+
check.target,
|
|
482
|
+
desc,
|
|
483
|
+
element_guards=element_guards,
|
|
484
|
+
gate_parts=gate_parts,
|
|
485
|
+
)
|
|
486
|
+
case _:
|
|
487
|
+
raise TypeError(
|
|
488
|
+
f"Unhandled FieldPath variant: {type(check.target).__name__}"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
for guard in reversed(column_guards):
|
|
492
|
+
expr = _render_variant_expr(expr, guard.values, guard.discriminator)
|
|
493
|
+
return expr
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _check_function_context(
|
|
497
|
+
*,
|
|
498
|
+
target: FieldPath,
|
|
499
|
+
func_name: str,
|
|
500
|
+
field: str,
|
|
501
|
+
name: str,
|
|
502
|
+
expr: str,
|
|
503
|
+
read_columns: frozenset[str],
|
|
504
|
+
) -> dict[str, object]:
|
|
505
|
+
"""Assemble the template context dict for one check function."""
|
|
506
|
+
return {
|
|
507
|
+
"func_name": func_name,
|
|
508
|
+
"field": field,
|
|
509
|
+
"check_name": name,
|
|
510
|
+
"expr": expr,
|
|
511
|
+
"shape": _check_shape_token(target),
|
|
512
|
+
"read_columns": read_columns,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _render_check_function_context(row: FieldCheckRow) -> dict[str, object]:
|
|
517
|
+
"""Build the template context for a per-field check function from a row.
|
|
518
|
+
|
|
519
|
+
The row carries the final `func_name`, `label`, and `name`; the
|
|
520
|
+
collisions that produce them are resolved once in `field_check_rows`.
|
|
521
|
+
"""
|
|
522
|
+
return _check_function_context(
|
|
523
|
+
target=row.check.target,
|
|
524
|
+
func_name=row.func_name,
|
|
525
|
+
field=row.label,
|
|
526
|
+
name=row.name,
|
|
527
|
+
expr=_render_check_expr(row.check, row.descriptor_idx),
|
|
528
|
+
read_columns=row.check.read_columns,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _render_model_constraint_function_context(row: ModelCheckRow) -> dict[str, object]:
|
|
533
|
+
"""Build the template context for a model-constraint check function."""
|
|
534
|
+
check = row.check
|
|
535
|
+
desc = check.descriptor
|
|
536
|
+
target = check.target
|
|
537
|
+
# Build the render frames once; both the field-reference context (var /
|
|
538
|
+
# struct_path) and the iteration wrap below read from them so nothing drifts.
|
|
539
|
+
frames: tuple[RenderFrame, ...] = ()
|
|
540
|
+
column_prefix: tuple[str, ...] = ()
|
|
541
|
+
if isinstance(target, Iterated):
|
|
542
|
+
# The innermost element (array element or projected map value) is
|
|
543
|
+
# iterated, so field references use the element accessor
|
|
544
|
+
# (`inner["foo"]`, `v["foo"]`) under the innermost lambda variable.
|
|
545
|
+
frames = _render_frames(target)
|
|
546
|
+
in_array = True
|
|
547
|
+
var = frames[-1].var_name
|
|
548
|
+
struct_path: tuple[str, ...] = target.leaf
|
|
549
|
+
else:
|
|
550
|
+
# A struct-nested model constraint (`Direct` with segments) qualifies
|
|
551
|
+
# every field reference with the struct prefix (`F.col("details.foo")`);
|
|
552
|
+
# a row-root constraint (empty `Direct`) leaves the prefix empty.
|
|
553
|
+
in_array = False
|
|
554
|
+
var, struct_path = "el", ()
|
|
555
|
+
column_prefix = tuple(s.name for s in target.segments)
|
|
556
|
+
|
|
557
|
+
def _field_ref(field_name: str) -> str:
|
|
558
|
+
return _render_field_ref(
|
|
559
|
+
field_name,
|
|
560
|
+
in_array=in_array,
|
|
561
|
+
struct_path=struct_path,
|
|
562
|
+
var=var,
|
|
563
|
+
column_prefix=column_prefix,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
def _condition_ref(parsed: FieldEq) -> str:
|
|
567
|
+
return _render_condition(
|
|
568
|
+
parsed,
|
|
569
|
+
in_array=in_array,
|
|
570
|
+
struct_path=struct_path,
|
|
571
|
+
var=var,
|
|
572
|
+
column_prefix=column_prefix,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
fn = model_constraint_function(desc)
|
|
576
|
+
|
|
577
|
+
def _cols_and_names(field_names: tuple[str, ...]) -> tuple[str, str]:
|
|
578
|
+
cols_list = "[" + ", ".join(_field_ref(f) for f in field_names) + "]"
|
|
579
|
+
names_list = py_literal(list(field_names))
|
|
580
|
+
return cols_list, names_list
|
|
581
|
+
|
|
582
|
+
match desc:
|
|
583
|
+
case RequireAnyOf() | RadioGroup():
|
|
584
|
+
cols_list, names_list = _cols_and_names(desc.field_names)
|
|
585
|
+
inner_expr = f"{fn}({cols_list}, {names_list})"
|
|
586
|
+
case RequireAnyTrue():
|
|
587
|
+
parsed_conditions = [require_field_eq(c) for c in desc.conditions]
|
|
588
|
+
conds_list = (
|
|
589
|
+
"[" + ", ".join(_condition_ref(p) for p in parsed_conditions) + "]"
|
|
590
|
+
)
|
|
591
|
+
names_list = py_literal([p.field_name for p in parsed_conditions])
|
|
592
|
+
inner_expr = f"{fn}({conds_list}, {names_list})"
|
|
593
|
+
case RequireIf() | ForbidIf():
|
|
594
|
+
target_name = desc.field_names[0]
|
|
595
|
+
parsed = require_field_eq(desc.condition)
|
|
596
|
+
condition_expr = _condition_ref(parsed)
|
|
597
|
+
condition_desc = _render_condition_desc(parsed)
|
|
598
|
+
target_ref = _field_ref(target_name)
|
|
599
|
+
inner_expr = (
|
|
600
|
+
f"{fn}({target_ref}, {condition_expr}, {py_literal(condition_desc)})"
|
|
601
|
+
)
|
|
602
|
+
case MinFieldsSet():
|
|
603
|
+
cols_list, names_list = _cols_and_names(desc.field_names)
|
|
604
|
+
inner_expr = f"{fn}({cols_list}, {names_list}, {desc.count})"
|
|
605
|
+
case _:
|
|
606
|
+
raise TypeError(f"Unhandled model constraint descriptor: {desc!r}")
|
|
607
|
+
|
|
608
|
+
if isinstance(target, Iterated):
|
|
609
|
+
if check.gate is not None:
|
|
610
|
+
# A gate reaches only an array-first target: check_builder zeros
|
|
611
|
+
# the gate for any iterated container, so a map-reached model
|
|
612
|
+
# carries none, and `element_relative_gate` asserts the array-first
|
|
613
|
+
# precondition. The wrap assumes a single array level.
|
|
614
|
+
assert not target.iter_struct_paths, (
|
|
615
|
+
f"gated ModelCheck with a nested-array target ({target!r}) is unsupported; "
|
|
616
|
+
f"the element-gate wrap assumes a single array level"
|
|
617
|
+
)
|
|
618
|
+
element_relative = target.element_relative_gate(check.gate)
|
|
619
|
+
assert element_relative is not None, (
|
|
620
|
+
f"ModelCheck gate={check.gate!r} is not reachable as an element-level "
|
|
621
|
+
f"accessor on target={target!r}; gates on ModelChecks must be Iterated "
|
|
622
|
+
f"entering the same outer array as the target"
|
|
623
|
+
)
|
|
624
|
+
inner_expr = _wrap_element_gate(inner_expr, var, element_relative)
|
|
625
|
+
expr = _wrap_in_iteration(frames, inner_expr)
|
|
626
|
+
elif check.gate is not None:
|
|
627
|
+
# A struct-nested model reached through an optional ancestor: skip the
|
|
628
|
+
# constraint when that ancestor is null (accessing a field of a null
|
|
629
|
+
# struct yields null, which would otherwise trip the constraint on an
|
|
630
|
+
# absent model). The gate is a struct prefix of the target, so it reads
|
|
631
|
+
# the target's top-level column and `_render_column_gate` renders
|
|
632
|
+
# `F.when(F.col("details").isNotNull(), ...)`. A gate on an empty
|
|
633
|
+
# (row-root) `Direct` target is meaningless -- check_builder never emits
|
|
634
|
+
# one -- so guard it rather than render a nonsensical `F.when`.
|
|
635
|
+
assert isinstance(target, Direct) and target.segments, (
|
|
636
|
+
f"ModelCheck gate={check.gate!r} on a row-root Direct target={target!r}; "
|
|
637
|
+
f"a gate only pairs with a struct-nested or iterated model"
|
|
638
|
+
)
|
|
639
|
+
expr = _render_column_gate(inner_expr, check.gate)
|
|
640
|
+
else:
|
|
641
|
+
expr = inner_expr
|
|
642
|
+
|
|
643
|
+
return _check_function_context(
|
|
644
|
+
target=target,
|
|
645
|
+
func_name=_model_check_func_name(check, row.idx),
|
|
646
|
+
field=row.label,
|
|
647
|
+
name=row.name,
|
|
648
|
+
expr=expr,
|
|
649
|
+
read_columns=check.read_columns,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _collect_constraint_expr_imports(
|
|
654
|
+
field_checks: list[Check],
|
|
655
|
+
model_checks: list[ModelCheck],
|
|
656
|
+
) -> set[str]:
|
|
657
|
+
"""Collect all constraint_expressions function names needed.
|
|
658
|
+
|
|
659
|
+
Field-descriptor names go through a `_COLUMN_PATTERN_HELPERS`
|
|
660
|
+
filter so column-pattern helpers route to their own import bucket.
|
|
661
|
+
Model-constraint function names (`check_require_any_of`,
|
|
662
|
+
`check_radio_group`, ...) are disjoint from that set, so they pass
|
|
663
|
+
through unfiltered.
|
|
664
|
+
"""
|
|
665
|
+
names: set[str] = set()
|
|
666
|
+
for check in field_checks:
|
|
667
|
+
for desc in check.descriptors:
|
|
668
|
+
if desc.function not in _COLUMN_PATTERN_HELPERS:
|
|
669
|
+
names.add(desc.function)
|
|
670
|
+
if desc.allow_literals:
|
|
671
|
+
names.add("except_literals")
|
|
672
|
+
for mc in model_checks:
|
|
673
|
+
names.add(model_constraint_function(mc.descriptor))
|
|
674
|
+
return names
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _needs_geometry_type_import(field_checks: list[Check]) -> bool:
|
|
678
|
+
"""Return True when any descriptor arg is a GeometryType instance."""
|
|
679
|
+
for check in field_checks:
|
|
680
|
+
for desc in check.descriptors:
|
|
681
|
+
if any(isinstance(a, GeometryType) for a in desc.args):
|
|
682
|
+
return True
|
|
683
|
+
return False
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _pattern_imports_for(target: FieldPath) -> set[str]:
|
|
687
|
+
"""Column-pattern helpers needed to iterate `target`.
|
|
688
|
+
|
|
689
|
+
Reads the helper names off `_render_frames` -- the single source of the
|
|
690
|
+
frame->helper mapping the fold also consumes -- so the imports never drift
|
|
691
|
+
from the emitted calls. A `Direct` target needs none.
|
|
692
|
+
"""
|
|
693
|
+
if isinstance(target, Iterated):
|
|
694
|
+
return {frame.helper_name for frame in _render_frames(target)}
|
|
695
|
+
return set()
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _collect_column_pattern_imports(
|
|
699
|
+
field_checks: list[Check],
|
|
700
|
+
model_checks: list[ModelCheck],
|
|
701
|
+
) -> set[str]:
|
|
702
|
+
"""Collect column_patterns function names needed."""
|
|
703
|
+
names: set[str] = set()
|
|
704
|
+
for check in field_checks:
|
|
705
|
+
names |= _pattern_imports_for(check.target)
|
|
706
|
+
for desc in check.descriptors:
|
|
707
|
+
if desc.function in _COLUMN_PATTERN_HELPERS:
|
|
708
|
+
names.add(desc.function)
|
|
709
|
+
for mc in model_checks:
|
|
710
|
+
names |= _pattern_imports_for(mc.target)
|
|
711
|
+
return names
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
_IDENTIFIER_TOKEN = re.compile(r"[A-Z][A-Za-z0-9_]*")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _identifier_tokens(expr: str) -> set[str]:
|
|
718
|
+
"""Tokenize a Spark type expression into capitalized identifiers."""
|
|
719
|
+
return set(_IDENTIFIER_TOKEN.findall(expr))
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _collect_spark_type_imports(schema_fields: list[SchemaField]) -> set[str]:
|
|
723
|
+
"""Collect Spark type class names from schema field type expressions.
|
|
724
|
+
|
|
725
|
+
`StructType` and `StructField` are always included: the model module
|
|
726
|
+
template emits the schema constant as `StructType([...])` unconditionally,
|
|
727
|
+
so the import must be present even when there are no fields.
|
|
728
|
+
"""
|
|
729
|
+
used: set[str] = {"StructType", "StructField"}
|
|
730
|
+
for sf in schema_fields:
|
|
731
|
+
used |= _identifier_tokens(sf.type_expr) & _SPARK_TYPES
|
|
732
|
+
return used
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _collect_schema_struct_imports(schema_fields: list[SchemaField]) -> set[str]:
|
|
736
|
+
"""Collect _schema_structs constant names referenced in field type expressions."""
|
|
737
|
+
refs: set[str] = set()
|
|
738
|
+
for sf in schema_fields:
|
|
739
|
+
refs |= _identifier_tokens(sf.type_expr) & _SHARED_STRUCT_REFS
|
|
740
|
+
return refs
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _field_check_function_entries(
|
|
744
|
+
field_checks: list[Check],
|
|
745
|
+
) -> list[dict[str, object]]:
|
|
746
|
+
"""Build template contexts for field-level checks."""
|
|
747
|
+
return [
|
|
748
|
+
_render_check_function_context(row) for row in field_check_rows(field_checks)
|
|
749
|
+
]
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _model_check_function_entries(
|
|
753
|
+
model_checks: list[ModelCheck],
|
|
754
|
+
) -> list[dict[str, object]]:
|
|
755
|
+
"""Build template contexts for model-level checks."""
|
|
756
|
+
return [
|
|
757
|
+
_render_model_constraint_function_context(row)
|
|
758
|
+
for row in model_check_rows(model_checks)
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def render_model_module(
|
|
763
|
+
model_name: str,
|
|
764
|
+
field_checks: list[Check],
|
|
765
|
+
model_checks: list[ModelCheck],
|
|
766
|
+
schema_fields: list[SchemaField],
|
|
767
|
+
geometry_types: tuple[GeometryType, ...] = (),
|
|
768
|
+
*,
|
|
769
|
+
entry_point: str = "tests.placeholder:Placeholder",
|
|
770
|
+
partitions: Mapping[str, str] | None = None,
|
|
771
|
+
) -> str:
|
|
772
|
+
"""Render a complete Python module for a model's checks and schema."""
|
|
773
|
+
constraint_expr_fns = sorted(
|
|
774
|
+
_collect_constraint_expr_imports(field_checks, model_checks)
|
|
775
|
+
)
|
|
776
|
+
column_pattern_fns = sorted(
|
|
777
|
+
_collect_column_pattern_imports(field_checks, model_checks)
|
|
778
|
+
)
|
|
779
|
+
spark_types = sorted(_collect_spark_type_imports(schema_fields))
|
|
780
|
+
schema_struct_refs = sorted(_collect_schema_struct_imports(schema_fields))
|
|
781
|
+
geometry_type = _needs_geometry_type_import(field_checks) or bool(geometry_types)
|
|
782
|
+
geometry_types_literal = (
|
|
783
|
+
_render_geometry_types(geometry_types) if geometry_types else None
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
check_functions = _field_check_function_entries(
|
|
787
|
+
field_checks
|
|
788
|
+
) + _model_check_function_entries(model_checks)
|
|
789
|
+
|
|
790
|
+
model_title = model_name.replace("_", " ").title()
|
|
791
|
+
|
|
792
|
+
template = jinja_env().get_template("model_module.py.jinja2")
|
|
793
|
+
return template.render(
|
|
794
|
+
model_name=model_name,
|
|
795
|
+
model_title=model_title,
|
|
796
|
+
constraint_expr_fns=constraint_expr_fns,
|
|
797
|
+
column_pattern_fns=column_pattern_fns,
|
|
798
|
+
spark_types=spark_types,
|
|
799
|
+
schema_struct_refs=schema_struct_refs,
|
|
800
|
+
geometry_type=geometry_type,
|
|
801
|
+
check_functions=check_functions,
|
|
802
|
+
schema_const_name=schema_const_name(model_name),
|
|
803
|
+
schema_fields=schema_fields,
|
|
804
|
+
geometry_types_literal=geometry_types_literal,
|
|
805
|
+
entry_point=entry_point,
|
|
806
|
+
partitions=dict(partitions) if partitions else {},
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _render_geometry_types(geo: tuple[GeometryType, ...]) -> str:
|
|
811
|
+
"""Render a `geometry_types` tuple literal.
|
|
812
|
+
|
|
813
|
+
`GeometryType` is an Enum, so `repr()` does not produce a valid
|
|
814
|
+
expression -- members need explicit `GeometryType.NAME` source.
|
|
815
|
+
"""
|
|
816
|
+
return tuple_literal(_geometry_type_literal(g) for g in geo)
|