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,33 @@
1
+ """Pydantic built-in type extraction."""
2
+
3
+ import re
4
+
5
+ from .docstring import first_docstring_line
6
+ from .specs import PydanticTypeSpec
7
+
8
+ __all__ = ["extract_pydantic_type"]
9
+
10
+ # Matches bare admonition labels like "Info:" or "Note:" with no following text.
11
+ _ADMONITION_LABEL = re.compile(r"^\w+:\s*$")
12
+
13
+
14
+ def _usable_description(doc: str | None) -> str | None:
15
+ """Return the first docstring line, or None if it's an admonition label."""
16
+ line = first_docstring_line(doc)
17
+ if line is None or _ADMONITION_LABEL.match(line):
18
+ return None
19
+ return line
20
+
21
+
22
+ def extract_pydantic_type(cls: type) -> PydanticTypeSpec:
23
+ """Extract a PydanticTypeSpec from a Pydantic built-in type class."""
24
+ module = getattr(cls, "__module__", "")
25
+ if not module.startswith("pydantic"):
26
+ msg = f"Expected a pydantic type, got {cls!r} from {module!r}"
27
+ raise ValueError(msg)
28
+ return PydanticTypeSpec(
29
+ name=cls.__name__,
30
+ description=_usable_description(cls.__doc__),
31
+ source_type=cls,
32
+ source_module=cls.__module__.removeprefix("pydantic."),
33
+ )
@@ -0,0 +1,295 @@
1
+ """Data types for extracted specifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, TypeAlias, TypeGuard
9
+
10
+ from annotated_types import Interval
11
+ from pydantic import BaseModel, RootModel
12
+
13
+ from overture.schema.system.discovery.tag import get_values_for_key
14
+ from overture.schema.system.model_constraint import ModelConstraint
15
+
16
+ from .field import FieldShape
17
+ from .type_analyzer import capture_union_members
18
+
19
+ __all__ = [
20
+ "AnnotatedField",
21
+ "EnumMemberSpec",
22
+ "EnumSpec",
23
+ "ModelSpec",
24
+ "FieldSpec",
25
+ "MemberSpec",
26
+ "RecordSpec",
27
+ "NewTypeSpec",
28
+ "NumericSpec",
29
+ "PydanticTypeSpec",
30
+ "SupplementarySpec",
31
+ "TypeIdentity",
32
+ "filter_model_classes",
33
+ "is_model_class",
34
+ "is_pydantic_sourced",
35
+ "is_rootmodel",
36
+ "is_union_alias",
37
+ "partitions_from_tags",
38
+ ]
39
+
40
+
41
+ def partitions_from_tags(tags: frozenset[str]) -> dict[str, str]:
42
+ """Map registry tags to Hive partition columns for a feature.
43
+
44
+ Today populated only from `overture:theme=<name>`; the value object is
45
+ a generic name -> value map so additional partition keys (e.g. release
46
+ version) can be added without changing the surrounding pipeline.
47
+ """
48
+ theme = next(iter(get_values_for_key(tags, "overture:theme")), None)
49
+ return {"theme": theme} if theme is not None else {}
50
+
51
+
52
+ @dataclass(frozen=True, eq=False)
53
+ class TypeIdentity:
54
+ """Unique identity for a type in the codegen system.
55
+
56
+ Pairs a unique Python object (class, NewType callable, or union
57
+ annotation) with its display name. Equality and hashing delegate
58
+ to `obj` identity so registry lookups work regardless of how
59
+ the display name was derived.
60
+ """
61
+
62
+ obj: object
63
+ name: str
64
+
65
+ @classmethod
66
+ def of(cls, obj: object) -> TypeIdentity:
67
+ """Derive a TypeIdentity from a named object (class, NewType, etc.)."""
68
+ name = getattr(obj, "__name__", None)
69
+ if name is None:
70
+ raise TypeError(f"Cannot derive TypeIdentity from {obj!r}: no __name__")
71
+ return cls(obj, name)
72
+
73
+ def __eq__(self, other: object) -> bool:
74
+ return isinstance(other, TypeIdentity) and self.obj is other.obj
75
+
76
+ def __hash__(self) -> int:
77
+ return id(self.obj)
78
+
79
+ @property
80
+ def module(self) -> str:
81
+ """Source module of the underlying object, or empty string."""
82
+ return getattr(self.obj, "__module__", "")
83
+
84
+
85
+ class _SourceTypeIdentityMixin:
86
+ """Mixin providing `identity` from `source_type` and `name`.
87
+
88
+ Shared by EnumSpec, RecordSpec, NewTypeSpec, and PydanticTypeSpec --
89
+ each has a `source_type` (the Python class/callable) and a `name`.
90
+ UnionSpec uses `source_annotation` instead, so it defines its
91
+ own `identity`.
92
+ """
93
+
94
+ source_type: object | None
95
+ name: str
96
+
97
+ @property
98
+ def identity(self) -> TypeIdentity:
99
+ if self.source_type is None:
100
+ raise ValueError(f"Cannot derive identity for {self.name}: no source_type")
101
+ return TypeIdentity(self.source_type, self.name)
102
+
103
+
104
+ @dataclass
105
+ class EnumMemberSpec:
106
+ """Specification for an enum member."""
107
+
108
+ name: str
109
+ value: str
110
+ description: str | None
111
+
112
+
113
+ @dataclass
114
+ class EnumSpec(_SourceTypeIdentityMixin):
115
+ """Specification for an Enum class."""
116
+
117
+ name: str
118
+ description: str | None
119
+ members: list[EnumMemberSpec] = field(default_factory=list)
120
+ source_type: type | None = None
121
+
122
+
123
+ @dataclass
124
+ class FieldSpec:
125
+ """Specification for a model field: header metadata plus structural shape.
126
+
127
+ `shape` is the full `FieldShape` tree, including any sub-model
128
+ (`ModelRef`) and sub-union (`UnionRef`) references already
129
+ resolved during extraction.
130
+ """
131
+
132
+ name: str
133
+ shape: FieldShape
134
+ description: str | None = None
135
+ is_required: bool = True
136
+ is_optional: bool = False
137
+
138
+
139
+ @dataclass
140
+ class RecordSpec(_SourceTypeIdentityMixin):
141
+ """Specification for a Pydantic model."""
142
+
143
+ name: str
144
+ description: str | None
145
+ fields: list[FieldSpec] = field(default_factory=list)
146
+ source_type: type[BaseModel] | None = None
147
+ entry_point: str | None = None
148
+ partitions: Mapping[str, str] = field(default_factory=dict)
149
+ constraints: tuple[ModelConstraint, ...] = ()
150
+
151
+
152
+ @dataclass
153
+ class AnnotatedField:
154
+ """A FieldSpec paired with union variant provenance."""
155
+
156
+ field_spec: FieldSpec
157
+ variant_sources: tuple[type[BaseModel], ...] | None
158
+
159
+
160
+ @dataclass
161
+ class MemberSpec:
162
+ """A union member's class paired with its extracted `RecordSpec`.
163
+
164
+ `extract_union` already runs `extract_model` on every member to
165
+ build the merged `annotated_fields`; retaining the result here lets
166
+ consumers (check builder, base-row generator) reuse it instead of
167
+ re-extracting the same subtree.
168
+ """
169
+
170
+ member_cls: type[BaseModel]
171
+ spec: RecordSpec
172
+
173
+
174
+ # eq=False: contains mutable lists and a cached_property, so the
175
+ # dataclass-generated __eq__ would compare by value over mutable fields and
176
+ # __hash__ would be disabled (unhashable). Consumers key on object identity.
177
+ @dataclass(eq=False)
178
+ class UnionSpec:
179
+ """Specification for a discriminated union type alias."""
180
+
181
+ name: str
182
+ description: str | None
183
+ annotated_fields: list[AnnotatedField]
184
+ members: list[type[BaseModel]]
185
+ discriminator_field: str | None
186
+ discriminator_mapping: dict[str, type[BaseModel]] | None
187
+ source_annotation: object
188
+ common_base: type[BaseModel]
189
+ member_specs: list[MemberSpec] = field(default_factory=list)
190
+ source_type: type[BaseModel] | None = field(default=None, init=False)
191
+ entry_point: str | None = None
192
+ partitions: Mapping[str, str] = field(default_factory=dict)
193
+ constraints: tuple[ModelConstraint, ...] = ()
194
+
195
+ @functools.cached_property
196
+ def fields(self) -> list[FieldSpec]:
197
+ """Plain field list for tree expansion and supplementary collection."""
198
+ return [af.field_spec for af in self.annotated_fields]
199
+
200
+ @property
201
+ def identity(self) -> TypeIdentity:
202
+ return TypeIdentity(self.source_annotation, self.name)
203
+
204
+
205
+ @dataclass
206
+ class NewTypeSpec(_SourceTypeIdentityMixin):
207
+ """Specification for a NewType.
208
+
209
+ `shape` is the underlying shape -- i.e. the `inner` of the
210
+ NewType's own `NewTypeShape` wrapper, with the wrapper stripped
211
+ so the NewType isn't a self-reference on its own page.
212
+ """
213
+
214
+ name: str
215
+ description: str | None
216
+ shape: FieldShape
217
+ source_type: object | None = None
218
+
219
+
220
+ @dataclass
221
+ class NumericSpec:
222
+ """Extracted specification for a numeric type."""
223
+
224
+ name: str
225
+ description: str | None
226
+ bounds: Interval = field(default_factory=Interval)
227
+ float_bits: int | None = None
228
+
229
+
230
+ @dataclass
231
+ class PydanticTypeSpec(_SourceTypeIdentityMixin):
232
+ """Specification for a Pydantic built-in type (HttpUrl, EmailStr, etc.)."""
233
+
234
+ name: str
235
+ description: str | None
236
+ source_type: type
237
+ source_module: str
238
+
239
+ @property
240
+ def docs_url(self) -> str:
241
+ """Pydantic documentation URL for this type."""
242
+ return (
243
+ f"https://docs.pydantic.dev/latest/api/{self.source_module}"
244
+ f"/#pydantic.{self.source_module}.{self.name}"
245
+ )
246
+
247
+
248
+ ModelSpec: TypeAlias = RecordSpec | UnionSpec
249
+ """A model is one record, or a tagged union of records.
250
+
251
+ The top-level type passed through the extraction pipeline. Consumers
252
+ narrow with `isinstance` when an arm-specific attribute is needed
253
+ (e.g. `UnionSpec.discriminator_field`).
254
+ """
255
+
256
+ SupplementarySpec = EnumSpec | NewTypeSpec | RecordSpec | PydanticTypeSpec
257
+ """Supplementary types referenced by models.
258
+
259
+ Excludes NumericSpec and geometry types, which are extracted
260
+ separately via dedicated functions.
261
+ """
262
+
263
+
264
+ def is_pydantic_sourced(source_type: type | None) -> bool:
265
+ """Check whether *source_type* originates from the `pydantic` package."""
266
+ return getattr(source_type, "__module__", "").startswith("pydantic")
267
+
268
+
269
+ def is_model_class(obj: object) -> TypeGuard[type[BaseModel]]:
270
+ """Check whether *obj* is a concrete BaseModel subclass (not a type alias)."""
271
+ return isinstance(obj, type) and issubclass(obj, BaseModel)
272
+
273
+
274
+ def is_rootmodel(obj: object) -> TypeGuard[type[RootModel]]:
275
+ """Check whether *obj* is a `RootModel` subclass.
276
+
277
+ A RootModel is a `BaseModel` (so `is_model_class` also accepts it) but
278
+ serializes as its bare root value rather than a struct of fields. It
279
+ has no record structure to extract, so callers treat it apart from a
280
+ plain model class.
281
+ """
282
+ return isinstance(obj, type) and issubclass(obj, RootModel)
283
+
284
+
285
+ def is_union_alias(obj: object) -> bool:
286
+ """Check whether *obj* is a discriminated union type alias of BaseModel subclasses."""
287
+ return capture_union_members(obj) is not None
288
+
289
+
290
+ def filter_model_classes(models: dict[Any, Any]) -> list[type[BaseModel]]:
291
+ """Filter discovered models to concrete BaseModel subclasses.
292
+
293
+ Excludes type aliases (like discriminated unions) and non-class entries.
294
+ """
295
+ return [v for v in models.values() if is_model_class(v)]