overture-schema-cli 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/cli/__init__.py +29 -0
- overture/schema/cli/__main__.py +6 -0
- overture/schema/cli/commands.py +930 -0
- overture/schema/cli/data_display.py +716 -0
- overture/schema/cli/docstrings.py +21 -0
- overture/schema/cli/error_formatting.py +587 -0
- overture/schema/cli/output.py +29 -0
- overture/schema/cli/py.typed +0 -0
- overture/schema/cli/tag_options.py +69 -0
- overture/schema/cli/type_analysis.py +448 -0
- overture/schema/cli/types.py +17 -0
- overture_schema_cli-0.1.1.dev0.dist-info/METADATA +38 -0
- overture_schema_cli-0.1.1.dev0.dist-info/RECORD +15 -0
- overture_schema_cli-0.1.1.dev0.dist-info/WHEEL +4 -0
- overture_schema_cli-0.1.1.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shared Click options for tag-based model selection."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from overture.schema.system.discovery import TagSelector
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable[..., object])
|
|
11
|
+
|
|
12
|
+
# Every tag named here must be one discovery actually emits -- a tag in help
|
|
13
|
+
# text reads as runnable. The namespaced form has no shipped example, so it is
|
|
14
|
+
# described rather than illustrated.
|
|
15
|
+
_TAG_SYNTAX_NOTE = (
|
|
16
|
+
"Accepts plain tags (e.g. feature, overture) and compound key/value tags "
|
|
17
|
+
"(e.g. overture:theme=buildings). A namespaced form, namespace:predicate, "
|
|
18
|
+
"is also accepted for tags that third-party packages register."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def tag_selection_options(func: F) -> F:
|
|
23
|
+
"""Decorate a Click command with --tag, --filter, and --exclude options.
|
|
24
|
+
|
|
25
|
+
The decorated command receives `tags`, `filters`, and `excludes`
|
|
26
|
+
keyword arguments (each a `tuple[str, ...]`), suitable for passing to
|
|
27
|
+
`build_selector`.
|
|
28
|
+
"""
|
|
29
|
+
func = click.option(
|
|
30
|
+
"--exclude",
|
|
31
|
+
"excludes",
|
|
32
|
+
multiple=True,
|
|
33
|
+
help=(
|
|
34
|
+
"Exclude feature types with these tags — removes from scope (OR-NOT; "
|
|
35
|
+
f"repeatable). {_TAG_SYNTAX_NOTE}"
|
|
36
|
+
),
|
|
37
|
+
)(func)
|
|
38
|
+
func = click.option(
|
|
39
|
+
"--filter",
|
|
40
|
+
"filters",
|
|
41
|
+
multiple=True,
|
|
42
|
+
help=(
|
|
43
|
+
"Require feature types to have these tags — narrows scope (AND; "
|
|
44
|
+
f"repeatable). {_TAG_SYNTAX_NOTE}"
|
|
45
|
+
),
|
|
46
|
+
)(func)
|
|
47
|
+
func = click.option(
|
|
48
|
+
"--tag",
|
|
49
|
+
"tags",
|
|
50
|
+
multiple=True,
|
|
51
|
+
help=(
|
|
52
|
+
"Include feature types with these tags — defines scope (OR; repeatable). "
|
|
53
|
+
f"{_TAG_SYNTAX_NOTE}"
|
|
54
|
+
),
|
|
55
|
+
)(func)
|
|
56
|
+
return func
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_selector(
|
|
60
|
+
tags: tuple[str, ...],
|
|
61
|
+
filters: tuple[str, ...],
|
|
62
|
+
excludes: tuple[str, ...],
|
|
63
|
+
) -> TagSelector:
|
|
64
|
+
"""Map `tag_selection_options` arguments to a `TagSelector`."""
|
|
65
|
+
return TagSelector(
|
|
66
|
+
include_any=tags,
|
|
67
|
+
require_all=filters,
|
|
68
|
+
exclude_any=excludes,
|
|
69
|
+
)
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Type introspection and structural analysis for union types."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Annotated as AnnotatedType
|
|
6
|
+
from typing import Any, Literal, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic.fields import FieldInfo
|
|
10
|
+
|
|
11
|
+
from overture.schema.system.feature import resolve_discriminator_field_name
|
|
12
|
+
|
|
13
|
+
from .types import ErrorLocation, ValidationErrorDict
|
|
14
|
+
|
|
15
|
+
# Type aliases for structural tuple elements
|
|
16
|
+
StructuralElement = Literal["list_index", "union", "model", "discriminator", "field"]
|
|
17
|
+
StructuralTuple = tuple[StructuralElement, ...]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class UnionMetadata:
|
|
22
|
+
"""Metadata about a union type's structure."""
|
|
23
|
+
|
|
24
|
+
is_discriminated: bool
|
|
25
|
+
discriminator_field: str | None
|
|
26
|
+
# Map discriminator values to their corresponding model types
|
|
27
|
+
discriminator_to_model: dict[str, type[BaseModel]]
|
|
28
|
+
# Map model class names to their types (for non-discriminated unions)
|
|
29
|
+
model_name_to_model: dict[str, type[BaseModel]]
|
|
30
|
+
# Nested union metadata for union members that are themselves unions
|
|
31
|
+
nested_unions: dict[str, "UnionMetadata"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_literal_value(model: type[BaseModel], field_name: str) -> str | None:
|
|
35
|
+
"""Extract the single Literal value from a model field as a string, if present."""
|
|
36
|
+
field_info = model.model_fields.get(field_name)
|
|
37
|
+
if field_info is None or field_info.annotation is None:
|
|
38
|
+
return None
|
|
39
|
+
if get_origin(field_info.annotation) is Literal:
|
|
40
|
+
args = get_args(field_info.annotation)
|
|
41
|
+
return str(args[0]) if args else None
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _process_union_member(
|
|
46
|
+
member: Any, # noqa: ANN401
|
|
47
|
+
discriminator_to_model: dict[str, type[BaseModel]],
|
|
48
|
+
model_name_to_model: dict[str, type[BaseModel]],
|
|
49
|
+
nested_unions: dict[str, UnionMetadata],
|
|
50
|
+
discriminator_field: str | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Process a single union member, handling nesting recursively.
|
|
53
|
+
|
|
54
|
+
Args
|
|
55
|
+
----
|
|
56
|
+
member: A union member type (could be Annotated, BaseModel, or nested union)
|
|
57
|
+
discriminator_to_model: Dict to populate with discriminator value mappings
|
|
58
|
+
model_name_to_model: Dict to populate with model name mappings
|
|
59
|
+
nested_unions: Dict to populate with nested union metadata
|
|
60
|
+
discriminator_field: The discriminator field name from the parent union annotation
|
|
61
|
+
"""
|
|
62
|
+
member_origin = get_origin(member)
|
|
63
|
+
|
|
64
|
+
# Case 1: Annotated type (might contain nested union or Tag)
|
|
65
|
+
if member_origin is AnnotatedType:
|
|
66
|
+
member_args = get_args(member)
|
|
67
|
+
if not member_args:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Check for discriminator in annotations
|
|
71
|
+
has_discriminator = any(
|
|
72
|
+
isinstance(metadata, FieldInfo) and hasattr(metadata, "discriminator")
|
|
73
|
+
for metadata in member_args[1:]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if has_discriminator or get_origin(member_args[0]) is not None:
|
|
77
|
+
# Nested union (with or without discriminator)
|
|
78
|
+
nested_metadata = introspect_union(member)
|
|
79
|
+
nested_unions[str(member)] = nested_metadata
|
|
80
|
+
discriminator_to_model.update(nested_metadata.discriminator_to_model)
|
|
81
|
+
# The nested union's discriminator_to_model uses the nested discriminator
|
|
82
|
+
# field (e.g. "subtype"). Re-extract using the parent discriminator field
|
|
83
|
+
# (e.g. "type") so leaf models are also reachable by the parent's values.
|
|
84
|
+
if discriminator_field is not None:
|
|
85
|
+
for model in nested_metadata.model_name_to_model.values():
|
|
86
|
+
value = _extract_literal_value(model, discriminator_field)
|
|
87
|
+
if value is not None:
|
|
88
|
+
discriminator_to_model[value] = model
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Unwrap Annotated to get the actual type (e.g., Annotated[Building, Tag('building')])
|
|
92
|
+
# and process it recursively
|
|
93
|
+
_process_union_member(
|
|
94
|
+
member_args[0],
|
|
95
|
+
discriminator_to_model,
|
|
96
|
+
model_name_to_model,
|
|
97
|
+
nested_unions,
|
|
98
|
+
discriminator_field,
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Case 2: BaseModel class
|
|
103
|
+
if inspect.isclass(member) and issubclass(member, BaseModel):
|
|
104
|
+
model_name_to_model[member.__name__] = member
|
|
105
|
+
|
|
106
|
+
if discriminator_field is not None:
|
|
107
|
+
value = _extract_literal_value(member, discriminator_field)
|
|
108
|
+
if value is not None:
|
|
109
|
+
discriminator_to_model[value] = member
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def introspect_union(union_type: Any) -> UnionMetadata: # noqa: ANN401
|
|
113
|
+
"""Introspect a union type to extract structural information.
|
|
114
|
+
|
|
115
|
+
Analyzes a union type (which may be discriminated or non-discriminated) to
|
|
116
|
+
extract metadata about its structure, including discriminator fields, model
|
|
117
|
+
mappings, and nested union information. This metadata is used for structural
|
|
118
|
+
analysis of validation error paths.
|
|
119
|
+
|
|
120
|
+
Args
|
|
121
|
+
----
|
|
122
|
+
union_type: A union type (may be Annotated with discriminator)
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
UnionMetadata describing the structure of the union
|
|
127
|
+
|
|
128
|
+
Examples
|
|
129
|
+
--------
|
|
130
|
+
>>> from typing import Annotated, Union
|
|
131
|
+
>>> from pydantic import Field
|
|
132
|
+
>>> from overture.schema.buildings import Building, BuildingPart
|
|
133
|
+
>>> from overture.schema.transportation import Segment, Connector
|
|
134
|
+
>>> # Discriminated union with 'type' field
|
|
135
|
+
>>> BuildingUnion = Annotated[
|
|
136
|
+
... Union[Building, BuildingPart],
|
|
137
|
+
... Field(discriminator='type')
|
|
138
|
+
... ]
|
|
139
|
+
>>> metadata = introspect_union(BuildingUnion)
|
|
140
|
+
>>> metadata.is_discriminated
|
|
141
|
+
True
|
|
142
|
+
>>> metadata.discriminator_field
|
|
143
|
+
'type'
|
|
144
|
+
>>> 'building' in metadata.discriminator_to_model
|
|
145
|
+
True
|
|
146
|
+
|
|
147
|
+
>>> # Non-discriminated union (using plain Union without discriminator)
|
|
148
|
+
>>> from overture.schema.transportation import Connector
|
|
149
|
+
>>> PlainUnion = Union[Building, Connector]
|
|
150
|
+
>>> metadata = introspect_union(PlainUnion)
|
|
151
|
+
>>> metadata.is_discriminated
|
|
152
|
+
False
|
|
153
|
+
>>> 'Connector' in metadata.model_name_to_model
|
|
154
|
+
True
|
|
155
|
+
|
|
156
|
+
>>> # List of discriminated union (unwraps to element type)
|
|
157
|
+
>>> FeatureList = list[BuildingUnion]
|
|
158
|
+
>>> metadata = introspect_union(FeatureList)
|
|
159
|
+
>>> metadata.is_discriminated
|
|
160
|
+
True
|
|
161
|
+
"""
|
|
162
|
+
# Check if this is a list type - unwrap to get the element type
|
|
163
|
+
origin = get_origin(union_type)
|
|
164
|
+
if origin is list:
|
|
165
|
+
args = get_args(union_type)
|
|
166
|
+
if args:
|
|
167
|
+
# Recursively introspect the list element type
|
|
168
|
+
return introspect_union(args[0])
|
|
169
|
+
|
|
170
|
+
# Check if this is an Annotated type with a discriminator
|
|
171
|
+
discriminator_field = None
|
|
172
|
+
actual_union = union_type
|
|
173
|
+
|
|
174
|
+
# Unwrap Annotated ONLY if the top level is Annotated
|
|
175
|
+
if origin is AnnotatedType:
|
|
176
|
+
# This is Annotated[Union[...], ...]
|
|
177
|
+
args = get_args(union_type)
|
|
178
|
+
if args:
|
|
179
|
+
# First arg is the actual type, rest are metadata
|
|
180
|
+
actual_union = args[0]
|
|
181
|
+
# Look for Field with discriminator in metadata
|
|
182
|
+
for metadata in args[1:]:
|
|
183
|
+
if isinstance(metadata, FieldInfo) and hasattr(
|
|
184
|
+
metadata, "discriminator"
|
|
185
|
+
):
|
|
186
|
+
discriminator_field = resolve_discriminator_field_name(
|
|
187
|
+
metadata.discriminator
|
|
188
|
+
)
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
# Get union members
|
|
192
|
+
union_origin = get_origin(actual_union)
|
|
193
|
+
if union_origin is None:
|
|
194
|
+
# Not a union, might be a single model
|
|
195
|
+
union_members = [actual_union]
|
|
196
|
+
else:
|
|
197
|
+
union_members = list(get_args(actual_union))
|
|
198
|
+
|
|
199
|
+
discriminator_to_model: dict[str, type[BaseModel]] = {}
|
|
200
|
+
model_name_to_model: dict[str, type[BaseModel]] = {}
|
|
201
|
+
nested_unions: dict[str, UnionMetadata] = {}
|
|
202
|
+
|
|
203
|
+
# Process each union member
|
|
204
|
+
for member in union_members:
|
|
205
|
+
_process_union_member(
|
|
206
|
+
member,
|
|
207
|
+
discriminator_to_model,
|
|
208
|
+
model_name_to_model,
|
|
209
|
+
nested_unions,
|
|
210
|
+
discriminator_field,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return UnionMetadata(
|
|
214
|
+
is_discriminated=discriminator_field is not None,
|
|
215
|
+
discriminator_field=discriminator_field,
|
|
216
|
+
discriminator_to_model=discriminator_to_model,
|
|
217
|
+
model_name_to_model=model_name_to_model,
|
|
218
|
+
nested_unions=nested_unions,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_or_create_structural_tuple(
|
|
223
|
+
loc: ErrorLocation,
|
|
224
|
+
metadata: UnionMetadata,
|
|
225
|
+
cache: dict[ErrorLocation, StructuralTuple] | None = None,
|
|
226
|
+
) -> StructuralTuple:
|
|
227
|
+
"""Get structural tuple with optional caching for systematic errors.
|
|
228
|
+
|
|
229
|
+
When validating collections with systematic errors (e.g., same field missing
|
|
230
|
+
across many rows), this cache dramatically reduces redundant classification work.
|
|
231
|
+
|
|
232
|
+
Args
|
|
233
|
+
----
|
|
234
|
+
loc: The location tuple from a Pydantic validation error
|
|
235
|
+
metadata: Pre-computed UnionMetadata from introspect_union()
|
|
236
|
+
cache: Optional dict to cache results (same cache used across all errors)
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
Tuple of same length as loc with structural labels for each element
|
|
241
|
+
"""
|
|
242
|
+
if cache is not None and loc in cache:
|
|
243
|
+
return cache[loc]
|
|
244
|
+
|
|
245
|
+
structural = create_structural_tuple(loc, metadata)
|
|
246
|
+
|
|
247
|
+
if cache is not None:
|
|
248
|
+
cache[loc] = structural
|
|
249
|
+
|
|
250
|
+
return structural
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def create_structural_tuple(
|
|
254
|
+
loc: ErrorLocation,
|
|
255
|
+
metadata: UnionMetadata,
|
|
256
|
+
) -> StructuralTuple:
|
|
257
|
+
"""Create a structural tuple parallel to error['loc'] describing each element.
|
|
258
|
+
|
|
259
|
+
The structural tuple helps identify which parts of an error path are:
|
|
260
|
+
- list_index: Indices from array iteration
|
|
261
|
+
- union: Pydantic's tagged union markers (e.g., 'tagged-union[type]')
|
|
262
|
+
- discriminator: Discriminator values (e.g., 'building', 'segment')
|
|
263
|
+
- model: Model class names in non-discriminated unions (e.g., 'Segment')
|
|
264
|
+
- field: Actual data field names (e.g., 'height', 'id')
|
|
265
|
+
|
|
266
|
+
Args
|
|
267
|
+
----
|
|
268
|
+
loc: The location tuple from a Pydantic validation error
|
|
269
|
+
metadata: Pre-computed UnionMetadata from introspect_union()
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
Tuple of same length as loc with structural labels for each element
|
|
274
|
+
|
|
275
|
+
Examples
|
|
276
|
+
--------
|
|
277
|
+
>>> from typing import Annotated, Union
|
|
278
|
+
>>> from pydantic import Field
|
|
279
|
+
>>> from overture.schema.buildings import Building, BuildingPart
|
|
280
|
+
>>> from overture.schema.transportation import Connector
|
|
281
|
+
>>> BuildingUnion = Annotated[
|
|
282
|
+
... Union[Building, BuildingPart],
|
|
283
|
+
... Field(discriminator='type')
|
|
284
|
+
... ]
|
|
285
|
+
>>> PlainUnion = Union[Building, Connector]
|
|
286
|
+
>>> # Error in first feature of a list, in a building's height field
|
|
287
|
+
>>> loc = (0, 'tagged-union[type]', 'building', 'height')
|
|
288
|
+
>>> metadata = introspect_union(BuildingUnion)
|
|
289
|
+
>>> create_structural_tuple(loc, metadata)
|
|
290
|
+
('list_index', 'union', 'discriminator', 'field')
|
|
291
|
+
|
|
292
|
+
>>> # Error in a non-discriminated union (uses model name)
|
|
293
|
+
>>> loc = ('Connector', 'connectors', 0)
|
|
294
|
+
>>> metadata = introspect_union(PlainUnion)
|
|
295
|
+
>>> create_structural_tuple(loc, metadata)
|
|
296
|
+
('model', 'field', 'list_index')
|
|
297
|
+
|
|
298
|
+
>>> # Simple field error (no union involved)
|
|
299
|
+
>>> loc = ('id',)
|
|
300
|
+
>>> metadata = introspect_union(Building)
|
|
301
|
+
>>> create_structural_tuple(loc, metadata)
|
|
302
|
+
('field',)
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
def classify(element: str | int) -> StructuralElement:
|
|
306
|
+
"""Classify a single location element."""
|
|
307
|
+
if isinstance(element, int):
|
|
308
|
+
return "list_index"
|
|
309
|
+
if isinstance(element, str):
|
|
310
|
+
# Pydantic generates various union marker formats
|
|
311
|
+
if (
|
|
312
|
+
element.startswith("tagged-union[")
|
|
313
|
+
or element.startswith("function-after[")
|
|
314
|
+
or element.startswith("function-wrap[")
|
|
315
|
+
):
|
|
316
|
+
return "union"
|
|
317
|
+
if element in metadata.model_name_to_model:
|
|
318
|
+
return "model"
|
|
319
|
+
if element in metadata.discriminator_to_model:
|
|
320
|
+
return "discriminator"
|
|
321
|
+
return "field"
|
|
322
|
+
|
|
323
|
+
return tuple(classify(e) for e in loc)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_item_index(loc: ErrorLocation) -> int | None:
|
|
327
|
+
"""Extract the top-level list index from an error location, if present.
|
|
328
|
+
|
|
329
|
+
Args
|
|
330
|
+
----
|
|
331
|
+
loc: The location tuple from a Pydantic validation error
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
The list index if the error is within a list item, otherwise None
|
|
336
|
+
"""
|
|
337
|
+
if loc and isinstance(loc[0], int):
|
|
338
|
+
return loc[0]
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def infer_model_from_error(
|
|
343
|
+
error: ValidationErrorDict,
|
|
344
|
+
metadata: UnionMetadata,
|
|
345
|
+
structural_cache: dict[ErrorLocation, StructuralTuple] | None = None,
|
|
346
|
+
) -> type[BaseModel] | None:
|
|
347
|
+
"""Infer the model type that an error is associated with.
|
|
348
|
+
|
|
349
|
+
Uses the LAST (most specific) discriminator or model name found in the
|
|
350
|
+
error path, as nested unions may have multiple discriminators.
|
|
351
|
+
|
|
352
|
+
Args
|
|
353
|
+
----
|
|
354
|
+
error: Pydantic validation error dict
|
|
355
|
+
metadata: Pre-computed UnionMetadata from introspect_union()
|
|
356
|
+
structural_cache: Optional cache for structural tuple computation
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
The inferred model type, or None if it cannot be determined
|
|
361
|
+
"""
|
|
362
|
+
loc = error["loc"]
|
|
363
|
+
try:
|
|
364
|
+
structural = get_or_create_structural_tuple(loc, metadata, structural_cache)
|
|
365
|
+
|
|
366
|
+
# Look for discriminator value or model name in the location path
|
|
367
|
+
# Use the LAST one found (most specific) rather than the first
|
|
368
|
+
inferred_model = None
|
|
369
|
+
for element, struct_type in zip(loc, structural, strict=False):
|
|
370
|
+
if struct_type == "discriminator" and isinstance(element, str):
|
|
371
|
+
model = metadata.discriminator_to_model.get(element)
|
|
372
|
+
if model is not None:
|
|
373
|
+
inferred_model = model
|
|
374
|
+
elif struct_type == "model" and isinstance(element, str):
|
|
375
|
+
model = metadata.model_name_to_model.get(element)
|
|
376
|
+
if model is not None:
|
|
377
|
+
inferred_model = model
|
|
378
|
+
|
|
379
|
+
return inferred_model
|
|
380
|
+
except (KeyError, TypeError, IndexError):
|
|
381
|
+
# Structural analysis can fail for unexpected error path formats
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def extract_discriminator_path(
|
|
388
|
+
loc: ErrorLocation,
|
|
389
|
+
structural: StructuralTuple,
|
|
390
|
+
) -> ErrorLocation:
|
|
391
|
+
"""Extract the discriminator path from a location tuple.
|
|
392
|
+
|
|
393
|
+
The discriminator path includes model names and discriminator values - everything
|
|
394
|
+
up to (but not including) the first field. List indices and union markers are
|
|
395
|
+
excluded to prevent false ambiguity when validating lists of features or complex
|
|
396
|
+
union structures.
|
|
397
|
+
|
|
398
|
+
This path uniquely identifies which model variant was selected during validation,
|
|
399
|
+
allowing errors to be grouped by the type they're associated with.
|
|
400
|
+
|
|
401
|
+
Args
|
|
402
|
+
----
|
|
403
|
+
loc: The location tuple from a Pydantic validation error
|
|
404
|
+
structural: The parallel structural tuple
|
|
405
|
+
|
|
406
|
+
Returns
|
|
407
|
+
-------
|
|
408
|
+
The discriminator path portion of the location tuple (excluding list_index and union)
|
|
409
|
+
|
|
410
|
+
Examples
|
|
411
|
+
--------
|
|
412
|
+
>>> # Discriminated union with field error
|
|
413
|
+
>>> loc = (0, 'tagged-union[type]', 'building', 'height')
|
|
414
|
+
>>> structural = ('list_index', 'union', 'discriminator', 'field')
|
|
415
|
+
>>> extract_discriminator_path(loc, structural)
|
|
416
|
+
('building',)
|
|
417
|
+
|
|
418
|
+
>>> # Non-discriminated union
|
|
419
|
+
>>> loc = ('Segment', 'connectors', 0)
|
|
420
|
+
>>> structural = ('model', 'field', 'list_index')
|
|
421
|
+
>>> extract_discriminator_path(loc, structural)
|
|
422
|
+
('Segment',)
|
|
423
|
+
|
|
424
|
+
>>> # Root field error (no discriminator)
|
|
425
|
+
>>> loc = ('id',)
|
|
426
|
+
>>> structural = ('field',)
|
|
427
|
+
>>> extract_discriminator_path(loc, structural)
|
|
428
|
+
()
|
|
429
|
+
|
|
430
|
+
>>> # Multiple list items with same error type are grouped together
|
|
431
|
+
>>> loc1 = (0, 'tagged-union[type]', 'building', 'height')
|
|
432
|
+
>>> loc2 = (5, 'tagged-union[type]', 'building', 'height')
|
|
433
|
+
>>> structural = ('list_index', 'union', 'discriminator', 'field')
|
|
434
|
+
>>> extract_discriminator_path(loc1, structural)
|
|
435
|
+
('building',)
|
|
436
|
+
>>> extract_discriminator_path(loc2, structural)
|
|
437
|
+
('building',)
|
|
438
|
+
>>> # Both produce same discriminator path despite different list indices and union markers
|
|
439
|
+
"""
|
|
440
|
+
discriminator_path = []
|
|
441
|
+
for element, struct_type in zip(loc, structural, strict=False):
|
|
442
|
+
if struct_type == "field":
|
|
443
|
+
# Stop at the first field
|
|
444
|
+
break
|
|
445
|
+
if struct_type not in ("list_index", "union"):
|
|
446
|
+
# Include only discriminator and model elements
|
|
447
|
+
discriminator_path.append(element)
|
|
448
|
+
return tuple(discriminator_path)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Type aliases for CLI module."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypeAlias
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic_core import ErrorDetails
|
|
7
|
+
|
|
8
|
+
# Type alias for union types created from Pydantic models
|
|
9
|
+
# This represents either a single model or a discriminated union of models
|
|
10
|
+
UnionType: TypeAlias = type[BaseModel] | Any
|
|
11
|
+
|
|
12
|
+
# Pydantic validation error dictionary structure
|
|
13
|
+
# In Pydantic v2, ValidationError.errors() returns list[ErrorDetails]
|
|
14
|
+
ValidationErrorDict: TypeAlias = ErrorDetails
|
|
15
|
+
|
|
16
|
+
# Error location tuple (mix of field names and list indices)
|
|
17
|
+
ErrorLocation: TypeAlias = tuple[str | int, ...]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: overture-schema-cli
|
|
3
|
+
Version: 0.1.1.dev0
|
|
4
|
+
Summary: Command-line interface for Overture Maps schema validation and JSON Schema generation
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Dist: overture-schema-common>=0.1.1
|
|
7
|
+
Requires-Dist: overture-schema-system>=0.1.1
|
|
8
|
+
Requires-Dist: pydantic>=2.13.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
10
|
+
Requires-Dist: click>=8.1
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: yamlcore>=0.0.4
|
|
13
|
+
Maintainer: Overture Maps Schema Working Group
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Project-URL: Homepage, https://overturemaps.org
|
|
16
|
+
Project-URL: Source, https://github.com/OvertureMaps/schema
|
|
17
|
+
Project-URL: Issues, https://github.com/OvertureMaps/schema/issues
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# overture-schema-cli
|
|
21
|
+
|
|
22
|
+
Command-line interface for validating and working with Overture Maps schema.
|
|
23
|
+
|
|
24
|
+
This package provides the `overture-schema` command for validating GeoJSON and YAML files against Overture Maps schemas.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install overture-schema-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
overture-schema validate <file>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See the main [overture-schema](https://github.com/OvertureMaps/schema) documentation for more details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
overture/schema/cli/__init__.py,sha256=dk5358XqAdAnJJ1GRqZH6QA-lYyHXf5NgpUU4JEfPJg,561
|
|
2
|
+
overture/schema/cli/__main__.py,sha256=4AxLsgs7Z4GpmyoaIFR2V7dEaICsxkKkOObYGO-cAZw,114
|
|
3
|
+
overture/schema/cli/commands.py,sha256=XoVyzts1tpSsK7mw6b1ouqtM5eVP86_Dib5wZerUuwc,31372
|
|
4
|
+
overture/schema/cli/data_display.py,sha256=CdNvXnc9bRI7N_P4ijaIBE_pE0d5iPvj8muuIhqWbqg,26637
|
|
5
|
+
overture/schema/cli/docstrings.py,sha256=04FUkV0m4tlHleijl7cr2QLJBkKvh3hFT1qwVjNYEXU,686
|
|
6
|
+
overture/schema/cli/error_formatting.py,sha256=3VIpFRk0RzsIcpZpTRzrJ_oyBy1_X0j-fG0IHOQUo2s,22805
|
|
7
|
+
overture/schema/cli/output.py,sha256=gGXAv3iVlEpeR9vu-gFVJ7UsnJA6etuycL_WjPwRxoQ,841
|
|
8
|
+
overture/schema/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
overture/schema/cli/tag_options.py,sha256=TUABBbmGwr6PCJCSbMflMvsczltCE5e4i2OLD5xEXJU,2060
|
|
10
|
+
overture/schema/cli/type_analysis.py,sha256=WDmCGavb7NYHHTnXaTpm_So5SxHIq84fjqIhfze_mpU,16722
|
|
11
|
+
overture/schema/cli/types.py,sha256=auIcqPAFbRSq7K4vL4pllKMlWsSI7LsUZxc74yS1Z2o,597
|
|
12
|
+
overture_schema_cli-0.1.1.dev0.dist-info/WHEEL,sha256=Lkz__M3n3EKWmzMwHnFILNW2SHO43bOxmbqnBjwb--4,80
|
|
13
|
+
overture_schema_cli-0.1.1.dev0.dist-info/entry_points.txt,sha256=j5ikYUcZV73gU4RHwRHwzKuZBts0BjK0mrOMo5ELQN8,61
|
|
14
|
+
overture_schema_cli-0.1.1.dev0.dist-info/METADATA,sha256=rCWurIj0FzT1gufZPJj7wTYZm0WPJAuhKfl_UKJ2zEk,1151
|
|
15
|
+
overture_schema_cli-0.1.1.dev0.dist-info/RECORD,,
|