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,835 @@
|
|
|
1
|
+
"""Generate valid base rows for the rendered conformance tests.
|
|
2
|
+
|
|
3
|
+
`generate_base_row` produces a minimal valid row (required fields only)
|
|
4
|
+
from a `ModelSpec`. `generate_populated_row` produces a fully
|
|
5
|
+
populated row including optional fields. `generate_arm_rows` and
|
|
6
|
+
`generate_populated_arm_rows` do the same for each arm of a discriminated
|
|
7
|
+
union.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from overture.schema.common.scoping.lr import LinearReferenceRangeConstraint
|
|
18
|
+
from overture.schema.system.geometric.geom import (
|
|
19
|
+
Geometry,
|
|
20
|
+
GeometryType,
|
|
21
|
+
GeometryTypeConstraint,
|
|
22
|
+
)
|
|
23
|
+
from overture.schema.system.model_constraint import (
|
|
24
|
+
ForbidIfConstraint,
|
|
25
|
+
MinFieldsSetConstraint,
|
|
26
|
+
RadioGroupConstraint,
|
|
27
|
+
RequireAnyOfConstraint,
|
|
28
|
+
RequireAnyTrueConstraint,
|
|
29
|
+
RequireIfConstraint,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from ...extraction.field import (
|
|
33
|
+
AnyScalar,
|
|
34
|
+
ArrayOf,
|
|
35
|
+
ConstraintSource,
|
|
36
|
+
FieldShape,
|
|
37
|
+
LiteralScalar,
|
|
38
|
+
MapOf,
|
|
39
|
+
ModelRef,
|
|
40
|
+
NewTypeShape,
|
|
41
|
+
Primitive,
|
|
42
|
+
UnionRef,
|
|
43
|
+
)
|
|
44
|
+
from ...extraction.field_walk import (
|
|
45
|
+
enum_source,
|
|
46
|
+
has_array_layer,
|
|
47
|
+
terminal_primitive,
|
|
48
|
+
terminal_scalar,
|
|
49
|
+
)
|
|
50
|
+
from ...extraction.length_constraints import ArrayMinLen
|
|
51
|
+
from ...extraction.specs import FieldSpec, ModelSpec, RecordSpec, UnionSpec
|
|
52
|
+
from ...extraction.type_registry import primitive_spark_category
|
|
53
|
+
from .._primitive_fill import PRIMITIVE_FILL_TABLE
|
|
54
|
+
from ..constraint_dispatch import (
|
|
55
|
+
ExpressionDescriptor,
|
|
56
|
+
FieldEq,
|
|
57
|
+
dispatch_constraint,
|
|
58
|
+
require_bool_field_eq,
|
|
59
|
+
require_field_eq,
|
|
60
|
+
)
|
|
61
|
+
from .constraint_values import (
|
|
62
|
+
CONSTRAINT_VALUES,
|
|
63
|
+
curated_pattern_values,
|
|
64
|
+
uncurated_pattern_error,
|
|
65
|
+
valid_bound,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"condition_overrides_for_present_field",
|
|
70
|
+
"generate_arm_rows",
|
|
71
|
+
"generate_base_row",
|
|
72
|
+
"generate_populated_arm_rows",
|
|
73
|
+
"generate_populated_row",
|
|
74
|
+
"resolve_arm_spec",
|
|
75
|
+
"value_for_field",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
_BASE_ROW_NAMESPACE = uuid.uuid5(
|
|
79
|
+
uuid.NAMESPACE_DNS, "overturemaps.org/codegen/base_row"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# WKT strings for each allowed geometry type (valid side)
|
|
84
|
+
_VALID_GEOMETRY_WKT: dict[GeometryType, str] = {
|
|
85
|
+
GeometryType.POINT: "POINT (0 0)",
|
|
86
|
+
GeometryType.LINE_STRING: "LINESTRING (0 0, 1 1)",
|
|
87
|
+
GeometryType.POLYGON: "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
|
|
88
|
+
GeometryType.MULTI_POLYGON: "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)))",
|
|
89
|
+
GeometryType.MULTI_LINE_STRING: "MULTILINESTRING ((0 0, 1 1))",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_PRIMITIVE_DEFAULTS: dict[str, object] = {
|
|
94
|
+
"str": "",
|
|
95
|
+
"NoWhitespaceString": "",
|
|
96
|
+
"HttpUrl": "https://example.com/",
|
|
97
|
+
"EmailStr": "user@example.com",
|
|
98
|
+
"bool": False,
|
|
99
|
+
"bytes": b"",
|
|
100
|
+
"datetime": "2024-01-01T00:00:00Z",
|
|
101
|
+
"date": "2024-01-01",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _bbox_value() -> dict[str, float]:
|
|
106
|
+
return {"xmin": 0.0, "xmax": 1.0, "ymin": 0.0, "ymax": 1.0}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Field-name overrides applied before any shape-based value generation in
|
|
110
|
+
# `value_for_field`. Each builder receives `(field_spec, spec_name)`.
|
|
111
|
+
_SPECIAL_FIELD_VALUES: dict[str, Callable[[FieldSpec, str], object]] = {
|
|
112
|
+
"id": lambda _f, spec_name: str(uuid.uuid5(_BASE_ROW_NAMESPACE, spec_name)),
|
|
113
|
+
"bbox": lambda _f, _spec_name: _bbox_value(),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_geometry_terminal(terminal: Primitive) -> bool:
|
|
118
|
+
"""Whether this terminal represents a geometry value.
|
|
119
|
+
|
|
120
|
+
Only fires for the `Geometry` source class. Fields wanting a
|
|
121
|
+
geometry value must declare `Geometry`; ad-hoc forms like
|
|
122
|
+
`Annotated[bytes, GeometryTypeConstraint(...)]` aren't recognized.
|
|
123
|
+
"""
|
|
124
|
+
return terminal.source_type is Geometry
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def generate_base_row(spec: ModelSpec, *, index: int = 0) -> dict[str, Any]:
|
|
128
|
+
"""Produce a minimal valid row from a feature spec (required fields only).
|
|
129
|
+
|
|
130
|
+
The row passes `TypeAdapter(validation_type).validate_python()`.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
spec
|
|
135
|
+
An expanded feature spec.
|
|
136
|
+
index
|
|
137
|
+
Position within a parent list. Non-zero values suffix string fields
|
|
138
|
+
to ensure uniqueness across list items.
|
|
139
|
+
"""
|
|
140
|
+
return _build_row(spec, index=index, populate_optional=False)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def generate_populated_row(spec: ModelSpec, *, index: int = 0) -> dict[str, Any]:
|
|
144
|
+
"""Produce a fully populated valid row (all fields, including optional).
|
|
145
|
+
|
|
146
|
+
Sub-models are recursively populated.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
spec
|
|
151
|
+
An expanded feature spec.
|
|
152
|
+
index
|
|
153
|
+
Position within a parent list. Non-zero values suffix string fields
|
|
154
|
+
to ensure uniqueness across list items.
|
|
155
|
+
"""
|
|
156
|
+
return _build_row(spec, index=index, populate_optional=True)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def generate_arm_rows(spec: ModelSpec) -> dict[str, dict[str, Any]]:
|
|
160
|
+
"""Produce one minimal valid row per discriminator arm of a union.
|
|
161
|
+
|
|
162
|
+
Returns `{arm_value: row}` where each row passes TypeAdapter
|
|
163
|
+
validation against the union's source annotation.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
spec
|
|
168
|
+
An expanded union spec.
|
|
169
|
+
"""
|
|
170
|
+
return _build_arm_rows(_require_union(spec), populate_optional=False)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def generate_populated_arm_rows(
|
|
174
|
+
spec: ModelSpec,
|
|
175
|
+
) -> dict[str, dict[str, Any]]:
|
|
176
|
+
"""Produce one fully populated valid row per discriminator arm.
|
|
177
|
+
|
|
178
|
+
Returns `{arm_value: row}` where each row passes TypeAdapter
|
|
179
|
+
validation and includes all optional fields with valid values.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
spec
|
|
184
|
+
An expanded union spec.
|
|
185
|
+
"""
|
|
186
|
+
return _build_arm_rows(_require_union(spec), populate_optional=True)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _require_union(spec: ModelSpec) -> UnionSpec:
|
|
190
|
+
if not isinstance(spec, UnionSpec):
|
|
191
|
+
raise TypeError(
|
|
192
|
+
f"Expected a UnionSpec, got {type(spec).__name__}: {spec.name!r}"
|
|
193
|
+
)
|
|
194
|
+
return spec
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _build_row(
|
|
198
|
+
spec: ModelSpec,
|
|
199
|
+
*,
|
|
200
|
+
index: int = 0,
|
|
201
|
+
populate_optional: bool,
|
|
202
|
+
name_override: str | None = None,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
row: dict[str, Any] = {}
|
|
205
|
+
name = name_override or spec.name
|
|
206
|
+
for field in spec.fields:
|
|
207
|
+
if not populate_optional and not field.is_required:
|
|
208
|
+
continue
|
|
209
|
+
row[field.name] = value_for_field(
|
|
210
|
+
field, name, index=index, populate_optional=populate_optional
|
|
211
|
+
)
|
|
212
|
+
_satisfy_model_constraints(row, spec)
|
|
213
|
+
return row
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _build_arm_rows(
|
|
217
|
+
spec: UnionSpec,
|
|
218
|
+
*,
|
|
219
|
+
populate_optional: bool,
|
|
220
|
+
) -> dict[str, dict[str, Any]]:
|
|
221
|
+
if spec.discriminator_field is None or spec.discriminator_mapping is None:
|
|
222
|
+
raise ValueError(f"UnionSpec {spec.name!r} has no discriminator")
|
|
223
|
+
if spec.constraints:
|
|
224
|
+
# Per-arm rows are built from member specs only; union-level
|
|
225
|
+
# constraints (e.g. radio_group on the union itself) would need
|
|
226
|
+
# `_satisfy_model_constraints` applied with the union's field
|
|
227
|
+
# list. No schema exercises this today; raise so a future union
|
|
228
|
+
# that adds one fails loudly rather than producing invalid rows.
|
|
229
|
+
raise NotImplementedError(
|
|
230
|
+
f"UnionSpec {spec.name!r} has {len(spec.constraints)} model "
|
|
231
|
+
"constraint(s); per-arm row generation does not enforce them"
|
|
232
|
+
)
|
|
233
|
+
spec_by_class = {ms.member_cls: ms.spec for ms in spec.member_specs}
|
|
234
|
+
result: dict[str, dict[str, Any]] = {}
|
|
235
|
+
for arm_val, member_cls in spec.discriminator_mapping.items():
|
|
236
|
+
row = _build_row(
|
|
237
|
+
spec_by_class[member_cls],
|
|
238
|
+
populate_optional=populate_optional,
|
|
239
|
+
name_override=spec.name,
|
|
240
|
+
)
|
|
241
|
+
row[spec.discriminator_field] = arm_val
|
|
242
|
+
result[arm_val] = row
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _condition_value(field_eq: FieldEq) -> object:
|
|
247
|
+
"""Return the condition's comparison value, with an `Enum` unwrapped to its value.
|
|
248
|
+
|
|
249
|
+
Row dicts store raw scalar values, so an `Enum`-typed condition value is
|
|
250
|
+
compared and written as its underlying `.value`.
|
|
251
|
+
"""
|
|
252
|
+
value = field_eq.value
|
|
253
|
+
return value.value if isinstance(value, Enum) else value
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _row_satisfies_condition(row: dict[str, Any], condition: object) -> bool:
|
|
257
|
+
"""Check whether the condition is satisfied by the row's current values.
|
|
258
|
+
|
|
259
|
+
Handles `FieldEqCondition` and `Not(FieldEqCondition)`. Raises
|
|
260
|
+
`TypeError` for any other condition kind so new condition types fail
|
|
261
|
+
loudly rather than silently returning an incorrect result.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
row
|
|
266
|
+
Current row dict being built.
|
|
267
|
+
condition
|
|
268
|
+
A `Condition` from a `RequireIfConstraint` or `ForbidIfConstraint`.
|
|
269
|
+
"""
|
|
270
|
+
field_eq = require_field_eq(condition) # type: ignore[arg-type]
|
|
271
|
+
matches = row.get(field_eq.field_name) == _condition_value(field_eq)
|
|
272
|
+
return matches != field_eq.negated
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _satisfy_model_constraints(row: dict[str, Any], spec: ModelSpec) -> None:
|
|
276
|
+
"""Adjust *row* so each model constraint is satisfied.
|
|
277
|
+
|
|
278
|
+
`require_if`/`radio_group`/`require_any_of`/`min_fields_set` fill in
|
|
279
|
+
optional fields the constraint makes mandatory. `forbid_if` removes
|
|
280
|
+
fields the constraint excludes. Constraints whose guard predicate is
|
|
281
|
+
false (e.g. a `RequireIf` whose condition does not hold for the
|
|
282
|
+
current row) need no adjustment and pass through; any constraint type
|
|
283
|
+
not matched by an arm here is silently skipped, intentionally -- new
|
|
284
|
+
constraint kinds surface via `dispatch_model_constraint` (which
|
|
285
|
+
raises) rather than here.
|
|
286
|
+
"""
|
|
287
|
+
fields_by_name = {f.name: f for f in spec.fields}
|
|
288
|
+
for constraint in spec.constraints:
|
|
289
|
+
match constraint:
|
|
290
|
+
case RequireIfConstraint() if _row_satisfies_condition(
|
|
291
|
+
row, constraint.condition
|
|
292
|
+
):
|
|
293
|
+
for field_name in constraint.field_names:
|
|
294
|
+
if field_name in row:
|
|
295
|
+
continue
|
|
296
|
+
field_spec = fields_by_name.get(field_name)
|
|
297
|
+
if field_spec is not None:
|
|
298
|
+
row[field_name] = value_for_field(field_spec, spec.name)
|
|
299
|
+
case RadioGroupConstraint() if not any(
|
|
300
|
+
row.get(fn) is True for fn in constraint.field_names
|
|
301
|
+
):
|
|
302
|
+
for field_name in constraint.field_names:
|
|
303
|
+
if field_name in fields_by_name:
|
|
304
|
+
row[field_name] = True
|
|
305
|
+
break
|
|
306
|
+
case RequireAnyTrueConstraint() if not any(
|
|
307
|
+
_row_satisfies_condition(row, c) for c in constraint.conditions
|
|
308
|
+
):
|
|
309
|
+
# Make the first condition hold by writing the boolean it tests
|
|
310
|
+
# for. `require_bool_field_eq` validates the positive-boolean
|
|
311
|
+
# invariant here too -- this path runs on the raw constraint and
|
|
312
|
+
# does not pass through `dispatch_model_constraint`.
|
|
313
|
+
field_eq = require_bool_field_eq(constraint.conditions[0]) # type: ignore[arg-type]
|
|
314
|
+
row[field_eq.field_name] = field_eq.value
|
|
315
|
+
case RequireAnyOfConstraint() if not any(
|
|
316
|
+
fn in row for fn in constraint.field_names
|
|
317
|
+
):
|
|
318
|
+
for field_name in constraint.field_names:
|
|
319
|
+
field_spec = fields_by_name.get(field_name)
|
|
320
|
+
if field_spec is not None:
|
|
321
|
+
row[field_name] = value_for_field(field_spec, spec.name)
|
|
322
|
+
break
|
|
323
|
+
case ForbidIfConstraint() if _row_satisfies_condition(
|
|
324
|
+
row, constraint.condition
|
|
325
|
+
):
|
|
326
|
+
for field_name in constraint.field_names:
|
|
327
|
+
row.pop(field_name, None)
|
|
328
|
+
case MinFieldsSetConstraint(count=count):
|
|
329
|
+
# Mirror Pydantic's `model_fields_set` semantics: every
|
|
330
|
+
# required field is "set" by the constructor, and counts
|
|
331
|
+
# alongside any non-null optional field. Required fields
|
|
332
|
+
# are always populated by the time we reach this branch,
|
|
333
|
+
# so satisfying `count` may need extra optional fills.
|
|
334
|
+
missing = count - sum(1 for f in spec.fields if f.name in row)
|
|
335
|
+
for opt_field in (f for f in spec.fields if not f.is_required):
|
|
336
|
+
if missing <= 0:
|
|
337
|
+
break
|
|
338
|
+
if opt_field.name in row:
|
|
339
|
+
continue
|
|
340
|
+
row[opt_field.name] = value_for_field(opt_field, spec.name)
|
|
341
|
+
missing -= 1
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _condition_disabling_value(field_eq: FieldEq, field_spec: FieldSpec) -> object:
|
|
345
|
+
"""Return a value for a condition field that makes the condition false.
|
|
346
|
+
|
|
347
|
+
`FieldEqCondition(f, X)` holds when `f == X`, so a different enum member
|
|
348
|
+
disables it; a negated condition (`Not(...)`, true when `f != X`) is
|
|
349
|
+
disabled by `X` itself. Every condition in the schema gates on an enum
|
|
350
|
+
field, so a non-enum condition field raises rather than guess a value.
|
|
351
|
+
|
|
352
|
+
Parameters
|
|
353
|
+
----------
|
|
354
|
+
field_eq
|
|
355
|
+
The unwrapped field-equality condition.
|
|
356
|
+
field_spec
|
|
357
|
+
Spec of the condition field, used to enumerate alternative values.
|
|
358
|
+
"""
|
|
359
|
+
forbidden = _condition_value(field_eq)
|
|
360
|
+
if field_eq.negated:
|
|
361
|
+
return forbidden
|
|
362
|
+
terminal = terminal_primitive(field_spec.shape)
|
|
363
|
+
enum_cls = enum_source(terminal) if terminal is not None else None
|
|
364
|
+
if enum_cls is None:
|
|
365
|
+
raise TypeError(
|
|
366
|
+
f"condition field {field_eq.field_name!r} is not enum-backed; "
|
|
367
|
+
"cannot derive a value that disables its forbid_if condition"
|
|
368
|
+
)
|
|
369
|
+
for member in enum_cls:
|
|
370
|
+
if member.value != forbidden:
|
|
371
|
+
return member.value
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"enum {enum_cls.__name__} has no member other than {forbidden!r}; "
|
|
374
|
+
"cannot disable its forbid_if condition"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def condition_overrides_for_present_field(
|
|
379
|
+
spec: ModelSpec, field_name: str
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""Return overrides that let `field_name` be present on a valid base row.
|
|
382
|
+
|
|
383
|
+
A `forbid_if` whose condition the base row satisfies forbids `field_name`,
|
|
384
|
+
so a scaffold that sets the field yields a row Pydantic rejects. Flip each
|
|
385
|
+
such condition field to a value the forbid rejects -- which also satisfies
|
|
386
|
+
the symmetric `require_if` that then mandates the field -- and re-satisfy
|
|
387
|
+
the model constraints, since a flipped condition can newly require other
|
|
388
|
+
fields. Returns only the fields whose value differs from the base row;
|
|
389
|
+
`field_name` itself is set by the scaffold and is excluded.
|
|
390
|
+
|
|
391
|
+
Returns `{}` when no `forbid_if` gates `field_name`, the common case.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
spec
|
|
396
|
+
The model whose constraints govern `field_name`.
|
|
397
|
+
field_name
|
|
398
|
+
A direct field of `spec` the scaffold needs to set.
|
|
399
|
+
"""
|
|
400
|
+
forbidding = [
|
|
401
|
+
c
|
|
402
|
+
for c in spec.constraints
|
|
403
|
+
if isinstance(c, ForbidIfConstraint) and field_name in c.field_names
|
|
404
|
+
]
|
|
405
|
+
if not forbidding:
|
|
406
|
+
return {}
|
|
407
|
+
base = generate_base_row(spec)
|
|
408
|
+
fields_by_name = {f.name: f for f in spec.fields}
|
|
409
|
+
flips: dict[str, Any] = {}
|
|
410
|
+
for constraint in forbidding:
|
|
411
|
+
if not _row_satisfies_condition(base, constraint.condition):
|
|
412
|
+
continue
|
|
413
|
+
field_eq = require_field_eq(constraint.condition)
|
|
414
|
+
cond_field = fields_by_name.get(field_eq.field_name)
|
|
415
|
+
if cond_field is not None:
|
|
416
|
+
flips[field_eq.field_name] = _condition_disabling_value(
|
|
417
|
+
field_eq, cond_field
|
|
418
|
+
)
|
|
419
|
+
if not flips:
|
|
420
|
+
return {}
|
|
421
|
+
merged = {**base, **flips}
|
|
422
|
+
_satisfy_model_constraints(merged, spec)
|
|
423
|
+
return {
|
|
424
|
+
name: value
|
|
425
|
+
for name, value in merged.items()
|
|
426
|
+
if name != field_name and base.get(name) != value
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def value_for_field(
|
|
431
|
+
field: FieldSpec,
|
|
432
|
+
spec_name: str,
|
|
433
|
+
*,
|
|
434
|
+
index: int = 0,
|
|
435
|
+
populate_optional: bool = False,
|
|
436
|
+
) -> object:
|
|
437
|
+
"""Produce a valid value for a single field.
|
|
438
|
+
|
|
439
|
+
Consults field constraints via `dispatch_constraint` to produce
|
|
440
|
+
constraint-satisfying values (e.g., a valid country code instead of
|
|
441
|
+
an empty string).
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
field
|
|
446
|
+
The field spec to produce a value for.
|
|
447
|
+
spec_name
|
|
448
|
+
The name of the containing spec, used for deterministic UUID generation.
|
|
449
|
+
index
|
|
450
|
+
Position within a parent list. Non-zero values suffix string fields
|
|
451
|
+
to ensure uniqueness across list items.
|
|
452
|
+
populate_optional
|
|
453
|
+
When True, MODEL and UNION sub-rows include optional fields via
|
|
454
|
+
`generate_populated_row`. When False (default), sub-rows are sparse
|
|
455
|
+
via `generate_base_row`.
|
|
456
|
+
"""
|
|
457
|
+
special = _SPECIAL_FIELD_VALUES.get(field.name)
|
|
458
|
+
if special is not None:
|
|
459
|
+
return special(field, spec_name)
|
|
460
|
+
|
|
461
|
+
shape = field.shape
|
|
462
|
+
|
|
463
|
+
# Geometry fields short-circuit to a WKT literal. PySpark's Geometry
|
|
464
|
+
# validator parses WKT via `from_wkt`; the field is stored as
|
|
465
|
+
# BinaryType (WKB) downstream.
|
|
466
|
+
terminal = terminal_primitive(shape)
|
|
467
|
+
if terminal is not None and _is_geometry_terminal(terminal):
|
|
468
|
+
return _geometry_wkt_from_shape_constraints(terminal.constraints)
|
|
469
|
+
|
|
470
|
+
# Non-list fields: try a constraint-driven value (e.g. CountryCode -> "US")
|
|
471
|
+
# before falling back to type defaults. The terminal scalar carries the
|
|
472
|
+
# constraints directly in the no-list case. Lists go through the recursive
|
|
473
|
+
# shape walk so array-level constraints and per-element constraints both
|
|
474
|
+
# get a chance to drive value generation.
|
|
475
|
+
if not has_array_layer(shape) and terminal is not None:
|
|
476
|
+
constraint_val = _value_from_scalar_constraints(terminal)
|
|
477
|
+
if constraint_val is not None:
|
|
478
|
+
if index > 0 and isinstance(constraint_val, str):
|
|
479
|
+
return f"{constraint_val}{index}"
|
|
480
|
+
return constraint_val
|
|
481
|
+
|
|
482
|
+
return _value_for_shape(
|
|
483
|
+
shape,
|
|
484
|
+
index=index,
|
|
485
|
+
check_constraints=False,
|
|
486
|
+
populate_optional=populate_optional,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _default_union_member(union: UnionSpec) -> RecordSpec:
|
|
491
|
+
"""Return the union member used when no discriminator value is known.
|
|
492
|
+
|
|
493
|
+
A field shared by name across arms always resolves to the same Spark
|
|
494
|
+
type in every arm (enforced by `schema_builder._deduplicate_by_name`,
|
|
495
|
+
which raises otherwise), so any arm's synthesized value is safe to
|
|
496
|
+
write into that shared column -- the member choice is arbitrary. Picks
|
|
497
|
+
the first member, deterministically, so regeneration is stable. See
|
|
498
|
+
`resolve_arm_spec` for why a *constraint* difference between arms never
|
|
499
|
+
reaches this fallback.
|
|
500
|
+
"""
|
|
501
|
+
return union.member_specs[0].spec
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def resolve_arm_spec(
|
|
505
|
+
union: UnionSpec, discriminator_value: object | None = None
|
|
506
|
+
) -> RecordSpec:
|
|
507
|
+
"""Return the member `RecordSpec` for one arm of a discriminated union.
|
|
508
|
+
|
|
509
|
+
Without a discriminator value, returns the union's first member. That
|
|
510
|
+
fallback is reached only for a check not gated to a specific arm, which
|
|
511
|
+
happens only when the check applies uniformly across arms -- so any arm
|
|
512
|
+
is representative and the first is a safe, deterministic choice.
|
|
513
|
+
|
|
514
|
+
Nothing is lost by not knowing the arm here. Two arms can share a field
|
|
515
|
+
name at the same Spark type but with *different* constraints (axle count
|
|
516
|
+
is discriminated on `dimension`, and its `value` carries `ge=1,
|
|
517
|
+
multiple_of=1` where the other `VehicleSelector` arms carry `ge=0`). Such
|
|
518
|
+
divergent-constraint fields are emitted as separate arm-gated checks, so
|
|
519
|
+
their base rows and scaffolds always arrive WITH a discriminator and
|
|
520
|
+
select the correct arm below -- they never reach the first-member
|
|
521
|
+
default. A raise here would therefore fire on the common, correct case
|
|
522
|
+
(uniform shared fields), not catch a bug; the loud guards against a
|
|
523
|
+
divergent field slipping through un-gated live where they can see the
|
|
524
|
+
divergence -- `_deduplicate_by_name` (Spark-type mismatch) and the
|
|
525
|
+
renderer's duplicate-violation-identity check (two checks colliding on
|
|
526
|
+
one arm's label).
|
|
527
|
+
|
|
528
|
+
With a value, returns the member that value selects, and raises when it
|
|
529
|
+
selects none: a seeded discriminator that matches no arm is a
|
|
530
|
+
check_builder/scaffold inconsistency, not a reason to fall back to an arm
|
|
531
|
+
whose fields contradict the seed.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
union
|
|
536
|
+
The union to resolve an arm from.
|
|
537
|
+
discriminator_value
|
|
538
|
+
The discriminator value identifying the arm (e.g. a scaffold's seeded
|
|
539
|
+
`ElementGuard` value), matching a `discriminator_mapping` key.
|
|
540
|
+
|
|
541
|
+
Raises
|
|
542
|
+
------
|
|
543
|
+
ValueError
|
|
544
|
+
When `discriminator_value` is given but selects no member arm.
|
|
545
|
+
"""
|
|
546
|
+
if discriminator_value is None:
|
|
547
|
+
return _default_union_member(union)
|
|
548
|
+
mapping = union.discriminator_mapping or {}
|
|
549
|
+
member_cls = mapping.get(discriminator_value) # type: ignore[call-overload]
|
|
550
|
+
if member_cls is not None:
|
|
551
|
+
for member in union.member_specs:
|
|
552
|
+
if member.member_cls is member_cls:
|
|
553
|
+
return member.spec
|
|
554
|
+
raise ValueError(
|
|
555
|
+
f"discriminator {discriminator_value!r} selects no arm of union "
|
|
556
|
+
f"{union.name!r} (arms: {sorted(mapping)})"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _row_from_model_spec(
|
|
561
|
+
spec: RecordSpec,
|
|
562
|
+
*,
|
|
563
|
+
index: int = 0,
|
|
564
|
+
populate_optional: bool = False,
|
|
565
|
+
) -> dict[str, Any]:
|
|
566
|
+
"""Generate a row dict from an already-extracted model spec."""
|
|
567
|
+
if populate_optional:
|
|
568
|
+
return generate_populated_row(spec, index=index)
|
|
569
|
+
return generate_base_row(spec, index=index)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _value_for_shape(
|
|
573
|
+
shape: FieldShape,
|
|
574
|
+
*,
|
|
575
|
+
index: int = 0,
|
|
576
|
+
check_constraints: bool = True,
|
|
577
|
+
populate_optional: bool = False,
|
|
578
|
+
) -> object:
|
|
579
|
+
"""Produce a valid value from a `FieldShape`.
|
|
580
|
+
|
|
581
|
+
Each shape layer carries its own constraints: `ArrayOf`'s
|
|
582
|
+
constraints drive list-length decisions; the element shape's
|
|
583
|
+
constraints (visible after descending into `element`) drive
|
|
584
|
+
per-item value generation.
|
|
585
|
+
|
|
586
|
+
Parameters
|
|
587
|
+
----------
|
|
588
|
+
shape
|
|
589
|
+
The field shape to produce a value for.
|
|
590
|
+
index
|
|
591
|
+
Array element index, used to suffix strings for uniqueness.
|
|
592
|
+
check_constraints
|
|
593
|
+
When True, attempt constraint-driven value generation at the
|
|
594
|
+
terminal Scalar before falling back to a primitive default.
|
|
595
|
+
populate_optional
|
|
596
|
+
When True, MODEL and UNION sub-rows include optional fields via
|
|
597
|
+
`generate_populated_row`. When False (default), sub-rows are
|
|
598
|
+
sparse via `generate_base_row`.
|
|
599
|
+
"""
|
|
600
|
+
match shape:
|
|
601
|
+
case ArrayOf(element=element, constraints=array_constraints):
|
|
602
|
+
list_val = _list_value_from_shape_constraints(array_constraints)
|
|
603
|
+
if list_val is not None:
|
|
604
|
+
return list_val
|
|
605
|
+
count = _min_length_from_shape_constraints(array_constraints)
|
|
606
|
+
return [
|
|
607
|
+
_value_for_shape(element, index=i, populate_optional=populate_optional)
|
|
608
|
+
for i in range(count)
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
case NewTypeShape(inner=inner):
|
|
612
|
+
return _value_for_shape(
|
|
613
|
+
inner,
|
|
614
|
+
index=index,
|
|
615
|
+
check_constraints=check_constraints,
|
|
616
|
+
populate_optional=populate_optional,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
case MapOf(key=key_shape, value=value_shape):
|
|
620
|
+
# One constraint-valid entry: an empty map satisfies Pydantic
|
|
621
|
+
# but leaves nothing for a conformance scenario to corrupt, so
|
|
622
|
+
# the key/value checks would never fire. A `dict[K, Any]` value
|
|
623
|
+
# (e.g. Infrastructure.source_tags) carries no constraint -- and
|
|
624
|
+
# thus no check -- and `Any` has no value strategy, so the map
|
|
625
|
+
# stays empty: there is nothing to validate or corrupt.
|
|
626
|
+
if isinstance(terminal_scalar(value_shape), AnyScalar):
|
|
627
|
+
return {}
|
|
628
|
+
map_key = _value_for_shape(
|
|
629
|
+
key_shape, index=index, populate_optional=populate_optional
|
|
630
|
+
)
|
|
631
|
+
map_value = _value_for_shape(
|
|
632
|
+
value_shape, index=index, populate_optional=populate_optional
|
|
633
|
+
)
|
|
634
|
+
return {map_key: map_value}
|
|
635
|
+
|
|
636
|
+
case LiteralScalar(values=values):
|
|
637
|
+
val = values[0]
|
|
638
|
+
return val.value if isinstance(val, Enum) else val
|
|
639
|
+
|
|
640
|
+
case Primitive() as p if (enum_cls := enum_source(p)) is not None:
|
|
641
|
+
return list(enum_cls)[0].value
|
|
642
|
+
|
|
643
|
+
case ModelRef(model=m):
|
|
644
|
+
return _row_from_model_spec(
|
|
645
|
+
m, index=index, populate_optional=populate_optional
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
case UnionRef(union=u):
|
|
649
|
+
# The selected member's discriminator field is a `Literal[X] = "x"`
|
|
650
|
+
# with a default, so it has `is_required=False`. In the populated
|
|
651
|
+
# case the LiteralScalar branch writes the literal explicitly; in
|
|
652
|
+
# the sparse case the field is omitted from the dict and Pydantic
|
|
653
|
+
# supplies the default during `TypeAdapter.validate_python()`.
|
|
654
|
+
return _row_from_model_spec(
|
|
655
|
+
_default_union_member(u),
|
|
656
|
+
index=index,
|
|
657
|
+
populate_optional=populate_optional,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
case AnyScalar():
|
|
661
|
+
# No value strategy exists for `Any`. The map walk descends
|
|
662
|
+
# into key/value shapes, so a `dict[K, Any]` value would reach
|
|
663
|
+
# here -- no schema declares one today, and this raises loudly
|
|
664
|
+
# rather than guess a value if one ever appears.
|
|
665
|
+
raise TypeError(
|
|
666
|
+
"AnyScalar reached base-row generation; no value strategy exists"
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
case Primitive() as scalar:
|
|
670
|
+
constraint_val: object | None = None
|
|
671
|
+
if check_constraints:
|
|
672
|
+
constraint_val = _value_from_scalar_constraints(scalar)
|
|
673
|
+
val = (
|
|
674
|
+
constraint_val
|
|
675
|
+
if constraint_val is not None
|
|
676
|
+
else _primitive_default(scalar.base_type)
|
|
677
|
+
)
|
|
678
|
+
if index > 0 and isinstance(val, str):
|
|
679
|
+
val = f"{val}{index}"
|
|
680
|
+
return val
|
|
681
|
+
|
|
682
|
+
raise TypeError(f"Unhandled FieldShape: {shape!r}")
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _value_from_check_enum(
|
|
686
|
+
desc: ExpressionDescriptor, _scalar: Primitive, _cs: ConstraintSource
|
|
687
|
+
) -> object:
|
|
688
|
+
"""Return the first allowed value from a `check_enum` descriptor."""
|
|
689
|
+
return desc.args[0][0] # type: ignore[index,no-any-return]
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _value_from_check_string_min_length(
|
|
693
|
+
desc: ExpressionDescriptor, _scalar: Primitive, _cs: ConstraintSource
|
|
694
|
+
) -> str:
|
|
695
|
+
"""Return a filler string of exactly `min_length` characters.
|
|
696
|
+
|
|
697
|
+
The descriptor's sole arg is the `min_length` bound. A shorter string
|
|
698
|
+
(a single character against `min_length > 1`) would violate the
|
|
699
|
+
constraint, making the generated conformance ::valid row invalid.
|
|
700
|
+
"""
|
|
701
|
+
min_length: int = desc.args[0] # type: ignore[assignment]
|
|
702
|
+
return "a" * min_length
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _value_from_check_pattern(
|
|
706
|
+
desc: ExpressionDescriptor, _scalar: Primitive, _cs: ConstraintSource
|
|
707
|
+
) -> object:
|
|
708
|
+
"""Return a pattern-matching value for a curated raw pydantic pattern.
|
|
709
|
+
|
|
710
|
+
Only raw `Field(pattern=)` constraints reach here -- named
|
|
711
|
+
`PatternConstraint` subclasses resolve earlier via `CONSTRAINT_VALUES`.
|
|
712
|
+
An uncurated pattern fails loud, symmetrically with `invalid_value`:
|
|
713
|
+
matching strings can't be generated generically, and silently falling
|
|
714
|
+
back to the primitive default would emit a row that fails the pattern,
|
|
715
|
+
surfacing later as a misleading "row should be valid" Pydantic error.
|
|
716
|
+
|
|
717
|
+
Raises
|
|
718
|
+
------
|
|
719
|
+
ValueError
|
|
720
|
+
When the pattern has no curated entry in `PATTERN_VALUES`.
|
|
721
|
+
"""
|
|
722
|
+
curated = curated_pattern_values(desc)
|
|
723
|
+
if curated is None:
|
|
724
|
+
raise uncurated_pattern_error(desc, side="valid")
|
|
725
|
+
return curated.valid
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
# Builders for descriptor-driven values, keyed by `ExpressionDescriptor.function`.
|
|
729
|
+
# `check_bounds` is intentionally absent: it is routed through
|
|
730
|
+
# `_value_from_scalar_constraints` to merge multiple bound descriptors (e.g.
|
|
731
|
+
# separate Gt + Lt) before calling `valid_bound` once with the combined kwargs,
|
|
732
|
+
# so a single-bound path never silently produces a value that violates a second
|
|
733
|
+
# bound on the same field.
|
|
734
|
+
# `check_pattern` only yields a value for a curated raw pydantic pattern;
|
|
735
|
+
# named pattern constraints resolve earlier via `CONSTRAINT_VALUES`.
|
|
736
|
+
_DESCRIPTOR_VALUE_BUILDERS: dict[
|
|
737
|
+
str, Callable[[ExpressionDescriptor, Primitive, ConstraintSource], object | None]
|
|
738
|
+
] = {
|
|
739
|
+
"check_enum": _value_from_check_enum,
|
|
740
|
+
"check_string_min_length": _value_from_check_string_min_length,
|
|
741
|
+
"check_pattern": _value_from_check_pattern,
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
_CONSTRAINT_VALID_LIST_VALUES: dict[type, list[object]] = {
|
|
746
|
+
LinearReferenceRangeConstraint: [0.0, 1.0],
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def _value_from_scalar_constraints(scalar: Primitive) -> object | None:
|
|
751
|
+
"""Return a value satisfying all dispatched constraints on a scalar.
|
|
752
|
+
|
|
753
|
+
Maps known constraint types to valid values directly. For `check_bounds`
|
|
754
|
+
descriptors, merges all bound kwargs from every constraint on the field
|
|
755
|
+
into one dict and calls `valid_bound` once, so a field carrying separate
|
|
756
|
+
`Gt`/`Lt` constraints (two `check_bounds` descriptors) gets a value
|
|
757
|
+
satisfying both bounds. Non-bounds constraints use first-match behavior.
|
|
758
|
+
"""
|
|
759
|
+
merged_bounds: dict[str, object] = {}
|
|
760
|
+
for cs in scalar.constraints:
|
|
761
|
+
constraint_type = type(cs.constraint)
|
|
762
|
+
if constraint_type in CONSTRAINT_VALUES:
|
|
763
|
+
return CONSTRAINT_VALUES[constraint_type].valid
|
|
764
|
+
desc = dispatch_constraint(cs.constraint, base_type=scalar.base_type)
|
|
765
|
+
if desc is None:
|
|
766
|
+
continue
|
|
767
|
+
if desc.function == "check_bounds":
|
|
768
|
+
# Skip structural bounds from numeric NewType ranges — those are
|
|
769
|
+
# enforced by the Spark/Parquet type system, not by field constraints.
|
|
770
|
+
if cs.source_name != scalar.base_type:
|
|
771
|
+
merged_bounds.update(desc.kwargs)
|
|
772
|
+
continue
|
|
773
|
+
builder = _DESCRIPTOR_VALUE_BUILDERS.get(desc.function)
|
|
774
|
+
if builder is None:
|
|
775
|
+
continue
|
|
776
|
+
val = builder(desc, scalar, cs)
|
|
777
|
+
if val is not None:
|
|
778
|
+
return val
|
|
779
|
+
if merged_bounds:
|
|
780
|
+
merged_desc = ExpressionDescriptor(
|
|
781
|
+
function="check_bounds", kwargs=tuple(merged_bounds.items())
|
|
782
|
+
)
|
|
783
|
+
return valid_bound(merged_desc)
|
|
784
|
+
return None
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _list_value_from_shape_constraints(
|
|
788
|
+
constraints: tuple[ConstraintSource, ...],
|
|
789
|
+
) -> list[object] | None:
|
|
790
|
+
"""Return a fixed valid list value if a list-level constraint requires it."""
|
|
791
|
+
for cs in constraints:
|
|
792
|
+
val = _CONSTRAINT_VALID_LIST_VALUES.get(type(cs.constraint))
|
|
793
|
+
if val is not None:
|
|
794
|
+
return val
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _min_length_from_shape_constraints(
|
|
799
|
+
constraints: tuple[ConstraintSource, ...],
|
|
800
|
+
) -> int:
|
|
801
|
+
"""Extract the array min_length from constraints anchored at this layer.
|
|
802
|
+
|
|
803
|
+
Constraints sit on the `ArrayOf` whose iteration they govern, so any
|
|
804
|
+
`ArrayMinLen` we see here applies to this list level directly -- no
|
|
805
|
+
anchor arithmetic is required.
|
|
806
|
+
"""
|
|
807
|
+
for cs in constraints:
|
|
808
|
+
if isinstance(cs.constraint, ArrayMinLen):
|
|
809
|
+
return max(cs.constraint.min_length, 1)
|
|
810
|
+
return 1
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _primitive_default(base_type: str) -> object:
|
|
814
|
+
"""Return a type-appropriate default for a primitive base_type."""
|
|
815
|
+
explicit = _PRIMITIVE_DEFAULTS.get(base_type)
|
|
816
|
+
if explicit is not None:
|
|
817
|
+
return explicit
|
|
818
|
+
category = primitive_spark_category(base_type)
|
|
819
|
+
entry = PRIMITIVE_FILL_TABLE.get(category)
|
|
820
|
+
return entry[1] if entry is not None else ""
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _geometry_wkt_from_shape_constraints(
|
|
824
|
+
constraints: tuple[ConstraintSource, ...],
|
|
825
|
+
) -> str:
|
|
826
|
+
"""Extract the allowed geometry type from constraints and return valid WKT."""
|
|
827
|
+
for cs in constraints:
|
|
828
|
+
if isinstance(cs.constraint, GeometryTypeConstraint):
|
|
829
|
+
geom_type = cs.constraint.allowed_types[0]
|
|
830
|
+
wkt = _VALID_GEOMETRY_WKT.get(geom_type)
|
|
831
|
+
if wkt is not None:
|
|
832
|
+
return wkt
|
|
833
|
+
raise ValueError(f"No WKT defined for geometry type: {geom_type!r}")
|
|
834
|
+
# No constraint — default to POINT
|
|
835
|
+
return _VALID_GEOMETRY_WKT[GeometryType.POINT]
|