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