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,220 @@
|
|
|
1
|
+
"""PySpark generation pipeline: produce modules without I/O.
|
|
2
|
+
|
|
3
|
+
Orchestrates check building, schema building, and rendering into
|
|
4
|
+
GeneratedModule objects. The caller decides what to do with them (write
|
|
5
|
+
to disk, stream to stdout, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import PurePosixPath
|
|
13
|
+
|
|
14
|
+
from overture.schema.system.case import to_snake_case
|
|
15
|
+
from overture.schema.system.discovery import entry_point_to_path
|
|
16
|
+
from overture.schema.system.geometric import GeometryType
|
|
17
|
+
|
|
18
|
+
from ..extraction.specs import ModelSpec, UnionSpec
|
|
19
|
+
from .check_builder import build_checks
|
|
20
|
+
from .check_ir import Check, ModelCheck
|
|
21
|
+
from .renderer import render_model_module
|
|
22
|
+
from .schema_builder import build_schema
|
|
23
|
+
from .test_data.base_row import (
|
|
24
|
+
generate_arm_rows,
|
|
25
|
+
generate_base_row,
|
|
26
|
+
generate_populated_arm_rows,
|
|
27
|
+
generate_populated_row,
|
|
28
|
+
)
|
|
29
|
+
from .test_renderer import render_test_module
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"GeneratedModule",
|
|
33
|
+
"PipelineOutput",
|
|
34
|
+
"generate_pyspark_module",
|
|
35
|
+
"generate_pyspark_modules",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class GeneratedModule:
|
|
41
|
+
"""A generated Python module with its content and output path."""
|
|
42
|
+
|
|
43
|
+
content: str
|
|
44
|
+
path: PurePosixPath
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class PipelineOutput:
|
|
49
|
+
"""PySpark modules emitted by the pipeline, split by output tree.
|
|
50
|
+
|
|
51
|
+
Source and test modules write to separate directories
|
|
52
|
+
(`--output-dir` and `--test-output-dir`), so they travel as two
|
|
53
|
+
lists rather than one. Both trees mirror the same relative layout,
|
|
54
|
+
so a path is meaningful only relative to its own tree.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
source: list[GeneratedModule]
|
|
58
|
+
test: list[GeneratedModule]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_OUTPUT_PACKAGE = "overture.schema.pyspark.expressions.generated"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _require_entry_point(spec: ModelSpec) -> str:
|
|
65
|
+
"""Return *spec*'s entry point or raise if it's missing."""
|
|
66
|
+
if spec.entry_point is None:
|
|
67
|
+
msg = f"ModelSpec {spec.name!r} has no entry_point."
|
|
68
|
+
raise ValueError(msg)
|
|
69
|
+
return spec.entry_point
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _directory_and_model_name(spec: ModelSpec) -> tuple[PurePosixPath, str]:
|
|
73
|
+
"""Return the output directory and snake_case model name for a spec.
|
|
74
|
+
|
|
75
|
+
Both halves derive from the entry-point's class name so filenames
|
|
76
|
+
and symbol names stay in sync with what the runtime registry
|
|
77
|
+
discovers.
|
|
78
|
+
"""
|
|
79
|
+
directory, cls_name = entry_point_to_path(_require_entry_point(spec))
|
|
80
|
+
return directory, to_snake_case(cls_name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _extract_geometry_types(
|
|
84
|
+
field_checks: list[Check],
|
|
85
|
+
) -> tuple[GeometryType, ...]:
|
|
86
|
+
"""Collect allowed geometry types from every `check_geometry_type` descriptor.
|
|
87
|
+
|
|
88
|
+
A model may carry multiple `check_geometry_type` descriptors -- e.g.
|
|
89
|
+
one per union arm with a distinct allowed-types set. The result is the
|
|
90
|
+
union of all of them, sorted by name for deterministic output.
|
|
91
|
+
"""
|
|
92
|
+
seen: set[GeometryType] = set()
|
|
93
|
+
for check in field_checks:
|
|
94
|
+
for desc in check.descriptors:
|
|
95
|
+
if desc.function != "check_geometry_type":
|
|
96
|
+
continue
|
|
97
|
+
for arg in desc.args:
|
|
98
|
+
if isinstance(arg, GeometryType):
|
|
99
|
+
seen.add(arg)
|
|
100
|
+
return tuple(sorted(seen, key=lambda g: g.name))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def generate_pyspark_module(spec: ModelSpec) -> GeneratedModule:
|
|
104
|
+
"""Generate a PySpark validation module from a model spec.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
spec
|
|
109
|
+
The extracted model spec to generate from.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
GeneratedModule
|
|
114
|
+
Module content and a relative output path mirroring the
|
|
115
|
+
model's entry-point package layout.
|
|
116
|
+
"""
|
|
117
|
+
return _render_module(spec, build_checks(spec))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def generate_pyspark_modules(
|
|
121
|
+
model_specs: Sequence[ModelSpec],
|
|
122
|
+
) -> PipelineOutput:
|
|
123
|
+
"""Generate PySpark validation modules for all models.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
model_specs
|
|
128
|
+
Extracted model specs to generate from.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
PipelineOutput
|
|
133
|
+
Source-tree model modules and test-tree modules. The generated
|
|
134
|
+
tree is PEP 420, so no `__init__.py` files are emitted.
|
|
135
|
+
"""
|
|
136
|
+
items = [(spec, build_checks(spec)) for spec in model_specs]
|
|
137
|
+
source = [_render_module(spec, checks) for spec, checks in items]
|
|
138
|
+
test: list[GeneratedModule] = []
|
|
139
|
+
for spec, checks in items:
|
|
140
|
+
test.extend(_render_test_modules(spec, checks))
|
|
141
|
+
return PipelineOutput(source=source, test=test)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _render_module(
|
|
145
|
+
spec: ModelSpec,
|
|
146
|
+
checks: tuple[list[Check], list[ModelCheck]],
|
|
147
|
+
) -> GeneratedModule:
|
|
148
|
+
"""Build checks, schema, and render for a model spec."""
|
|
149
|
+
field_checks, model_checks = checks
|
|
150
|
+
schema_fields = build_schema(spec)
|
|
151
|
+
geometry_types = _extract_geometry_types(field_checks)
|
|
152
|
+
directory, model_name = _directory_and_model_name(spec)
|
|
153
|
+
content = render_model_module(
|
|
154
|
+
model_name,
|
|
155
|
+
field_checks,
|
|
156
|
+
model_checks,
|
|
157
|
+
schema_fields,
|
|
158
|
+
geometry_types,
|
|
159
|
+
entry_point=_require_entry_point(spec),
|
|
160
|
+
partitions=spec.partitions,
|
|
161
|
+
)
|
|
162
|
+
return GeneratedModule(
|
|
163
|
+
content=content,
|
|
164
|
+
path=directory / f"{model_name}.py",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _select_arm_rows(
|
|
169
|
+
spec: ModelSpec,
|
|
170
|
+
) -> dict[str | None, tuple[dict[str, object], dict[str, object]]]:
|
|
171
|
+
"""Map each test module's arm key to its (sparse, populated) base rows.
|
|
172
|
+
|
|
173
|
+
Multi-arm unions key by discriminator value (one entry per arm); other
|
|
174
|
+
specs use a single `None` key. Either way the caller iterates the dict
|
|
175
|
+
to emit one test module per entry.
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(spec, UnionSpec) and spec.discriminator_field:
|
|
178
|
+
sparse_arm_rows = generate_arm_rows(spec)
|
|
179
|
+
populated_arm_rows = generate_populated_arm_rows(spec)
|
|
180
|
+
return {
|
|
181
|
+
arm: (sparse_arm_rows[arm], populated_arm_rows[arm])
|
|
182
|
+
for arm in sparse_arm_rows
|
|
183
|
+
}
|
|
184
|
+
return {None: (generate_base_row(spec), generate_populated_row(spec))}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _render_test_modules(
|
|
188
|
+
spec: ModelSpec,
|
|
189
|
+
checks: tuple[list[Check], list[ModelCheck]],
|
|
190
|
+
) -> list[GeneratedModule]:
|
|
191
|
+
"""Render test modules for a model spec.
|
|
192
|
+
|
|
193
|
+
For union specs with multiple discriminator arms, produces one
|
|
194
|
+
test module per arm. Each arm's test includes the field and
|
|
195
|
+
model checks tagged with that arm (or untagged), filtered by
|
|
196
|
+
`render_test_module`.
|
|
197
|
+
"""
|
|
198
|
+
field_checks, model_checks = checks
|
|
199
|
+
directory, model_name = _directory_and_model_name(spec)
|
|
200
|
+
expression_import = ".".join([_OUTPUT_PACKAGE, *directory.parts, model_name])
|
|
201
|
+
|
|
202
|
+
modules: list[GeneratedModule] = []
|
|
203
|
+
for arm, (base_row_sparse, base_row_populated) in _select_arm_rows(spec).items():
|
|
204
|
+
suffix = f"_{arm}" if arm is not None else ""
|
|
205
|
+
modules.append(
|
|
206
|
+
GeneratedModule(
|
|
207
|
+
content=render_test_module(
|
|
208
|
+
model_name,
|
|
209
|
+
field_checks,
|
|
210
|
+
model_checks,
|
|
211
|
+
base_row_sparse=base_row_sparse,
|
|
212
|
+
base_row_populated=base_row_populated,
|
|
213
|
+
arm=arm,
|
|
214
|
+
spec=spec,
|
|
215
|
+
expression_import=expression_import,
|
|
216
|
+
),
|
|
217
|
+
path=directory / f"test_{model_name}{suffix}.py",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
return modules
|