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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.6
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ overture-schema = overture.schema.cli:cli
3
+