overture-schema-validation 2.0.0__tar.gz

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,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: overture-schema-validation
3
+ Version: 2.0.0
4
+ Summary: Validation helpers for the union of all discovered Overture models
5
+ License-Expression: MIT
6
+ Requires-Dist: overture-schema-common>=2.0.0
7
+ Requires-Dist: overture-schema-system>=2.0.0
8
+ Requires-Dist: pydantic>=2.12.0
9
+ Maintainer: Overture Maps Schema Working Group
10
+ Requires-Python: >=3.10
11
+ Project-URL: Homepage, https://overturemaps.org
12
+ Project-URL: Source, https://github.com/OvertureMaps/schema
13
+ Project-URL: Issues, https://github.com/OvertureMaps/schema/issues
14
+ Description-Content-Type: text/markdown
15
+
16
+ # overture-schema-validation
17
+
18
+ Validate data against the union of all discovered Overture Maps models.
19
+
20
+ This package provides `validate` and `validate_json`, which check a Python object or JSON document against every Overture model registered on the `overture.models` entry point and return the matching validated model instance.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install overture-schema-validation
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from overture.schema.validation import validate, validate_json
32
+
33
+ # A Python object -- the flat, tabular (Parquet-style) shape
34
+ feature = validate(feature_row)
35
+
36
+ # A JSON document -- GeoJSON
37
+ feature = validate_json(geojson_text)
38
+ ```
39
+
40
+ The two entry points are not interchangeable. `validate` runs Pydantic's Python mode, which reads the flat column layout of the Parquet release -- the shape Overture publishes. `validate_json` runs JSON mode, which reads the GeoJSON representation the models support for compatibility with tools that expect features rather than rows. Handing a GeoJSON dict to `validate` reports `theme` and `version` missing and `type` set to `'Feature'`.
41
+
42
+ Both raise `pydantic.ValidationError` when the input matches no model. Which models participate is resolved at runtime by entry-point discovery, so installing additional Overture theme packages widens what these functions accept.
@@ -0,0 +1,27 @@
1
+ # overture-schema-validation
2
+
3
+ Validate data against the union of all discovered Overture Maps models.
4
+
5
+ This package provides `validate` and `validate_json`, which check a Python object or JSON document against every Overture model registered on the `overture.models` entry point and return the matching validated model instance.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install overture-schema-validation
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from overture.schema.validation import validate, validate_json
17
+
18
+ # A Python object -- the flat, tabular (Parquet-style) shape
19
+ feature = validate(feature_row)
20
+
21
+ # A JSON document -- GeoJSON
22
+ feature = validate_json(geojson_text)
23
+ ```
24
+
25
+ The two entry points are not interchangeable. `validate` runs Pydantic's Python mode, which reads the flat column layout of the Parquet release -- the shape Overture publishes. `validate_json` runs JSON mode, which reads the GeoJSON representation the models support for compatibility with tools that expect features rather than rows. Handing a GeoJSON dict to `validate` reports `theme` and `version` missing and `type` set to `'Feature'`.
26
+
27
+ Both raise `pydantic.ValidationError` when the input matches no model. Which models participate is resolved at runtime by entry-point discovery, so installing additional Overture theme packages widens what these functions accept.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.32,<0.13"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "overture-schema-validation"
7
+ version = "2.0.0"
8
+ description = "Validation helpers for the union of all discovered Overture models"
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ dependencies = [
13
+ "overture-schema-common>=2.0.0",
14
+ "overture-schema-system>=2.0.0",
15
+ "pydantic>=2.12.0",
16
+ ]
17
+
18
+ [[project.maintainers]]
19
+ name = "Overture Maps Schema Working Group"
20
+
21
+ [project.urls]
22
+ Homepage = "https://overturemaps.org"
23
+ Source = "https://github.com/OvertureMaps/schema"
24
+ Issues = "https://github.com/OvertureMaps/schema/issues"
25
+
26
+ [tool.uv.sources.overture-schema-common]
27
+ workspace = true
28
+
29
+ [tool.uv.sources.overture-schema-system]
30
+ workspace = true
31
+
32
+ [tool.uv.build-backend]
33
+ module-name = "overture.schema.validation"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pyyaml>=6.0.2",
38
+ "yamlcore>=0.0.4",
39
+ ]
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.32,<0.13"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ maintainers = [
7
+ {name = "Overture Maps Schema Working Group"},
8
+ ]
9
+ name = "overture-schema-validation"
10
+ version = "2.0.0"
11
+ description = "Validation helpers for the union of all discovered Overture models"
12
+ requires-python = ">=3.10"
13
+ license = "MIT"
14
+ readme = "README.md"
15
+ dependencies = [
16
+ "overture-schema-common>=2.0.0",
17
+ "overture-schema-system>=2.0.0",
18
+ "pydantic>=2.12.0",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://overturemaps.org"
23
+ Source = "https://github.com/OvertureMaps/schema"
24
+ Issues = "https://github.com/OvertureMaps/schema/issues"
25
+
26
+ [tool.uv.sources]
27
+ overture-schema-common = { workspace = true }
28
+ overture-schema-system = { workspace = true }
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pyyaml>=6.0.2",
33
+ "yamlcore>=0.0.4",
34
+ ]
35
+
36
+ [tool.uv.build-backend]
37
+ module-name = "overture.schema.validation"
@@ -0,0 +1,169 @@
1
+ from collections.abc import Generator
2
+ from functools import reduce
3
+ from operator import or_
4
+ from types import UnionType
5
+ from typing import Annotated, Any, Literal, cast, get_args, get_origin
6
+
7
+ from pydantic import BaseModel, Field, Tag, TypeAdapter
8
+
9
+ from overture.schema.common import OvertureFeature
10
+ from overture.schema.system.discovery import discover_models
11
+ from overture.schema.system.feature import Feature
12
+
13
+
14
+ def validate(data: object) -> BaseModel:
15
+ """
16
+ Validate a Python object, which can be a dictionary or model instance, using the union of all
17
+ discovered Overture models.
18
+
19
+ Parameters
20
+ ----------
21
+ data : object
22
+ Python object to validate against the model.
23
+
24
+ Returns
25
+ -------
26
+ BaseModel
27
+ Validated model class
28
+
29
+ Raises
30
+ ------
31
+ ValidationError
32
+ If `data` is not valid according to one of the discovered Overture models
33
+ """
34
+ tap = _union_type_adapter()
35
+
36
+ return cast(BaseModel, tap.validate_python(data))
37
+
38
+
39
+ def validate_json(json_data: str | bytes | bytearray) -> BaseModel:
40
+ """
41
+ Validate JSON data using the union of all discovered Overture models.
42
+
43
+ Parameters
44
+ ----------
45
+ json_data : str | bytes | bytearray
46
+ JSON data to validate
47
+
48
+ Returns
49
+ -------
50
+ BaseModel
51
+ Validated model class
52
+
53
+ Raises
54
+ ------
55
+ ValidationError
56
+ If `json_data` is not valid according to one of the discovered Overture models
57
+ """
58
+ tap = _union_type_adapter()
59
+
60
+ return cast(BaseModel, tap.validate_json(json_data))
61
+
62
+
63
+ __all__ = [
64
+ "validate",
65
+ "validate_json",
66
+ ]
67
+
68
+
69
+ def _union_type_adapter() -> TypeAdapter:
70
+ """
71
+ Return a Pydantic type adapter that can validate the union of all models discovered using entry
72
+ points.
73
+ """
74
+ models = discover_models()
75
+ if not models:
76
+ raise RuntimeError("no registered models found via entry points")
77
+
78
+ discriminated_models: tuple[type[OvertureFeature], ...] = tuple(
79
+ cast(type[OvertureFeature], m) for m in models.values() if _can_discriminate(m)
80
+ )
81
+ discriminated_union: UnionType | None = _discriminated_union(discriminated_models)
82
+
83
+ non_discriminated_models: Generator[type[BaseModel], None, None] = (
84
+ m for m in models.values() if not _can_discriminate(m)
85
+ )
86
+ non_discriminated_union: UnionType | None = reduce(
87
+ or_, non_discriminated_models, None
88
+ )
89
+
90
+ if discriminated_union and non_discriminated_union:
91
+ model_union = discriminated_union | non_discriminated_union
92
+ elif discriminated_union:
93
+ model_union = discriminated_union
94
+ elif non_discriminated_union:
95
+ model_union = non_discriminated_union
96
+ else:
97
+ raise RuntimeError("logic error: unreachable code")
98
+
99
+ return TypeAdapter(model_union)
100
+
101
+
102
+ def _discriminated_union(
103
+ feature_classes: tuple[type[OvertureFeature], ...],
104
+ ) -> Any: # noqa: ANN401
105
+ """
106
+ Create a discriminated union of the Overture features since they can be discriminated on the
107
+ `type` field. This is just a performance optimization, and the union will work even if no models
108
+ are discriminated.
109
+ """
110
+ if not feature_classes:
111
+ return None
112
+ else:
113
+ return Annotated[
114
+ reduce(
115
+ or_,
116
+ (
117
+ Annotated[f, Tag(cast(str, _typeliteral(f)))]
118
+ for f in feature_classes
119
+ ),
120
+ ),
121
+ Field(discriminator=Feature.field_discriminator("type", *feature_classes)),
122
+ ]
123
+
124
+
125
+ def _can_discriminate(model_class: object) -> bool:
126
+ """
127
+ Return true if given value can participate in a discriminated union on the `type` field because
128
+ it is an Overture feature with where the `type` field has a single literal value.
129
+ """
130
+ return (
131
+ isinstance(model_class, type)
132
+ and issubclass(model_class, OvertureFeature)
133
+ and _typeliteral(cast(type[OvertureFeature], model_class)) is not None
134
+ )
135
+
136
+
137
+ def _typeliteral(feature_class: type[OvertureFeature]) -> object:
138
+ """
139
+ Return the literal value of the Overture Feature model's `type` field, if it has one, or `None`
140
+ if it does not.
141
+
142
+ Parameters
143
+ ----------
144
+ feature_class : type[OvertureFeature]
145
+ Overture feature model class
146
+
147
+ Returns
148
+ -------
149
+ object
150
+ The literal constrained value of the model class' `type` field, or `None` if the `type`
151
+ field does not have a literal value
152
+
153
+ Raises
154
+ ------
155
+ TypeError
156
+ If the `type` field is constrained to `Literal[None]`, as this is absurd
157
+ """
158
+ type_type = feature_class.model_fields["type"].annotation
159
+ while get_origin(type_type) is Annotated:
160
+ type_type = get_args(type_type)[0]
161
+ if get_origin(type_type) is not Literal:
162
+ return None
163
+ literal = get_args(type_type)[0]
164
+ if literal is None:
165
+ raise TypeError(
166
+ f"literal value of `type` field for `{OvertureFeature.__name__}` class "
167
+ f"`{feature_class.__name__}` is constrained to `None`"
168
+ )
169
+ return literal