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.
Files changed (61) hide show
  1. overture/schema/codegen/__init__.py +1 -0
  2. overture/schema/codegen/cli.py +228 -0
  3. overture/schema/codegen/extraction/__init__.py +0 -0
  4. overture/schema/codegen/extraction/docstring.py +46 -0
  5. overture/schema/codegen/extraction/enum_extraction.py +40 -0
  6. overture/schema/codegen/extraction/examples.py +367 -0
  7. overture/schema/codegen/extraction/field.py +172 -0
  8. overture/schema/codegen/extraction/field_constraints.py +185 -0
  9. overture/schema/codegen/extraction/field_walk.py +275 -0
  10. overture/schema/codegen/extraction/length_constraints.py +49 -0
  11. overture/schema/codegen/extraction/literal_alternatives.py +26 -0
  12. overture/schema/codegen/extraction/model_constraints.py +252 -0
  13. overture/schema/codegen/extraction/model_extraction.py +240 -0
  14. overture/schema/codegen/extraction/newtype_extraction.py +73 -0
  15. overture/schema/codegen/extraction/numeric_extraction.py +74 -0
  16. overture/schema/codegen/extraction/pydantic_extraction.py +33 -0
  17. overture/schema/codegen/extraction/specs.py +295 -0
  18. overture/schema/codegen/extraction/type_analyzer.py +693 -0
  19. overture/schema/codegen/extraction/type_registry.py +137 -0
  20. overture/schema/codegen/extraction/union_extraction.py +270 -0
  21. overture/schema/codegen/layout/__init__.py +0 -0
  22. overture/schema/codegen/layout/module_layout.py +139 -0
  23. overture/schema/codegen/layout/type_collection.py +122 -0
  24. overture/schema/codegen/markdown/__init__.py +0 -0
  25. overture/schema/codegen/markdown/link_computation.py +70 -0
  26. overture/schema/codegen/markdown/path_assignment.py +114 -0
  27. overture/schema/codegen/markdown/pipeline.py +198 -0
  28. overture/schema/codegen/markdown/renderer.py +641 -0
  29. overture/schema/codegen/markdown/reverse_references.py +169 -0
  30. overture/schema/codegen/markdown/templates/_used_by.md.jinja2 +10 -0
  31. overture/schema/codegen/markdown/templates/enum.md.jinja2 +13 -0
  32. overture/schema/codegen/markdown/templates/feature.md.jinja2 +45 -0
  33. overture/schema/codegen/markdown/templates/geometric.md.jinja2 +11 -0
  34. overture/schema/codegen/markdown/templates/newtype.md.jinja2 +17 -0
  35. overture/schema/codegen/markdown/templates/numeric.md.jinja2 +27 -0
  36. overture/schema/codegen/markdown/templates/pydantic_type.md.jinja2 +8 -0
  37. overture/schema/codegen/markdown/type_format.py +383 -0
  38. overture/schema/codegen/py.typed +0 -0
  39. overture/schema/codegen/pyspark/__init__.py +1 -0
  40. overture/schema/codegen/pyspark/_primitive_fill.py +23 -0
  41. overture/schema/codegen/pyspark/_render_common.py +477 -0
  42. overture/schema/codegen/pyspark/check_builder.py +961 -0
  43. overture/schema/codegen/pyspark/check_ir.py +223 -0
  44. overture/schema/codegen/pyspark/constraint_dispatch.py +753 -0
  45. overture/schema/codegen/pyspark/pipeline.py +220 -0
  46. overture/schema/codegen/pyspark/renderer.py +816 -0
  47. overture/schema/codegen/pyspark/schema_builder.py +187 -0
  48. overture/schema/codegen/pyspark/templates/_check_function.py.jinja2 +10 -0
  49. overture/schema/codegen/pyspark/templates/model_module.py.jinja2 +83 -0
  50. overture/schema/codegen/pyspark/templates/test_module.py.jinja2 +129 -0
  51. overture/schema/codegen/pyspark/test_data/__init__.py +9 -0
  52. overture/schema/codegen/pyspark/test_data/base_row.py +835 -0
  53. overture/schema/codegen/pyspark/test_data/constraint_values.py +203 -0
  54. overture/schema/codegen/pyspark/test_data/invalid_value.py +105 -0
  55. overture/schema/codegen/pyspark/test_data/scaffold.py +390 -0
  56. overture/schema/codegen/pyspark/test_renderer.py +708 -0
  57. overture/schema/codegen/spec_discovery.py +66 -0
  58. overture_schema_codegen-0.1.1.dev0.dist-info/METADATA +13 -0
  59. overture_schema_codegen-0.1.1.dev0.dist-info/RECORD +61 -0
  60. overture_schema_codegen-0.1.1.dev0.dist-info/WHEEL +4 -0
  61. 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]