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,223 @@
1
+ """Tree-shaped IR for PySpark check expressions.
2
+
3
+ Sum types describe each check's structural placement:
4
+
5
+ - `Check.target: FieldPath` -- a `Direct` or `Iterated` locating
6
+ where the descriptor's expression is evaluated. The choice of variant
7
+ signals whether the renderer wraps the expression in an iteration fold
8
+ (`array_check` / `map_values_check` / their nested variants).
9
+ - `Guard` -- a single discriminator gate. `Check.guards` is a tuple
10
+ of `Guard`s AND-composed at render time; nested-union gating
11
+ composes one `ColumnGuard` with one `ElementGuard`.
12
+
13
+ The check_builder produces these types and the renderer consumes them.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+ from typing import TypeAlias
20
+
21
+ from overture.schema.system.field_path import (
22
+ Direct,
23
+ FieldPath,
24
+ Iterated,
25
+ StructSegment,
26
+ )
27
+
28
+ from .constraint_dispatch import (
29
+ ExpressionDescriptor,
30
+ ForbidIf,
31
+ MinFieldsSet,
32
+ ModelConstraintDescriptor,
33
+ RadioGroup,
34
+ RequireAnyOf,
35
+ RequireAnyTrue,
36
+ RequireIf,
37
+ require_field_eq,
38
+ )
39
+
40
+ __all__ = [
41
+ "Check",
42
+ "ColumnGuard",
43
+ "ElementGuard",
44
+ "Guard",
45
+ "ModelCheck",
46
+ ]
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ColumnGuard:
51
+ """Discriminator gate where the discriminator is a top-level row column."""
52
+
53
+ discriminator: str
54
+ values: tuple[str, ...]
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class ElementGuard:
59
+ """Discriminator gate where the discriminator is a struct field inside an array element."""
60
+
61
+ discriminator: str
62
+ values: tuple[str, ...]
63
+
64
+
65
+ Guard: TypeAlias = ColumnGuard | ElementGuard
66
+
67
+
68
+ def _top_level(name: str) -> str:
69
+ """Strip a dotted field name to its top-level column."""
70
+ return name.split(".", 1)[0]
71
+
72
+
73
+ def _path_top_column(path: FieldPath) -> str | None:
74
+ """Top-level row column for a `FieldPath`, or `None` for an empty `Direct`.
75
+
76
+ Collapses dotted struct navigation to its first segment -- the granularity
77
+ at which `validate_model` detects column absence. `Iterated.outer_column`
78
+ may be dotted when the iterated column is nested inside a struct (e.g.
79
+ `names.rules`); this strips to `names`.
80
+ """
81
+ match path:
82
+ case Direct(segments=(StructSegment(name=first), *_)):
83
+ return first
84
+ case Direct():
85
+ return None
86
+ case Iterated():
87
+ return _top_level(path.outer_column)
88
+ case _:
89
+ raise TypeError(f"Unhandled FieldPath variant: {type(path).__name__}")
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class Check:
94
+ """A field-level validation check."""
95
+
96
+ descriptors: tuple[ExpressionDescriptor, ...]
97
+ target: FieldPath
98
+ guards: tuple[Guard, ...] = ()
99
+
100
+ @property
101
+ def read_columns(self) -> frozenset[str]:
102
+ """Top-level row columns this check's expression dereferences.
103
+
104
+ Includes the target's outermost column, any `ColumnGuard` discriminator
105
+ (rendered as `F.col(...)`), and any descriptor gate on a `Direct`
106
+ target (rendered as `F.col("{gate}").isNotNull()`). `ElementGuard`
107
+ discriminators are excluded -- they reference `el[...]`, an
108
+ element-relative accessor, not a row-level column. Descriptor gates on
109
+ `Iterated` targets are also excluded -- they are applied element-relatively
110
+ via `element_relative_gate`.
111
+ """
112
+ cols: set[str] = set()
113
+ top = _path_top_column(self.target)
114
+ if top is not None:
115
+ cols.add(top)
116
+ for guard in self.guards:
117
+ match guard:
118
+ case ColumnGuard(discriminator=d):
119
+ cols.add(d)
120
+ case ElementGuard():
121
+ pass # element-relative: not a row-level read
122
+ case _:
123
+ raise TypeError(f"Unhandled Guard variant: {type(guard).__name__}")
124
+ if isinstance(self.target, Direct):
125
+ for desc in self.descriptors:
126
+ if desc.gate is not None:
127
+ gate_col = _path_top_column(desc.gate)
128
+ if gate_col is not None:
129
+ cols.add(gate_col)
130
+ return frozenset(cols)
131
+
132
+
133
+ @dataclass(frozen=True, slots=True)
134
+ class ModelCheck:
135
+ """A model-level validation check (cross-field constraint).
136
+
137
+ `target` locates the model the constraint applies to: an empty
138
+ `Direct()` for row-root constraints, or an `Iterated` when the
139
+ constrained model is reached by iterating one or more arrays or maps.
140
+ The default `Direct()` makes the row-root case ergonomic at
141
+ construction sites and is the common case; `Check.target` has no
142
+ sensible default and is required.
143
+
144
+ `arm` records the discriminator value of the union member that
145
+ contributed the constraint, or `None` when the constraint applies to
146
+ every arm. The test renderer filters per-arm test modules by this
147
+ value. Constraints discovered through a variant-specific field's
148
+ sub-model or sub-union inherit the contributing outer arm, so they
149
+ land only in that arm's test module.
150
+
151
+ `gate` is the optional-ancestor path that must be non-null for the
152
+ constraint to apply. Set when the constrained model is reached via
153
+ an optional field (`field: Model | None`). The renderer wraps the
154
+ constraint expression in `F.when(<accessor>.isNotNull(), ...)` so
155
+ the check is skipped when the optional model is absent (NULL).
156
+ `gate` is always applied element-relatively for array targets and
157
+ must be `None` for scalar targets, so it never contributes a
158
+ top-level row column to `read_columns`.
159
+ """
160
+
161
+ descriptor: ModelConstraintDescriptor
162
+ target: FieldPath = Direct()
163
+ arm: str | None = None
164
+ gate: FieldPath | None = None
165
+
166
+ @property
167
+ def read_columns(self) -> frozenset[str]:
168
+ """Top-level row columns this model check's expression dereferences.
169
+
170
+ For row-root constraints (`Direct` target): all `field_names` from
171
+ the constraint (collapsed to top-level column) and, for `RequireIf`/
172
+ `ForbidIf`, the condition field (both rendered as `F.col(...)`).
173
+
174
+ For `Iterated` (array/map) targets: only the outermost container
175
+ column is a row-level read (`array_check("col", ...)` /
176
+ `map_values_check("col", ...)`). The `field_names` and condition field
177
+ are accessed as element-relative `el[...]` / `inner[...]` accessors
178
+ inside the lambda -- not as `F.col(...)` -- so they do not contribute
179
+ top-level column reads.
180
+
181
+ `gate` is excluded: for `Iterated` targets it is element-relative; for
182
+ `Direct` targets the renderer asserts it is `None`. The `arm` field
183
+ carries no column information.
184
+ """
185
+ cols: set[str] = set()
186
+ desc = self.descriptor
187
+ # Iterated targets wrap everything in array_check/map_values_check;
188
+ # field references inside the lambda are element-relative, not row-level.
189
+ # Only the container column itself is a top-level read.
190
+ if isinstance(self.target, Iterated):
191
+ container_col = _path_top_column(self.target)
192
+ if container_col is not None:
193
+ cols.add(container_col)
194
+ return frozenset(cols)
195
+ # Struct-nested target (non-empty Direct): every field and condition
196
+ # reference qualifies to `<top>.<field>`, and the gate is a struct
197
+ # prefix of the target, so the sole top-level column read is the
198
+ # target's first segment.
199
+ struct_top = _path_top_column(self.target)
200
+ if struct_top is not None:
201
+ return frozenset({struct_top})
202
+ # Row-root target: field_names and condition field render as F.col(...).
203
+ match desc:
204
+ case (
205
+ RequireAnyOf(field_names=names)
206
+ | RadioGroup(field_names=names)
207
+ | RequireAnyTrue(field_names=names)
208
+ | MinFieldsSet(field_names=names)
209
+ ):
210
+ for name in names:
211
+ cols.add(_top_level(name))
212
+ case (
213
+ RequireIf(field_names=names, condition=cond)
214
+ | ForbidIf(field_names=names, condition=cond)
215
+ ):
216
+ for name in names:
217
+ cols.add(_top_level(name))
218
+ cols.add(require_field_eq(cond).field_name)
219
+ case _:
220
+ raise TypeError(
221
+ f"Unhandled ModelConstraintDescriptor variant: {type(desc).__name__}"
222
+ )
223
+ return frozenset(cols)