-
Notifications
You must be signed in to change notification settings - Fork 21
[REFACTOR](schema) Extract overture-schema-validation package #620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added `deepdiff` to the development dependency group. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # overture-schema-validation | ||
|
|
||
| Validate data against the union of all discovered Overture Maps models. | ||
|
|
||
| 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. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| pip install overture-schema-validation | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```python | ||
| from overture.schema.validation import validate, validate_json | ||
|
|
||
| # Validate a Python object (a dict or a model instance) | ||
| feature = validate({"type": "segment", "id": "...", "geometry": "..."}) | ||
|
|
||
| # Validate a JSON document | ||
| feature = validate_json('{"type": "segment", "id": "...", "geometry": "..."}') | ||
| ``` | ||
|
|
||
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Extracted `validate()` and `validate_json()` into the new `overture-schema-validation` package, off the shared `overture.schema` namespace root. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Changelog fragments | ||
|
|
||
| [towncrier](https://towncrier.readthedocs.io) news fragments for this package. | ||
| One file per change to this package (including patch-level fixes and internal | ||
| work): | ||
|
|
||
| ```text | ||
| changelog.d/<issue-or-pr>.<type>.md | ||
| ``` | ||
|
|
||
| Types, body format, the preview command, and when a fragment is required are | ||
| documented once in | ||
| [docs/versioning.md -> Changelog quick start](../../../docs/versioning.md#changelog-quick-start). | ||
|
|
||
| > [!NOTE] | ||
| > This README also keeps `changelog.d/` tracked in git, so no `.gitkeep` is | ||
| > needed. Leave it in place even when the directory holds no fragments. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| maintainers = [ | ||
| {name = "Overture Maps Schema Working Group"}, | ||
| ] | ||
| name = "overture-schema-validation" | ||
| version = "0.1.1" | ||
| description = "Validation helpers for the union of all discovered Overture models" | ||
| requires-python = ">=3.10" | ||
| license = "MIT" | ||
| readme = "README.md" | ||
| dependencies = [ | ||
| "overture-schema-common", | ||
| "overture-schema-system", | ||
| "pydantic>=2.12.0", | ||
| ] | ||
|
|
||
| [project.urls] | ||
| Homepage = "https://overturemaps.org" | ||
| Source = "https://github.com/OvertureMaps/schema" | ||
| Issues = "https://github.com/OvertureMaps/schema/issues" | ||
|
|
||
| [tool.uv.sources] | ||
| overture-schema-common = { workspace = true } | ||
| overture-schema-system = { workspace = true } | ||
|
|
||
| [dependency-groups] | ||
| dev = [ | ||
| "pyyaml>=6.0.2", | ||
| "yamlcore>=0.0.4", | ||
| ] | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["src/overture"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| __path__ = __import__("pkgutil").extend_path(__path__, __name__) |
1 change: 1 addition & 0 deletions
1
packages/overture-schema-validation/src/overture/schema/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| __path__ = __import__("pkgutil").extend_path(__path__, __name__) |
169 changes: 169 additions & 0 deletions
169
packages/overture-schema-validation/src/overture/schema/validation/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| from collections.abc import Generator | ||
| from functools import reduce | ||
| from operator import or_ | ||
| from types import UnionType | ||
| from typing import Annotated, Any, Literal, cast, get_args, get_origin | ||
|
|
||
| from pydantic import BaseModel, Field, Tag, TypeAdapter | ||
|
|
||
| from overture.schema.common import OvertureFeature | ||
| from overture.schema.system.discovery import discover_models | ||
| from overture.schema.system.feature import Feature | ||
|
|
||
|
|
||
| def validate(data: object) -> BaseModel: | ||
| """ | ||
| Validate a Python object, which can be a dictionary or model instance, using the union of all | ||
| discovered Overture models. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| data : object | ||
| Python object to validate against the model. | ||
|
|
||
| Returns | ||
| ------- | ||
| BaseModel | ||
| Validated model class | ||
|
|
||
| Raises | ||
| ------ | ||
| ValidationError | ||
| If `data` is not valid according to one of the discovered Overture models | ||
| """ | ||
| tap = _union_type_adapter() | ||
|
|
||
| return cast(BaseModel, tap.validate_python(data)) | ||
|
|
||
|
|
||
| def validate_json(json_data: str | bytes | bytearray) -> BaseModel: | ||
| """ | ||
| Validate JSON data using the union of all discovered Overture models. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| json_data : str | bytes | bytearray | ||
| JSON data to validate | ||
|
|
||
| Returns | ||
| ------- | ||
| BaseModel | ||
| Validated model class | ||
|
|
||
| Raises | ||
| ------ | ||
| ValidationError | ||
| If `json_data` is not valid according to one of the discovered Overture models | ||
| """ | ||
| tap = _union_type_adapter() | ||
|
|
||
| return cast(BaseModel, tap.validate_json(json_data)) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "validate", | ||
| "validate_json", | ||
| ] | ||
|
|
||
|
|
||
| def _union_type_adapter() -> TypeAdapter: | ||
| """ | ||
| Return a Pydantic type adapter that can validate the union of all models discovered using entry | ||
| points. | ||
| """ | ||
| models = discover_models() | ||
| if not models: | ||
| raise RuntimeError("no registered models found via entry points") | ||
|
|
||
| discriminated_models: tuple[type[OvertureFeature], ...] = tuple( | ||
| cast(type[OvertureFeature], m) for m in models.values() if _can_discriminate(m) | ||
| ) | ||
| discriminated_union: UnionType | None = _discriminated_union(discriminated_models) | ||
|
|
||
| non_discriminated_models: Generator[type[BaseModel], None, None] = ( | ||
| m for m in models.values() if not _can_discriminate(m) | ||
| ) | ||
| non_discriminated_union: UnionType | None = reduce( | ||
| or_, non_discriminated_models, None | ||
| ) | ||
|
|
||
| if discriminated_union and non_discriminated_union: | ||
| model_union = discriminated_union | non_discriminated_union | ||
| elif discriminated_union: | ||
| model_union = discriminated_union | ||
| elif non_discriminated_union: | ||
| model_union = non_discriminated_union | ||
| else: | ||
| raise RuntimeError("logic error: unreachable code") | ||
|
|
||
| return TypeAdapter(model_union) | ||
|
|
||
|
|
||
| def _discriminated_union( | ||
| feature_classes: tuple[type[OvertureFeature], ...], | ||
| ) -> Any: # noqa: ANN401 | ||
| """ | ||
| Create a discriminated union of the Overture features since they can be discriminated on the | ||
| `type` field. This is just a performance optimization, and the union will work even if no models | ||
| are discriminated. | ||
| """ | ||
| if not feature_classes: | ||
| return None | ||
| else: | ||
| return Annotated[ | ||
| reduce( | ||
| or_, | ||
| ( | ||
| Annotated[f, Tag(cast(str, _typeliteral(f)))] | ||
| for f in feature_classes | ||
| ), | ||
| ), | ||
| Field(discriminator=Feature.field_discriminator("type", *feature_classes)), | ||
| ] | ||
|
|
||
|
|
||
| def _can_discriminate(model_class: object) -> bool: | ||
| """ | ||
| Return true if given value can participate in a discriminated union on the `type` field because | ||
| it is an Overture feature with where the `type` field has a single literal value. | ||
| """ | ||
| return ( | ||
| isinstance(model_class, type) | ||
| and issubclass(model_class, OvertureFeature) | ||
| and _typeliteral(cast(type[OvertureFeature], model_class)) is not None | ||
| ) | ||
|
|
||
|
|
||
| def _typeliteral(feature_class: type[OvertureFeature]) -> object: | ||
| """ | ||
| Return the literal value of the Overture Feature model's `type` field, if it has one, or `None` | ||
| if it does not. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| feature_class : type[OvertureFeature] | ||
| Overture feature model class | ||
|
|
||
| Returns | ||
| ------- | ||
| object | ||
| The literal constrained value of the model class' `type` field, or `None` if the `type` | ||
| field does not have a literal value | ||
|
|
||
| Raises | ||
| ------ | ||
| TypeError | ||
| If the `type` field is constrained to `Literal[None]`, as this is absurd | ||
| """ | ||
| type_type = feature_class.model_fields["type"].annotation | ||
| while get_origin(type_type) is Annotated: | ||
| type_type = get_args(type_type)[0] | ||
| if get_origin(type_type) is not Literal: | ||
| return None | ||
| literal = get_args(type_type)[0] | ||
| if literal is None: | ||
| raise TypeError( | ||
| f"literal value of `type` field for `{OvertureFeature.__name__}` class " | ||
| f"`{feature_class.__name__}` is constrained to `None`" | ||
| ) | ||
| return literal | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Moved `validate()` and `validate_json()` into the new `overture-schema-validation` dependency; the `overture.schema` namespace root is now a bare pkgutil shim. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.