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.
- overture_schema_validation-2.0.0/PKG-INFO +42 -0
- overture_schema_validation-2.0.0/README.md +27 -0
- overture_schema_validation-2.0.0/pyproject.toml +39 -0
- overture_schema_validation-2.0.0/pyproject.toml.orig +37 -0
- overture_schema_validation-2.0.0/src/overture/schema/validation/__init__.py +169 -0
- overture_schema_validation-2.0.0/src/overture/schema/validation/py.typed +0 -0
|
@@ -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
|
|
File without changes
|