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,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)
|