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,70 @@
1
+ """Relative link computation between rendered output files."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import PurePosixPath
5
+
6
+ from overture.schema.system.case import to_snake_case
7
+
8
+ from ..extraction.specs import TypeIdentity
9
+
10
+ __all__ = ["LinkContext", "relative_link"]
11
+
12
+
13
+ @dataclass
14
+ class LinkContext:
15
+ """Placement context for resolving cross-directory markdown links."""
16
+
17
+ page_path: PurePosixPath
18
+ registry: dict[TypeIdentity, PurePosixPath]
19
+
20
+ def resolve_link(self, identity: TypeIdentity) -> str | None:
21
+ """Resolve *identity* to a relative link if it exists in the registry."""
22
+ if identity in self.registry:
23
+ return relative_link(self.page_path, self.registry[identity])
24
+ return None
25
+
26
+ def resolve_link_or_slug(self, identity: TypeIdentity) -> str:
27
+ """Resolve *identity* to a relative link, falling back to a slug filename.
28
+
29
+ Always returns a usable link string. Use when the caller needs a
30
+ link regardless of whether the type has a registered page.
31
+ """
32
+ return self.resolve_link(identity) or f"{to_snake_case(identity.name)}.md"
33
+
34
+
35
+ def _is_normalized(path: PurePosixPath) -> bool:
36
+ """Check whether the path contains no '..' or '.' components (except root '.')."""
37
+ return ".." not in path.parts and path.parts.count(".") <= 1
38
+
39
+
40
+ def relative_link(source: PurePosixPath, target: PurePosixPath) -> str:
41
+ """Compute a relative path from source file to target file.
42
+
43
+ Both paths must be normalized (no `..` components) and relative
44
+ to the same output root.
45
+ """
46
+ if not _is_normalized(source):
47
+ msg = f"Source path not normalized: {source}"
48
+ raise ValueError(msg)
49
+ if not _is_normalized(target):
50
+ msg = f"Target path not normalized: {target}"
51
+ raise ValueError(msg)
52
+ source_dir = source.parent
53
+ # Count how many levels up from source_dir to common ancestor,
54
+ # then descend to target. PurePosixPath doesn't have os.path.relpath,
55
+ # so compute manually.
56
+ source_parts = source_dir.parts
57
+ target_parts = target.parts
58
+
59
+ # Find common prefix length
60
+ common = 0
61
+ for s, t in zip(source_parts, target_parts, strict=False):
62
+ if s != t:
63
+ break
64
+ common += 1
65
+
66
+ ups = len(source_parts) - common
67
+ downs = target_parts[common:]
68
+
69
+ parts = [".."] * ups + list(downs)
70
+ return "/".join(parts) if parts else "."
@@ -0,0 +1,114 @@
1
+ """Map types to markdown output file paths.
2
+
3
+ Uses module-mirrored output directories: output paths derive from
4
+ the source Python module path relative to schema_root.
5
+ """
6
+
7
+ from collections.abc import Sequence
8
+ from pathlib import PurePosixPath
9
+
10
+ from overture.schema.system.case import to_snake_case
11
+
12
+ from ..extraction.specs import (
13
+ ModelSpec,
14
+ PydanticTypeSpec,
15
+ SupplementarySpec,
16
+ TypeIdentity,
17
+ )
18
+ from ..layout.module_layout import compute_output_dir, output_dir_for_entry_point
19
+
20
+ __all__ = [
21
+ "GEOMETRIC_PAGE",
22
+ "NUMERIC_PAGE",
23
+ "build_placement_registry",
24
+ "resolve_output_path",
25
+ ]
26
+
27
+ # Aggregate page paths.
28
+ NUMERIC_PAGE = PurePosixPath("system/numeric.md")
29
+ GEOMETRIC_PAGE = PurePosixPath("system/geometric.md")
30
+
31
+
32
+ def build_placement_registry(
33
+ model_specs: Sequence[ModelSpec],
34
+ all_specs: dict[TypeIdentity, SupplementarySpec],
35
+ numeric_names: list[TypeIdentity],
36
+ geometry_names: list[TypeIdentity],
37
+ schema_root: str,
38
+ ) -> dict[TypeIdentity, PurePosixPath]:
39
+ """Build a mapping from TypeIdentity to output file paths.
40
+
41
+ Uses module-mirrored output directories: output paths derive from
42
+ the source Python module path relative to schema_root.
43
+ """
44
+ registry: dict[TypeIdentity, PurePosixPath] = _aggregate_page_entries(
45
+ numeric_names, geometry_names
46
+ )
47
+
48
+ feature_dirs: set[PurePosixPath] = set()
49
+ for spec in model_specs:
50
+ spec_dir = output_dir_for_entry_point(spec.entry_point, schema_root)
51
+ registry[spec.identity] = _md_path(spec_dir, spec.name)
52
+ feature_dirs.add(spec_dir)
53
+
54
+ for tid, supp_spec in all_specs.items():
55
+ if tid in registry:
56
+ continue
57
+ if isinstance(supp_spec, PydanticTypeSpec):
58
+ registry[tid] = _md_path(
59
+ PurePosixPath("pydantic") / supp_spec.source_module, tid.name
60
+ )
61
+ continue
62
+ source_module = getattr(supp_spec.source_type, "__module__", None)
63
+ if source_module is None:
64
+ continue
65
+ output_dir = compute_output_dir(source_module, schema_root)
66
+ output_dir = _nest_under_types(output_dir, feature_dirs)
67
+ registry[tid] = _md_path(output_dir, tid.name)
68
+
69
+ return registry
70
+
71
+
72
+ def resolve_output_path(
73
+ identity: TypeIdentity,
74
+ registry: dict[TypeIdentity, PurePosixPath] | None,
75
+ ) -> PurePosixPath:
76
+ """Look up a type's output path from the registry, with flat-file fallback."""
77
+ if registry is not None and identity in registry:
78
+ return registry[identity]
79
+ return _md_path(PurePosixPath(""), identity.name)
80
+
81
+
82
+ def _aggregate_page_entries(
83
+ numeric_names: list[TypeIdentity],
84
+ geometry_names: list[TypeIdentity],
85
+ ) -> dict[TypeIdentity, PurePosixPath]:
86
+ """Pre-populate registry entries for types documented on aggregate pages."""
87
+ entries: dict[TypeIdentity, PurePosixPath] = dict.fromkeys(
88
+ numeric_names, NUMERIC_PAGE
89
+ )
90
+ entries.update(dict.fromkeys(geometry_names, GEOMETRIC_PAGE))
91
+ return entries
92
+
93
+
94
+ def _nest_under_types(
95
+ output_dir: PurePosixPath, feature_dirs: set[PurePosixPath]
96
+ ) -> PurePosixPath:
97
+ """Insert `types/` after the feature directory portion.
98
+
99
+ If *output_dir* equals or is a subdirectory of a feature directory,
100
+ returns a path with `types/` inserted after the feature directory.
101
+ Otherwise returns *output_dir* unchanged.
102
+ """
103
+ for fd in sorted(feature_dirs, key=lambda p: len(p.parts), reverse=True):
104
+ try:
105
+ relative = output_dir.relative_to(fd)
106
+ except ValueError:
107
+ continue
108
+ return fd / "types" / relative
109
+ return output_dir
110
+
111
+
112
+ def _md_path(directory: PurePosixPath, name: str) -> PurePosixPath:
113
+ """Build a .md file path from a directory and a PascalCase type name."""
114
+ return directory / f"{to_snake_case(name)}.md"
@@ -0,0 +1,198 @@
1
+ """Markdown generation pipeline: render pages without I/O.
2
+
3
+ Orchestrates tree expansion, type collection, placement, reverse
4
+ references, and rendering into a list of RenderedPage objects. The
5
+ caller decides what to do with them (write to disk, add frontmatter,
6
+ stream to stdout, etc.).
7
+ """
8
+
9
+ from collections.abc import Mapping, Sequence
10
+ from dataclasses import dataclass
11
+ from pathlib import PurePosixPath
12
+
13
+ import overture.schema.system.geometric as _system_geometric
14
+ import overture.schema.system.numeric as _system_numeric
15
+ from overture.schema.system.geometric import GeometryType
16
+
17
+ from ..extraction.examples import ExampleRecord, load_examples
18
+ from ..extraction.numeric_extraction import extract_numerics
19
+ from ..extraction.specs import (
20
+ EnumSpec,
21
+ ModelSpec,
22
+ NewTypeSpec,
23
+ PydanticTypeSpec,
24
+ RecordSpec,
25
+ SupplementarySpec,
26
+ TypeIdentity,
27
+ UnionSpec,
28
+ )
29
+ from ..extraction.type_analyzer import is_newtype
30
+ from ..layout.type_collection import collect_all_supplementary_types
31
+ from .link_computation import LinkContext
32
+ from .path_assignment import (
33
+ GEOMETRIC_PAGE,
34
+ NUMERIC_PAGE,
35
+ build_placement_registry,
36
+ resolve_output_path,
37
+ )
38
+ from .renderer import (
39
+ render_enum,
40
+ render_geometry_from_values,
41
+ render_model,
42
+ render_newtype,
43
+ render_numeric_from_specs,
44
+ render_pydantic_type,
45
+ )
46
+ from .reverse_references import UsedByEntry, compute_reverse_references
47
+
48
+ __all__ = [
49
+ "RenderedPage",
50
+ "generate_markdown_pages",
51
+ "partition_numeric_and_geometry_types",
52
+ ]
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class RenderedPage:
57
+ """A rendered page with its content and output path."""
58
+
59
+ content: str
60
+ path: PurePosixPath
61
+ is_model: bool = False
62
+
63
+
64
+ def _load_model_examples(
65
+ spec: ModelSpec,
66
+ ) -> list[ExampleRecord] | None:
67
+ """Load examples for a feature spec, returning None when absent."""
68
+ if isinstance(spec, UnionSpec):
69
+ pyproject_source = spec.members[0] if spec.members else None
70
+ validation_type = spec.source_annotation
71
+ model_fields = spec.common_base.model_fields
72
+ else:
73
+ pyproject_source = spec.source_type
74
+ validation_type = spec.source_type
75
+ model_fields = spec.source_type.model_fields if spec.source_type else {}
76
+ if not pyproject_source:
77
+ return None
78
+ field_names = [f.name for f in spec.fields]
79
+ examples = load_examples(
80
+ validation_type,
81
+ spec.name,
82
+ field_names,
83
+ pyproject_source=pyproject_source,
84
+ model_fields=model_fields,
85
+ )
86
+ return examples or None
87
+
88
+
89
+ def _render_supplement(
90
+ tid: TypeIdentity,
91
+ spec: SupplementarySpec,
92
+ registry: dict[TypeIdentity, PurePosixPath],
93
+ reverse_refs: dict[TypeIdentity, list[UsedByEntry]],
94
+ ) -> RenderedPage:
95
+ """Render a single supplementary type page."""
96
+ output_path = resolve_output_path(tid, registry)
97
+ ctx = LinkContext(output_path, registry)
98
+ used_by = reverse_refs.get(tid)
99
+
100
+ match spec:
101
+ case EnumSpec():
102
+ content = render_enum(spec, link_ctx=ctx, used_by=used_by)
103
+ case NewTypeSpec():
104
+ content = render_newtype(spec, ctx, used_by=used_by)
105
+ case RecordSpec():
106
+ content = render_model(spec, ctx, used_by=used_by)
107
+ case PydanticTypeSpec():
108
+ content = render_pydantic_type(spec, link_ctx=ctx, used_by=used_by)
109
+ case _:
110
+ raise TypeError(
111
+ f"Unhandled SupplementarySpec variant: {type(spec).__name__}"
112
+ )
113
+
114
+ return RenderedPage(content=content, path=output_path)
115
+
116
+
117
+ def partition_numeric_and_geometry_types(
118
+ numeric_module: object,
119
+ geometric_module: object,
120
+ ) -> tuple[list[TypeIdentity], list[TypeIdentity]]:
121
+ """Discover numeric and geometry types from their source modules' exports.
122
+
123
+ NewType exports of *numeric_module* are numeric types.
124
+ Non-constraint class/enum exports of *geometric_module* are geometry types.
125
+ """
126
+ numerics: list[TypeIdentity] = []
127
+ for name in getattr(numeric_module, "__all__", []):
128
+ obj = getattr(numeric_module, name)
129
+ if is_newtype(obj):
130
+ numerics.append(TypeIdentity(obj, name))
131
+
132
+ geometries: list[TypeIdentity] = []
133
+ for name in getattr(geometric_module, "__all__", []):
134
+ obj = getattr(geometric_module, name)
135
+ if isinstance(obj, type) and not name.endswith("Constraint"):
136
+ geometries.append(TypeIdentity(obj, name))
137
+
138
+ return numerics, geometries
139
+
140
+
141
+ def generate_markdown_pages(
142
+ model_specs: Sequence[ModelSpec],
143
+ schema_root: str,
144
+ *,
145
+ external_specs: Mapping[TypeIdentity, SupplementarySpec] | None = None,
146
+ ) -> list[RenderedPage]:
147
+ """Generate all markdown pages from feature specs.
148
+
149
+ Returns rendered pages without writing to disk. The caller handles
150
+ I/O, frontmatter injection, and any output-format-specific concerns
151
+ (like Docusaurus category files).
152
+
153
+ `external_specs` are supplementary types documented on their own but not
154
+ reachable by walking feature field trees -- a `RootModel` entry point,
155
+ which serializes as its bare root value and so appears in no feature as
156
+ a named reference. They join the collected supplementary types and
157
+ render, place, and cross-reference identically.
158
+ """
159
+ numeric_names, geometry_names = partition_numeric_and_geometry_types(
160
+ _system_numeric, _system_geometric
161
+ )
162
+ all_specs = collect_all_supplementary_types(model_specs)
163
+ if external_specs:
164
+ all_specs = {**all_specs, **external_specs}
165
+ registry = build_placement_registry(
166
+ model_specs, all_specs, numeric_names, geometry_names, schema_root
167
+ )
168
+
169
+ reverse_refs = compute_reverse_references(model_specs, all_specs)
170
+
171
+ pages: list[RenderedPage] = []
172
+
173
+ for spec in model_specs:
174
+ output_path = registry[spec.identity]
175
+ ctx = LinkContext(output_path, registry)
176
+ examples = _load_model_examples(spec)
177
+ used_by = reverse_refs.get(spec.identity)
178
+ content = render_model(spec, link_ctx=ctx, examples=examples, used_by=used_by)
179
+ pages.append(RenderedPage(content=content, path=output_path, is_model=True))
180
+
181
+ for tid, supp_spec in all_specs.items():
182
+ pages.append(_render_supplement(tid, supp_spec, registry, reverse_refs))
183
+
184
+ pages.append(
185
+ RenderedPage(
186
+ content=render_numeric_from_specs(extract_numerics(numeric_names)),
187
+ path=NUMERIC_PAGE,
188
+ )
189
+ )
190
+
191
+ pages.append(
192
+ RenderedPage(
193
+ content=render_geometry_from_values([m.value for m in GeometryType]),
194
+ path=GEOMETRIC_PAGE,
195
+ )
196
+ )
197
+
198
+ return pages