diff --git a/Makefile b/Makefile index c882bf059..ba3f2fb0f 100644 --- a/Makefile +++ b/Makefile @@ -11,17 +11,17 @@ check: test doctest @uv run ruff format --check packages/ test-all: uv-sync - @uv run pytest packages/ + @uv run pytest -W error packages/ test: uv-sync - @uv run pytest packages/ -x + @uv run pytest -W error packages/ -x coverage: uv-sync @uv run pytest packages/ --cov overture.schema --cov-report=term --cov-report=html && open htmlcov/index.html docformat: @find packages/*/src -name "*.py" -type f -not -name "__*" \ - | xargs uv run pydocstyle --convention=numpy --add-ignore=D105 + | xargs uv run pydocstyle --convention=numpy --add-ignore=D102,D105,D200,D205,D400 doctest: uv-sync @# $$ escapes $ for make - sed needs literal $ for end-of-line anchor diff --git a/packages/overture-schema-addresses-theme/src/overture/schema/addresses/address.py b/packages/overture-schema-addresses-theme/src/overture/schema/addresses/address.py index 8efeaaa23..6469cb130 100644 --- a/packages/overture-schema-addresses-theme/src/overture/schema/addresses/address.py +++ b/packages/overture-schema-addresses-theme/src/overture/schema/addresses/address.py @@ -1,4 +1,6 @@ -"""Address feature model.""" +""" +The `Address` feature type model and supporting types. +""" import textwrap from typing import Annotated, Literal @@ -64,7 +66,8 @@ class Address(OvertureFeature[Literal["addresses"], Literal["address"]]): model_config = ConfigDict(title="address") - # Core + # Overture Feature + geometry: Annotated[ Geometry, GeometryTypeConstraint(GeometryType.POINT), diff --git a/packages/overture-schema-addresses-theme/tests/address_baseline_schema.json b/packages/overture-schema-addresses-theme/tests/address_baseline_schema.json index 758bbc485..46d07f176 100644 --- a/packages/overture-schema-addresses-theme/tests/address_baseline_schema.json +++ b/packages/overture-schema-addresses-theme/tests/address_baseline_schema.json @@ -15,12 +15,12 @@ "title": "AddressLevel", "type": "object" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -32,34 +32,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -69,7 +70,7 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, @@ -178,9 +179,9 @@ "type": "string" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-addresses-theme/tests/test_address_json_schema_baseline.py b/packages/overture-schema-addresses-theme/tests/test_address_json_schema_baseline.py index 9514f1995..dc97faaeb 100644 --- a/packages/overture-schema-addresses-theme/tests/test_address_json_schema_baseline.py +++ b/packages/overture-schema-addresses-theme/tests/test_address_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.addresses import Address -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_address_json_schema_baseline() -> None: diff --git a/packages/overture-schema-annex/src/overture/schema/annex/models.py b/packages/overture-schema-annex/src/overture/schema/annex/models.py index b27542580..1e3cb041d 100644 --- a/packages/overture-schema-annex/src/overture/schema/annex/models.py +++ b/packages/overture-schema-annex/src/overture/schema/annex/models.py @@ -68,6 +68,7 @@ class Dataset(BaseModel): str, Field(description="Any attribution required by this source."), ] + # FIXME: This should be a `BBox` primitive, not a `list[float]`. coverage_bbox: Annotated[ list[float], Field( diff --git a/packages/overture-schema-annex/tests/test_sources_json_schema_baseline.py b/packages/overture-schema-annex/tests/test_sources_json_schema_baseline.py index 3021b3ada..b34383be5 100644 --- a/packages/overture-schema-annex/tests/test_sources_json_schema_baseline.py +++ b/packages/overture-schema-annex/tests/test_sources_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.annex import Sources -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_sources_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/pyproject.toml b/packages/overture-schema-base-theme/pyproject.toml index 8298454e9..b3b3434fa 100644 --- a/packages/overture-schema-base-theme/pyproject.toml +++ b/packages/overture-schema-base-theme/pyproject.toml @@ -25,9 +25,9 @@ path = "src/overture/schema/base/__about__.py" packages = ["src/overture"] [project.entry-points."overture.models"] -"base.bathymetry" = "overture.schema.base.bathymetry.models:Bathymetry" -"base.infrastructure" = "overture.schema.base.infrastructure.models:Infrastructure" -"base.land" = "overture.schema.base.land.models:Land" -"base.land_cover" = "overture.schema.base.land_cover.models:LandCover" -"base.land_use" = "overture.schema.base.land_use.models:LandUse" -"base.water" = "overture.schema.base.water.models:Water" +"base.bathymetry" = "overture.schema.base:Bathymetry" +"base.infrastructure" = "overture.schema.base:Infrastructure" +"base.land" = "overture.schema.base:Land" +"base.land_cover" = "overture.schema.base:LandCover" +"base.land_use" = "overture.schema.base:LandUse" +"base.water" = "overture.schema.base:Water" diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/__init__.py index 7061c9bfc..186016dbf 100644 --- a/packages/overture-schema-base-theme/src/overture/schema/base/__init__.py +++ b/packages/overture-schema-base-theme/src/overture/schema/base/__init__.py @@ -6,11 +6,41 @@ __path__ = __import__("pkgutil").extend_path(__path__, __name__) + +from ._common import ( + Depth, + Elevation, + Height, + SourcedFromOpenStreetMap, + SourceTags, + SurfaceMaterial, +) from .bathymetry import Bathymetry -from .infrastructure import Infrastructure -from .land import Land -from .land_cover import LandCover -from .land_use import LandUse -from .water import Water +from .infrastructure import Infrastructure, InfrastructureClass, InfrastructureSubtype +from .land import Land, LandClass, LandSubtype +from .land_cover import LandCover, LandCoverSubtype +from .land_use import LandUse, LandUseClass, LandUseSubtype +from .water import Water, WaterClass, WaterSubtype -__all__ = ["Bathymetry", "Infrastructure", "Land", "LandCover", "LandUse", "Water"] +# Only the theme's feature type classes should be available for `import *`. +__all__ = [ + "Bathymetry", + "Depth", + "Elevation", + "Height", + "Infrastructure", + "InfrastructureClass", + "InfrastructureSubType", + "Land", + "LandClass", + "LandCover", + "LandCoverSubtype", + "LandSubtype", + "LandUse", + "SourcedFromOpenStreetMap", + "SourceTags", + "SurfaceMaterial", + "Water", + "WaterClass", + "WaterSubtype", +] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/_common.py b/packages/overture-schema-base-theme/src/overture/schema/base/_common.py new file mode 100644 index 000000000..e0b777947 --- /dev/null +++ b/packages/overture-schema-base-theme/src/overture/schema/base/_common.py @@ -0,0 +1,89 @@ +import textwrap +from enum import Enum +from typing import Annotated, Any, NewType + +from pydantic import BaseModel, Field + +from overture.schema.system.primitive import float64, int32 +from overture.schema.system.string import WikidataId + +Depth = NewType( + "Depth", + Annotated[ + int32, + Field( + ge=0, + description="Depth below surface level of the feature in meters.", + ), + ], +) + +Elevation = NewType( + "Elevation", + Annotated[ + int32, + Field( + le=9000, + description="Elevation above sea level of the feature in meters.", + ), + ], +) + +Height = NewType( + "Height", + Annotated[float64, Field(gt=0, description="Height of the feature in meters.")], +) + + +SourceTags = NewType( + "SourceTags", + Annotated[ + dict[str, Any], + Field( + description=textwrap.dedent(""" + Key/value pairs imported directly from the source data without change. + + This field provides access to raw OSM entity tags for features sourced from + OpenStreetMap. + """).strip() + ), + ], +) + + +class SourcedFromOpenStreetMap(BaseModel): + """ + Model derived from an OpenStreetMap entity and containing the entity's OSM tags and wikidata ID. + """ + + source_tags: SourceTags | None = None + wikidata: WikidataId | None = None + + +class SurfaceMaterial(str, Enum): + """Material that makes up the surface of `Infrastructure` and `Land` features.""" + + ASPHALT = "asphalt" + COBBLESTONE = "cobblestone" + COMPACTED = "compacted" + CONCRETE = "concrete" + CONCRETE_PLATES = "concrete_plates" + DIRT = "dirt" + EARTH = "earth" + FINE_GRAVEL = "fine_gravel" + GRASS = "grass" + GRAVEL = "gravel" + GROUND = "ground" + PAVED = "paved" + PAVING_STONES = "paving_stones" + PEBBLESTONE = "pebblestone" + RECREATION_GRASS = "recreation_grass" + RECREATION_PAVED = "recreation_paved" + RECREATION_SAND = "recreation_sand" + RUBBER = "rubber" + SAND = "sand" + SETT = "sett" + TARTAN = "tartan" + UNPAVED = "unpaved" + WOOD = "wood" + WOODCHIPS = "woodchips" diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry.py similarity index 55% rename from packages/overture-schema-base-theme/src/overture/schema/base/bathymetry/models.py rename to packages/overture-schema-base-theme/src/overture/schema/base/bathymetry.py index a9e6f2599..80b5b260f 100644 --- a/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry/models.py +++ b/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry.py @@ -1,35 +1,42 @@ -"""Bathymetry feature models for Overture Maps base theme.""" +""" +The `Bathymetry` feature type model and supporting types. +""" from typing import Annotated, Literal from pydantic import ConfigDict, Field -from overture.schema.base.types import Depth from overture.schema.core import ( OvertureFeature, ) -from overture.schema.core.models import CartographicallyHinted +from overture.schema.core.cartography import CartographicallyHinted from overture.schema.system.primitive import ( Geometry, GeometryType, GeometryTypeConstraint, ) +from ._common import Depth + class Bathymetry( OvertureFeature[Literal["base"], Literal["bathymetry"]], CartographicallyHinted ): - """Topographic representation of an underwater area, such as a part of the ocean - floor.""" + """ + Bathymetry features provide topographic representations of underwater areas, such as parts of + lake beds or ocean floors. + """ model_config = ConfigDict(title="bathymetry") - # Core + # Overture Feature geometry: Annotated[ Geometry, GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), - Field(description="Geometry (Polygon or MultiPolygon)"), + Field( + description="Shape of the underwater area, which may be a polygon or multi-polygon." + ), ] # Required diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry/__init__.py deleted file mode 100644 index 893694daf..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/bathymetry/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .models import Bathymetry - -__all__ = ["Bathymetry"] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/enums.py b/packages/overture-schema-base-theme/src/overture/schema/base/enums.py deleted file mode 100644 index 4231f18b1..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/enums.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Common structures and enums shared across base theme types.""" - -from enum import Enum - - -class SurfaceMaterial(str, Enum): - """Surface material enum used by infrastructure and land features.""" - - ASPHALT = "asphalt" - COBBLESTONE = "cobblestone" - COMPACTED = "compacted" - CONCRETE = "concrete" - CONCRETE_PLATES = "concrete_plates" - DIRT = "dirt" - EARTH = "earth" - FINE_GRAVEL = "fine_gravel" - GRASS = "grass" - GRAVEL = "gravel" - GROUND = "ground" - PAVED = "paved" - PAVING_STONES = "paving_stones" - PEBBLESTONE = "pebblestone" - RECREATION_GRASS = "recreation_grass" - RECREATION_PAVED = "recreation_paved" - RECREATION_SAND = "recreation_sand" - RUBBER = "rubber" - SAND = "sand" - SETT = "sett" - TARTAN = "tartan" - UNPAVED = "unpaved" - WOOD = "wood" - WOODCHIPS = "woodchips" - - -__all__ = [ - "SurfaceMaterial", -] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/enums.py b/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure.py similarity index 73% rename from packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/enums.py rename to packages/overture-schema-base-theme/src/overture/schema/base/infrastructure.py index f5771b3c7..00fadf403 100644 --- a/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/enums.py +++ b/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure.py @@ -1,8 +1,34 @@ +""" +The `Infrastructure` feature type model and supporting types. +""" + +import textwrap from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from overture.schema.base._common import Height, SourcedFromOpenStreetMap +from overture.schema.core import ( + OvertureFeature, +) +from overture.schema.core.models import Stacked +from overture.schema.core.names import Named +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) + +from ._common import SurfaceMaterial class InfrastructureSubtype(str, Enum): - """Further description of the type of infrastructure.""" + """ + Broadest classification of the type of infrastructure. + + This broad classification can be refined by `InfrastructureClass`. + """ AERIALWAY = "aerialway" AIRPORT = "airport" @@ -25,7 +51,11 @@ class InfrastructureSubtype(str, Enum): class InfrastructureClass(str, Enum): - """Further classification of the infrastructure type.""" + """ + Further classification of the type of infrastructure. + + The infrastructure class adds detail to the broad classification of `InfrastructureSubtype`. + """ AERIALWAY_STATION = "aerialway_station" AIRPORT = "airport" @@ -191,3 +221,45 @@ class InfrastructureClass(str, Enum): WATER_TOWER = "water_tower" WEIR = "weir" ZIP_LINE = "zip_line" + + +class Infrastructure( + OvertureFeature[Literal["base"], Literal["infrastructure"]], + Named, + Stacked, + SourcedFromOpenStreetMap, +): + """ + Infrastructure features provide basic information about real-world infrastructure entitites + such as bridges, airports, runways, aerialways, communication towers, and power lines. + """ + + model_config = ConfigDict(title="infrastructure") + + # Overture Feature + + geometry: Annotated[ + Geometry, + GeometryTypeConstraint( + GeometryType.POINT, + GeometryType.LINE_STRING, + GeometryType.POLYGON, + GeometryType.MULTI_POLYGON, + ), + Field( + description=textwrap.dedent(""" + Geometry of the infrastructure feature, which may be a point, line string, polygon, or + multi-polygon. + """).strip() + ), + ] + + # Required + + class_: Annotated[InfrastructureClass, Field(alias="class")] + subtype: InfrastructureSubtype + + # Optional + + height: Height | None = None + surface: SurfaceMaterial | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/__init__.py deleted file mode 100644 index 337aabefb..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .enums import InfrastructureClass, InfrastructureSubtype -from .models import ( - Infrastructure, -) - -__all__ = [ - "Infrastructure", - "InfrastructureSubtype", - "InfrastructureClass", -] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/models.py deleted file mode 100644 index 5e858200a..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/infrastructure/models.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Infrastructure feature models for Overture Maps base theme.""" - -from typing import Annotated, Literal - -from pydantic import ConfigDict, Field - -from overture.schema.base.infrastructure.enums import ( - InfrastructureClass, - InfrastructureSubtype, -) -from overture.schema.base.models import SourcedFromOpenStreetMap -from overture.schema.base.types import Height -from overture.schema.core import ( - OvertureFeature, -) -from overture.schema.core.models import Stacked -from overture.schema.core.names import Named -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) - -from ..enums import SurfaceMaterial - - -class Infrastructure( - OvertureFeature[Literal["base"], Literal["infrastructure"]], - Named, - Stacked, - SourcedFromOpenStreetMap, -): - """Various features from OpenStreetMap such as bridges, airport runways, aerialways, - or communication towers and lines.""" - - model_config = ConfigDict(title="Infrastructure Schema") - - # Core - - geometry: Annotated[ - Geometry, - GeometryTypeConstraint( - GeometryType.POINT, - GeometryType.LINE_STRING, - GeometryType.POLYGON, - GeometryType.MULTI_POLYGON, - ), - Field(description="Geometry (Point, LineString, Polygon, or MultiPolygon)"), - ] - - # Required - - class_: Annotated[InfrastructureClass, Field(alias="class")] - subtype: InfrastructureSubtype - - # Optional - - height: Height | None = None - surface: SurfaceMaterial | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land.py b/packages/overture-schema-base-theme/src/overture/schema/base/land.py new file mode 100644 index 000000000..df94125c1 --- /dev/null +++ b/packages/overture-schema-base-theme/src/overture/schema/base/land.py @@ -0,0 +1,150 @@ +""" +The `Land` feature type model and supporting types. +""" + +import textwrap +from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from overture.schema.base._common import Elevation, SourcedFromOpenStreetMap +from overture.schema.core import ( + OvertureFeature, +) +from overture.schema.core.models import Stacked +from overture.schema.core.names import Named +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) + +from ._common import SurfaceMaterial + + +class LandSubtype(str, Enum): + """ + Broadest classification of the land. + + This broad classification can be refined by `LandClass`. + """ + + CRATER = "crater" + DESERT = "desert" + FOREST = "forest" + GLACIER = "glacier" + GRASS = "grass" + LAND = "land" + PHYSICAL = "physical" + REEF = "reef" + ROCK = "rock" + SAND = "sand" + SHRUB = "shrub" + TREE = "tree" + WETLAND = "wetland" + + +class LandClass(str, Enum): + """ + Further classification of the land. + + The land class adds detail to the broad classification of `LandSubtype`. + """ + + ARCHIPELAGO = "archipelago" + BARE_ROCK = "bare_rock" + BEACH = "beach" + CAVE_ENTRANCE = "cave_entrance" + CLIFF = "cliff" + DESERT = "desert" + DUNE = "dune" + FELL = "fell" + FOREST = "forest" + GLACIER = "glacier" + GRASS = "grass" + GRASSLAND = "grassland" + HEATH = "heath" + HILL = "hill" + ISLAND = "island" + ISLET = "islet" + LAND = "land" + MEADOW = "meadow" + METEOR_CRATER = "meteor_crater" + MOUNTAIN_RANGE = "mountain_range" + PEAK = "peak" + PENINSULA = "peninsula" + PLATEAU = "plateau" + REEF = "reef" + RIDGE = "ridge" + ROCK = "rock" + SADDLE = "saddle" + SAND = "sand" + SCREE = "scree" + SCRUB = "scrub" + SHINGLE = "shingle" + SHRUB = "shrub" + SHRUBBERY = "shrubbery" + STONE = "stone" + TREE = "tree" + TREE_ROW = "tree_row" + TUNDRA = "tundra" + VALLEY = "valley" + VOLCANIC_CALDERA_RIM = "volcanic_caldera_rim" + VOLCANO = "volcano" + WETLAND = "wetland" + WOOD = "wood" + + +class Land( + OvertureFeature[Literal["base"], Literal["land"]], + Named, + Stacked, + SourcedFromOpenStreetMap, +): + """ + Land features are representations of physical land surfaces. + + In Overture data releases, land features are sourced from OpenStreetMap. TODO. Finish this when + I get more info from Jennings. + + + + Physical representations of land surfaces. + + Global land derived from the inverse of OSM Coastlines. Translates `natural` tags from OpenStreetMap. + + TODO: Update this description when the relationship to `land_cover` is better understood. + """ + + model_config = ConfigDict(title="land") + + # Overture Feature + + geometry: Annotated[ + Geometry, + GeometryTypeConstraint( + GeometryType.POINT, + GeometryType.LINE_STRING, + GeometryType.POLYGON, + GeometryType.MULTI_POLYGON, + ), + Field( + description=textwrap.dedent(""" + Geometry of the land feature, which may be a point, line string, polygon, or + multi-polygon. + """).strip() + ), + ] + + # Required + + class_: Annotated[LandClass, Field(default=LandClass.LAND, alias="class")] = ( + LandClass.LAND + ) + subtype: Annotated[LandSubtype, Field(default=LandSubtype.LAND)] = LandSubtype.LAND + + # Optional + + elevation: Elevation | None = None + surface: SurfaceMaterial | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/land/__init__.py deleted file mode 100644 index 279ce8395..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .enums import LandClass, LandSubtype -from .models import ( - Land, -) - -__all__ = [ - "Land", - "LandSubtype", - "LandClass", -] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land/enums.py b/packages/overture-schema-base-theme/src/overture/schema/base/land/enums.py deleted file mode 100644 index 6cf004581..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land/enums.py +++ /dev/null @@ -1,67 +0,0 @@ -from enum import Enum - - -class LandSubtype(str, Enum): - """Further description of the type of land cover, such as forest, glacier, grass, or - a physical feature, such as a mountain peak.""" - - CRATER = "crater" - DESERT = "desert" - FOREST = "forest" - GLACIER = "glacier" - GRASS = "grass" - LAND = "land" - PHYSICAL = "physical" - REEF = "reef" - ROCK = "rock" - SAND = "sand" - SHRUB = "shrub" - TREE = "tree" - WETLAND = "wetland" - - -class LandClass(str, Enum): - """Further classification of type of landcover.""" - - ARCHIPELAGO = "archipelago" - BARE_ROCK = "bare_rock" - BEACH = "beach" - CAVE_ENTRANCE = "cave_entrance" - CLIFF = "cliff" - DESERT = "desert" - DUNE = "dune" - FELL = "fell" - FOREST = "forest" - GLACIER = "glacier" - GRASS = "grass" - GRASSLAND = "grassland" - HEATH = "heath" - HILL = "hill" - ISLAND = "island" - ISLET = "islet" - LAND = "land" - MEADOW = "meadow" - METEOR_CRATER = "meteor_crater" - MOUNTAIN_RANGE = "mountain_range" - PEAK = "peak" - PENINSULA = "peninsula" - PLATEAU = "plateau" - REEF = "reef" - RIDGE = "ridge" - ROCK = "rock" - SADDLE = "saddle" - SAND = "sand" - SCREE = "scree" - SCRUB = "scrub" - SHINGLE = "shingle" - SHRUB = "shrub" - SHRUBBERY = "shrubbery" - STONE = "stone" - TREE = "tree" - TREE_ROW = "tree_row" - TUNDRA = "tundra" - VALLEY = "valley" - VOLCANIC_CALDERA_RIM = "volcanic_caldera_rim" - VOLCANO = "volcano" - WETLAND = "wetland" - WOOD = "wood" diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/land/models.py deleted file mode 100644 index c67a2c72c..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land/models.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Land feature models for Overture Maps base theme.""" - -from typing import Annotated, Literal - -from pydantic import ConfigDict, Field - -from overture.schema.base.land.enums import LandClass, LandSubtype -from overture.schema.base.models import SourcedFromOpenStreetMap -from overture.schema.base.types import Elevation -from overture.schema.core import ( - OvertureFeature, -) -from overture.schema.core.models import Stacked -from overture.schema.core.names import Named -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) - -from ..enums import SurfaceMaterial - - -class Land( - OvertureFeature[Literal["base"], Literal["land"]], - Named, - Stacked, - SourcedFromOpenStreetMap, -): - """Physical representations of land surfaces. - - Global land derived from the inverse of OSM Coastlines. Translates `natural` tags from OpenStreetMap. - """ - - model_config = ConfigDict(title="land") - - # Core - - geometry: Annotated[ - Geometry, - GeometryTypeConstraint( - GeometryType.POINT, - GeometryType.LINE_STRING, - GeometryType.POLYGON, - GeometryType.MULTI_POLYGON, - ), - Field(description="Geometry (Point, LineString, Polygon, or MultiPolygon)"), - ] - - # Required - - class_: Annotated[LandClass, Field(default=LandClass.LAND, alias="class")] = ( - LandClass.LAND - ) - subtype: Annotated[LandSubtype, Field(default=LandSubtype.LAND)] = LandSubtype.LAND - - # Optional - - elevation: Elevation | None = None - surface: SurfaceMaterial | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_cover.py new file mode 100644 index 000000000..385501877 --- /dev/null +++ b/packages/overture-schema-base-theme/src/overture/schema/base/land_cover.py @@ -0,0 +1,65 @@ +""" +The `LandCover` feature type model and supporting types. +""" + +from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from overture.schema.core import ( + OvertureFeature, +) +from overture.schema.core.cartography import CartographicallyHinted +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) + + +class LandCoverSubtype(str, Enum): + """Primary or dominant material covering the land.""" + + BARREN = "barren" + CROP = "crop" + FOREST = "forest" + GRASS = "grass" + MANGROVE = "mangrove" + MOSS = "moss" + SHRUB = "shrub" + SNOW = "snow" + URBAN = "urban" + WETLAND = "wetland" + + +class LandCover( + OvertureFeature[Literal["base"], Literal["land_cover"]], CartographicallyHinted +): + """ + Land cover features indicate the primary natural or artificial surface material covering a land + area on the earth, including vegetation types like forests and crops, built environments like + cities, and natural surfaces like wetlands or barren ground. + + Land cover features relate to `LandUse` features in the following way: land cover is the + physical thing covering the land, while land use is the human use to which the land is being + put. + + TODO: Explain relationship to `Land` features. + """ + + model_config = ConfigDict(title="land_cover") + + # Overture Feature + + geometry: Annotated[ + Geometry, + GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), + Field( + description="Shape of the covered land area, which may be a polygon or multi-polygon." + ), + ] + + # Required + + subtype: LandCoverSubtype diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/__init__.py deleted file mode 100644 index 9f2a5dad5..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .enums import LandCoverSubtype -from .models import ( - LandCover, -) - -__all__ = [ - "LandCover", - "enums", -] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/enums.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/enums.py deleted file mode 100644 index d775a8249..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/enums.py +++ /dev/null @@ -1,16 +0,0 @@ -from enum import Enum - - -class LandCoverSubtype(str, Enum): - """Type of surface represented.""" - - BARREN = "barren" - CROP = "crop" - FOREST = "forest" - GRASS = "grass" - MANGROVE = "mangrove" - MOSS = "moss" - SHRUB = "shrub" - SNOW = "snow" - URBAN = "urban" - WETLAND = "wetland" diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/models.py deleted file mode 100644 index 2fd55d368..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land_cover/models.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Land cover feature models for Overture Maps base theme.""" - -from typing import Annotated, Literal - -from pydantic import ConfigDict, Field - -from overture.schema.base.land_cover.enums import LandCoverSubtype -from overture.schema.core import ( - OvertureFeature, -) -from overture.schema.core.models import CartographicallyHinted -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) - - -class LandCover( - OvertureFeature[Literal["base"], Literal["land_cover"]], CartographicallyHinted -): - """Representation of the Earth's natural surfaces.""" - - model_config = ConfigDict(title="land_cover") - - # Core - - geometry: Annotated[ - Geometry, - GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), - Field(description="Geometry (Polygon or MultiPolygon)"), - ] - - # Required - - subtype: LandCoverSubtype diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_use/enums.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_use.py similarity index 65% rename from packages/overture-schema-base-theme/src/overture/schema/base/land_use/enums.py rename to packages/overture-schema-base-theme/src/overture/schema/base/land_use.py index 389a6d39f..362af100d 100644 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land_use/enums.py +++ b/packages/overture-schema-base-theme/src/overture/schema/base/land_use.py @@ -1,8 +1,34 @@ +""" +The `LandUse` feature type model and supporting types. +""" + +import textwrap from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from overture.schema.base._common import Elevation, SourcedFromOpenStreetMap +from overture.schema.core import ( + OvertureFeature, +) +from overture.schema.core.models import Stacked +from overture.schema.core.names import Named +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) + +from ._common import SurfaceMaterial class LandUseSubtype(str, Enum): - """Broad type of land.""" + """ + Broadest classification of the land use. + + This broad classification can be refined by `LandUseClass`. + """ AGRICULTURE = "agriculture" AQUACULTURE = "aquaculture" @@ -31,7 +57,11 @@ class LandUseSubtype(str, Enum): class LandUseClass(str, Enum): - """Further classification of the land use.""" + """ + Further classification of the land use. + + The land use class adds detail to the broad classification of `LandUseSubtype`. + """ ABORIGINAL_LAND = "aboriginal_land" AIRFIELD = "airfield" @@ -142,3 +172,52 @@ class LandUseClass(str, Enum): WINTER_SPORTS = "winter_sports" WORKS = "works" ZOO = "zoo" + + +class LandUse( + OvertureFeature[Literal["base"], Literal["land_use"]], + Named, + Stacked, + SourcedFromOpenStreetMap, +): + """ + Land use features specify the predominant human use of an area of land, for example commercial + activity, recreation, farming, housing, education, or military use. + + Land use features relate to `LandCover` features in the following way: land use is the human + human activity being done with the land, while land cover is the physical thing that covers it. + + TODO: Explain relationship to `Land` features. + """ + + model_config = ConfigDict(title="land_use") + + # Core + + geometry: Annotated[ + Geometry, + GeometryTypeConstraint( + GeometryType.POINT, + GeometryType.LINE_STRING, + GeometryType.POLYGON, + GeometryType.MULTI_POLYGON, + ), + Field( + description=textwrap.dedent( + """ + Geometry of the land use area, which may be a point, line string, polygon, or + multi-polygon. + """ + ).strip(), + ), + ] + + # Required + + class_: Annotated[LandUseClass, Field(alias="class")] + subtype: LandUseSubtype + + # Optional + + elevation: Elevation | None = None + surface: SurfaceMaterial | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_use/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_use/__init__.py deleted file mode 100644 index 7925f2fc2..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land_use/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .models import ( - LandUse, -) - -__all__ = [ - "LandUse", -] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/land_use/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/land_use/models.py deleted file mode 100644 index 0d3f5072d..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/land_use/models.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Land use feature models for Overture Maps base theme.""" - -from typing import Annotated, Literal - -from pydantic import ConfigDict, Field - -from overture.schema.base.land_use.enums import LandUseClass, LandUseSubtype -from overture.schema.base.models import SourcedFromOpenStreetMap -from overture.schema.base.types import Elevation -from overture.schema.core import ( - OvertureFeature, -) -from overture.schema.core.models import Stacked -from overture.schema.core.names import Named -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) - -from ..enums import SurfaceMaterial - - -class LandUse( - OvertureFeature[Literal["base"], Literal["land_use"]], - Named, - Stacked, - SourcedFromOpenStreetMap, -): - """Land use features from OpenStreetMap.""" - - model_config = ConfigDict(title="land_use") - - # Core - - geometry: Annotated[ - Geometry, - GeometryTypeConstraint( - GeometryType.POINT, - GeometryType.LINE_STRING, - GeometryType.POLYGON, - GeometryType.MULTI_POLYGON, - ), - Field( - description="Classifications of the human use of a section of land. Translates `landuse` from OpenStreetMap tag from OpenStreetMap.", - ), - ] - - # Required - - class_: Annotated[LandUseClass, Field(alias="class")] - subtype: LandUseSubtype - - # Optional - - elevation: Elevation | None = None - surface: SurfaceMaterial | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/models.py deleted file mode 100644 index f459d9760..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/models.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic import BaseModel - -from overture.schema.base.types import SourceTags -from overture.schema.system.string import WikidataId - - -class SourcedFromOpenStreetMap(BaseModel): - source_tags: SourceTags | None = None - wikidata: WikidataId | None = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/types.py b/packages/overture-schema-base-theme/src/overture/schema/base/types.py deleted file mode 100644 index 05553387f..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/types.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Annotated, Any, NewType - -from pydantic import Field - -from overture.schema.system.primitive import float64, int32 - -Elevation = NewType( - "Elevation", - Annotated[ - int32, - Field( - le=9000, - description="Elevation above sea level (in meters) of the feature.", - ), - ], -) - -Depth = NewType( - "Depth", - Annotated[ - int32, - Field( - ge=0, - description="Depth below surface level (in meters) of the feature.", - ), - ], -) - -Height = NewType( - "Height", - Annotated[float64, Field(gt=0, description="Height of the feature in meters.")], -) - -SourceTags = NewType( - "SourceTags", - Annotated[ - dict[str, Any], - Field( - description="Any attributes/tags from the original source data that should be passed through." - ), - ], -) diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/water.py b/packages/overture-schema-base-theme/src/overture/schema/base/water.py new file mode 100644 index 000000000..f4a7518e9 --- /dev/null +++ b/packages/overture-schema-base-theme/src/overture/schema/base/water.py @@ -0,0 +1,167 @@ +"""Water feature models for Overture Maps base theme.""" + +import textwrap +from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from overture.schema.base._common import SourcedFromOpenStreetMap +from overture.schema.core import ( + OvertureFeature, +) +from overture.schema.core.models import Stacked +from overture.schema.core.names import Named +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) + + +class WaterSubtype(str, Enum): + """ + The broad classification of water body such as river, ocean or lake. + + This broad classification can be refined using `WaterClass`. + """ + + CANAL = "canal" + HUMAN_MADE = "human_made" + LAKE = "lake" + OCEAN = "ocean" + PHYSICAL = "physical" + POND = "pond" + RESERVOIR = "reservoir" + RIVER = "river" + SPRING = "spring" + STREAM = "stream" + WASTEWATER = "wastewater" + WATER = "water" + + +class WaterClass(str, Enum): + """ + Further description of the type of water body. + + The water class adds detail to the broad classification of `WaterSubtype`. + """ + + BASIN = "basin" + BAY = "bay" + BLOWHOLE = "blowhole" + CANAL = "canal" + CAPE = "cape" + DITCH = "ditch" + DOCK = "dock" + DRAIN = "drain" + FAIRWAY = "fairway" + FISH_PASS = "fish_pass" + FISHPOND = "fishpond" + GEYSER = "geyser" + HOT_SPRING = "hot_spring" + LAGOON = "lagoon" + LAKE = "lake" + MOAT = "moat" + OCEAN = "ocean" + OXBOW = "oxbow" + POND = "pond" + REFLECTING_POOL = "reflecting_pool" + RESERVOIR = "reservoir" + RIVER = "river" + SALT_POND = "salt_pond" + SEA = "sea" + SEWAGE = "sewage" + SHOAL = "shoal" + SPRING = "spring" + STRAIT = "strait" + STREAM = "stream" + SWIMMING_POOL = "swimming_pool" + TIDAL_CHANNEL = "tidal_channel" + WASTEWATER = "wastewater" + WATER = "water" + WATER_STORAGE = "water_storage" + WATERFALL = "waterfall" + + +class Water( + OvertureFeature[Literal["base"], Literal["water"]], + Stacked, + Named, + SourcedFromOpenStreetMap, +): + """ + Water features represent ocean and inland water bodies. + + In Overture data releases, water features are sourced from OpenStreetMap. There are two main + categories of water feature: ocean and inland water bodies. + + Ocean + ----- + The `subytpe` value `"ocean"` indicates an ocean area feature whose geometry represents the + surface area of an ocean or part of an ocean. Ocean area may be tiled into many small polygons + of consistent complexity to ensure manageable geometry. In Overture data releases, ocean area + features are created from OpenStreetMap coastlines data (`natural=coastline`) using a QA'd + version of the output from the OSMCoastline tool. In aggregate, all the ocean area features + represent the inverse of the land features with subtype `"land"` and class `"land"`. + + The names and recommended label position for oceans and seas can be found in features with the + subtype `"physical"` and the class `"ocean"` or `"sea"`. + + Inland Water + ------------ + Subtypes other than `"ocean"` (and `"physical"`) represent inland water bodies. In Overture data + releases, these features are sourced from the OpenStreetMap tag `natural=*` where the tag value + indicates a water body, as well as the tags `natural=water`, `waterway=*`, + and `water=*`. + """ + + model_config = ConfigDict(title="water") + + # Overture Feature + + geometry: Annotated[ + Geometry, + GeometryTypeConstraint( + GeometryType.POINT, + GeometryType.LINE_STRING, + GeometryType.POLYGON, + GeometryType.MULTI_POLYGON, + ), + Field( + description=textwrap.dedent(""" + Geometry of the water feature, which may be a point, line string, polygon, or + multi-polygon. + """).strip() + ), + ] + + # Required + + class_: Annotated[ + WaterClass, + Field( + default=WaterClass.WATER, + alias="class", + ), + ] = WaterClass.WATER + subtype: Annotated[ + WaterSubtype, + Field( + default=WaterSubtype.WATER, + ), + ] = WaterSubtype.WATER + + # Optional + + is_intermittent: Annotated[ + bool | None, + Field( + description="Whether the water body exists intermittently, not permanently", + strict=True, + ), + ] = None + is_salt: Annotated[ + bool | None, + Field(description="Whether the water body contains salt water", strict=True), + ] = None diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/water/__init__.py b/packages/overture-schema-base-theme/src/overture/schema/base/water/__init__.py deleted file mode 100644 index 75e8f2f41..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/water/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .enums import WaterClass, WaterSubtype -from .models import ( - Water, -) - -__all__ = [ - "Water", - "WaterSubtype", - "WaterClass", -] diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/water/enums.py b/packages/overture-schema-base-theme/src/overture/schema/base/water/enums.py deleted file mode 100644 index 28de78487..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/water/enums.py +++ /dev/null @@ -1,58 +0,0 @@ -from enum import Enum - - -class WaterSubtype(str, Enum): - """The type of water body such as an river, ocean or lake.""" - - CANAL = "canal" - HUMAN_MADE = "human_made" - LAKE = "lake" - OCEAN = "ocean" - PHYSICAL = "physical" - POND = "pond" - RESERVOIR = "reservoir" - RIVER = "river" - SPRING = "spring" - STREAM = "stream" - WASTEWATER = "wastewater" - WATER = "water" - - -class WaterClass(str, Enum): - """Further description of the type of water.""" - - BASIN = "basin" - BAY = "bay" - BLOWHOLE = "blowhole" - CANAL = "canal" - CAPE = "cape" - DITCH = "ditch" - DOCK = "dock" - DRAIN = "drain" - FAIRWAY = "fairway" - FISH_PASS = "fish_pass" - FISHPOND = "fishpond" - GEYSER = "geyser" - HOT_SPRING = "hot_spring" - LAGOON = "lagoon" - LAKE = "lake" - MOAT = "moat" - OCEAN = "ocean" - OXBOW = "oxbow" - POND = "pond" - REFLECTING_POOL = "reflecting_pool" - RESERVOIR = "reservoir" - RIVER = "river" - SALT_POND = "salt_pond" - SEA = "sea" - SEWAGE = "sewage" - SHOAL = "shoal" - SPRING = "spring" - STRAIT = "strait" - STREAM = "stream" - SWIMMING_POOL = "swimming_pool" - TIDAL_CHANNEL = "tidal_channel" - WASTEWATER = "wastewater" - WATER = "water" - WATER_STORAGE = "water_storage" - WATERFALL = "waterfall" diff --git a/packages/overture-schema-base-theme/src/overture/schema/base/water/models.py b/packages/overture-schema-base-theme/src/overture/schema/base/water/models.py deleted file mode 100644 index f63fe6862..000000000 --- a/packages/overture-schema-base-theme/src/overture/schema/base/water/models.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Water feature models for Overture Maps base theme.""" - -from typing import Annotated, Literal - -from pydantic import ConfigDict, Field - -from overture.schema.base.models import SourcedFromOpenStreetMap -from overture.schema.base.water.enums import WaterClass, WaterSubtype -from overture.schema.core import ( - OvertureFeature, -) -from overture.schema.core.models import Stacked -from overture.schema.core.names import Named -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) - - -class Water( - OvertureFeature[Literal["base"], Literal["water"]], - Stacked, - Named, - SourcedFromOpenStreetMap, -): - """Physical representations of inland and ocean marine surfaces. - - Translates `natural` and `waterway` tags from OpenStreetMap. - """ - - model_config = ConfigDict(title="water") - - # Core - - geometry: Annotated[ - Geometry, - GeometryTypeConstraint( - GeometryType.POINT, - GeometryType.LINE_STRING, - GeometryType.POLYGON, - GeometryType.MULTI_POLYGON, - ), - Field( - description="Geometry (Point, LineString, Polygon, or MultiPolygon)", - ), - ] - - # Required - - class_: Annotated[ - WaterClass, - Field( - default=WaterClass.WATER, - alias="class", - ), - ] = WaterClass.WATER - subtype: Annotated[ - WaterSubtype, - Field( - default=WaterSubtype.WATER, - ), - ] = WaterSubtype.WATER - - # Optional - - is_intermittent: Annotated[ - bool | None, Field(description="Is it intermittent water or not", strict=True) - ] = None - is_salt: Annotated[ - bool | None, Field(description="Is it salt water or not", strict=True) - ] = None diff --git a/packages/overture-schema-base-theme/tests/bathymetry_baseline_schema.json b/packages/overture-schema-base-theme/tests/bathymetry_baseline_schema.json index 9ac75f590..41fa38d73 100644 --- a/packages/overture-schema-base-theme/tests/bathymetry_baseline_schema.json +++ b/packages/overture-schema-base-theme/tests/bathymetry_baseline_schema.json @@ -2,31 +2,33 @@ "$defs": { "CartographicHints": { "additionalProperties": false, - "description": "Defines cartographic hints for optimal use of Overture features in map-making.", + "description": "Cartographic hints for optimal use of Overture features in map-making.", "properties": { "max_zoom": { - "description": "Recommended maximum tile zoom per the Slippy Maps convention.\n\nThe Slippy Maps zooms are explained in the following references:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", + "description": "Recommended maximum tile zoom level in which this feature should be displayed.\n\nIt is recommended that the feature be hidden at zoom levels above this value.\n\nZoom levels follow the Slippy Maps convention, documented in the following\nreferences:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", "maximum": 23, "minimum": 0, "title": "Max Zoom", "type": "integer" }, "min_zoom": { - "description": "Recommended minimum tile zoom per the Slippy Maps convention.\n\nThe Slippy Maps zooms are explained in the following references:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", + "description": "Recommended minimum tile zoom level in which this feature should be displayed.\n\nIt is recommended that the feature be hidden at zoom levels below this value.\n\nZoom levels follow the Slippy Maps convention, documented in the following\nreferences:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", "maximum": 23, "minimum": 0, "title": "Min Zoom", "type": "integer" }, "prominence": { - "description": "Represents Overture's view of a place's significance or importance. This value can be used to help drive cartographic display of a place and is derived from various factors including, but not limited to: population, capital status, place tags, and type.", - "exclusiveMaximum": 100, + "description": "Subjective scale of feature significance or importance, with 1 being the least, and\n100 being the most, significant.\n\nThis value can be used to help drive decisions about how and when to display a\nfeature, and how to treat it relative to neighboring features.\n\nWhen populated by Overture, this value is derived from various factors including,\nbut not limited to: feature and subtype, population, and capital status.", + "maximum": 100, "minimum": 1, "title": "Prominence", "type": "integer" }, "sort_key": { - "description": "An ascending numeric that defines the recommended order features should be drawn in. Features with lower number should be shown on top of features with a higher number.", + "description": "Integer indicating the recommended order in which to draw features.\n\nFeatures with a lower number should be drawn \"in front\" of features with a higher\nnumber.", + "maximum": 255, + "minimum": 0, "title": "Sort Key", "type": "integer" } @@ -34,12 +36,12 @@ "title": "CartographicHints", "type": "object" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -51,34 +53,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -88,12 +91,12 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, "additionalProperties": false, - "description": "Topographic representation of an underwater area, such as a part of the ocean\nfloor.", + "description": "Bathymetry features provide topographic representations of underwater areas, such as parts of\nlake beds or ocean floors.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -106,7 +109,7 @@ "type": "array" }, "geometry": { - "description": "Geometry (Polygon or MultiPolygon)", + "description": "Shape of the underwater area, which may be a polygon or multi-polygon.", "oneOf": [ { "properties": { @@ -214,16 +217,16 @@ "title": "cartography" }, "depth": { - "description": "Depth below surface level (in meters) of the feature.", + "description": "Depth below surface level of the feature in meters.", "maximum": 2147483647, "minimum": 0, "title": "Depth", "type": "integer" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-base-theme/tests/infrastructure_baseline_schema.json b/packages/overture-schema-base-theme/tests/infrastructure_baseline_schema.json index e4d93c55e..1deda233e 100644 --- a/packages/overture-schema-base-theme/tests/infrastructure_baseline_schema.json +++ b/packages/overture-schema-base-theme/tests/infrastructure_baseline_schema.json @@ -1,7 +1,7 @@ { "$defs": { "InfrastructureClass": { - "description": "Further classification of the infrastructure type.", + "description": "Further classification of the type of infrastructure.\n\nThe infrastructure class adds detail to the broad classification of `InfrastructureSubtype`.", "enum": [ "aerialway_station", "airport", @@ -172,7 +172,7 @@ "type": "string" }, "InfrastructureSubtype": { - "description": "Further description of the type of infrastructure.", + "description": "Broadest classification of the type of infrastructure.\n\nThis broad classification can be refined by `InfrastructureClass`.", "enum": [ "aerialway", "airport", @@ -198,7 +198,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -213,7 +213,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -227,14 +227,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -245,6 +246,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -343,12 +345,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -360,34 +362,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -397,11 +400,11 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" }, "SurfaceMaterial": { - "description": "Surface material enum used by infrastructure and land features.", + "description": "Material that makes up the surface of `Infrastructure` and `Land` features.", "enum": [ "asphalt", "cobblestone", @@ -433,7 +436,7 @@ } }, "additionalProperties": false, - "description": "Various features from OpenStreetMap such as bridges, airport runways, aerialways,\nor communication towers and lines.", + "description": "Infrastructure features provide basic information about real-world infrastructure entitites\nsuch as bridges, airports, runways, aerialways, communication towers, and power lines.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -446,7 +449,7 @@ "type": "array" }, "geometry": { - "description": "Geometry (Point, LineString, Polygon, or MultiPolygon)", + "description": "Geometry of the infrastructure feature, which may be a point, line string, polygon, or\nmulti-polygon.", "oneOf": [ { "properties": { @@ -619,9 +622,10 @@ "type": "number" }, "level": { + "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Level", "type": "integer" }, @@ -630,14 +634,14 @@ }, "source_tags": { "additionalProperties": true, - "description": "Any attributes/tags from the original source data that should be passed through.", + "description": "Key/value pairs imported directly from the source data without change.\n\nThis field provides access to raw OSM entity tags for features sourced from\nOpenStreetMap.", "title": "Source Tags", "type": "object" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", @@ -694,6 +698,6 @@ "geometry", "properties" ], - "title": "Infrastructure Schema", + "title": "infrastructure", "type": "object" } \ No newline at end of file diff --git a/packages/overture-schema-base-theme/tests/land_baseline_schema.json b/packages/overture-schema-base-theme/tests/land_baseline_schema.json index 55022ea51..35c904003 100644 --- a/packages/overture-schema-base-theme/tests/land_baseline_schema.json +++ b/packages/overture-schema-base-theme/tests/land_baseline_schema.json @@ -1,7 +1,7 @@ { "$defs": { "LandClass": { - "description": "Further classification of type of landcover.", + "description": "Further classification of the land.\n\nThe land class adds detail to the broad classification of `LandSubtype`.", "enum": [ "archipelago", "bare_rock", @@ -50,7 +50,7 @@ "type": "string" }, "LandSubtype": { - "description": "Further description of the type of land cover, such as forest, glacier, grass, or\na physical feature, such as a mountain peak.", + "description": "Broadest classification of the land.\n\nThis broad classification can be refined by `LandClass`.", "enum": [ "crater", "desert", @@ -71,7 +71,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -86,7 +86,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -100,14 +100,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -118,6 +119,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -216,12 +218,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -233,34 +235,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -270,11 +273,11 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" }, "SurfaceMaterial": { - "description": "Surface material enum used by infrastructure and land features.", + "description": "Material that makes up the surface of `Infrastructure` and `Land` features.", "enum": [ "asphalt", "cobblestone", @@ -306,7 +309,7 @@ } }, "additionalProperties": false, - "description": "Physical representations of land surfaces.\n\nGlobal land derived from the inverse of OSM Coastlines. Translates `natural` tags from OpenStreetMap.", + "description": "Land features are representations of physical land surfaces.\n\nIn Overture data releases, land features are sourced from OpenStreetMap. TODO. Finish this when\nI get more info from Jennings.\n\n\n\nPhysical representations of land surfaces.\n\nGlobal land derived from the inverse of OSM Coastlines. Translates `natural` tags from OpenStreetMap.\n\nTODO: Update this description when the relationship to `land_cover` is better understood.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -319,7 +322,7 @@ "type": "array" }, "geometry": { - "description": "Geometry (Point, LineString, Polygon, or MultiPolygon)", + "description": "Geometry of the land feature, which may be a point, line string, polygon, or\nmulti-polygon.", "oneOf": [ { "properties": { @@ -487,16 +490,17 @@ "default": "land" }, "elevation": { - "description": "Elevation above sea level (in meters) of the feature.", + "description": "Elevation above sea level of the feature in meters.", "maximum": 9000, "minimum": -2147483648, "title": "Elevation", "type": "integer" }, "level": { + "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Level", "type": "integer" }, @@ -505,14 +509,14 @@ }, "source_tags": { "additionalProperties": true, - "description": "Any attributes/tags from the original source data that should be passed through.", + "description": "Key/value pairs imported directly from the source data without change.\n\nThis field provides access to raw OSM entity tags for features sourced from\nOpenStreetMap.", "title": "Source Tags", "type": "object" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-base-theme/tests/land_cover_baseline_schema.json b/packages/overture-schema-base-theme/tests/land_cover_baseline_schema.json index 783d48d5b..84fb66df4 100644 --- a/packages/overture-schema-base-theme/tests/land_cover_baseline_schema.json +++ b/packages/overture-schema-base-theme/tests/land_cover_baseline_schema.json @@ -2,31 +2,33 @@ "$defs": { "CartographicHints": { "additionalProperties": false, - "description": "Defines cartographic hints for optimal use of Overture features in map-making.", + "description": "Cartographic hints for optimal use of Overture features in map-making.", "properties": { "max_zoom": { - "description": "Recommended maximum tile zoom per the Slippy Maps convention.\n\nThe Slippy Maps zooms are explained in the following references:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", + "description": "Recommended maximum tile zoom level in which this feature should be displayed.\n\nIt is recommended that the feature be hidden at zoom levels above this value.\n\nZoom levels follow the Slippy Maps convention, documented in the following\nreferences:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", "maximum": 23, "minimum": 0, "title": "Max Zoom", "type": "integer" }, "min_zoom": { - "description": "Recommended minimum tile zoom per the Slippy Maps convention.\n\nThe Slippy Maps zooms are explained in the following references:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", + "description": "Recommended minimum tile zoom level in which this feature should be displayed.\n\nIt is recommended that the feature be hidden at zoom levels below this value.\n\nZoom levels follow the Slippy Maps convention, documented in the following\nreferences:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", "maximum": 23, "minimum": 0, "title": "Min Zoom", "type": "integer" }, "prominence": { - "description": "Represents Overture's view of a place's significance or importance. This value can be used to help drive cartographic display of a place and is derived from various factors including, but not limited to: population, capital status, place tags, and type.", - "exclusiveMaximum": 100, + "description": "Subjective scale of feature significance or importance, with 1 being the least, and\n100 being the most, significant.\n\nThis value can be used to help drive decisions about how and when to display a\nfeature, and how to treat it relative to neighboring features.\n\nWhen populated by Overture, this value is derived from various factors including,\nbut not limited to: feature and subtype, population, and capital status.", + "maximum": 100, "minimum": 1, "title": "Prominence", "type": "integer" }, "sort_key": { - "description": "An ascending numeric that defines the recommended order features should be drawn in. Features with lower number should be shown on top of features with a higher number.", + "description": "Integer indicating the recommended order in which to draw features.\n\nFeatures with a lower number should be drawn \"in front\" of features with a higher\nnumber.", + "maximum": 255, + "minimum": 0, "title": "Sort Key", "type": "integer" } @@ -35,7 +37,7 @@ "type": "object" }, "LandCoverSubtype": { - "description": "Type of surface represented.", + "description": "Primary or dominant material covering the land.", "enum": [ "barren", "crop", @@ -51,12 +53,12 @@ "title": "LandCoverSubtype", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -68,34 +70,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -105,12 +108,12 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, "additionalProperties": false, - "description": "Representation of the Earth's natural surfaces.", + "description": "Land cover features indicate the primary natural or artificial surface material covering a land\narea on the earth, including vegetation types like forests and crops, built environments like\ncities, and natural surfaces like wetlands or barren ground.\n\nLand cover features relate to `LandUse` features in the following way: land cover is the\nphysical thing covering the land, while land use is the human use to which the land is being\nput.\n\nTODO: Explain relationship to `Land` features.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -123,7 +126,7 @@ "type": "array" }, "geometry": { - "description": "Geometry (Polygon or MultiPolygon)", + "description": "Shape of the covered land area, which may be a polygon or multi-polygon.", "oneOf": [ { "properties": { @@ -231,9 +234,9 @@ "title": "cartography" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-base-theme/tests/land_use_baseline_schema.json b/packages/overture-schema-base-theme/tests/land_use_baseline_schema.json index a4dd509a1..161673bbb 100644 --- a/packages/overture-schema-base-theme/tests/land_use_baseline_schema.json +++ b/packages/overture-schema-base-theme/tests/land_use_baseline_schema.json @@ -1,7 +1,7 @@ { "$defs": { "LandUseClass": { - "description": "Further classification of the land use.", + "description": "Further classification of the land use.\n\nThe land use class adds detail to the broad classification of `LandUseSubtype`.", "enum": [ "aboriginal_land", "airfield", @@ -117,7 +117,7 @@ "type": "string" }, "LandUseSubtype": { - "description": "Broad type of land.", + "description": "Broadest classification of the land use.\n\nThis broad classification can be refined by `LandUseClass`.", "enum": [ "agriculture", "aquaculture", @@ -149,7 +149,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -164,7 +164,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -178,14 +178,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -196,6 +197,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -294,12 +296,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -311,34 +313,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -348,11 +351,11 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" }, "SurfaceMaterial": { - "description": "Surface material enum used by infrastructure and land features.", + "description": "Material that makes up the surface of `Infrastructure` and `Land` features.", "enum": [ "asphalt", "cobblestone", @@ -384,7 +387,7 @@ } }, "additionalProperties": false, - "description": "Land use features from OpenStreetMap.", + "description": "Land use features specify the predominant human use of an area of land, for example commercial\nactivity, recreation, farming, housing, education, or military use.\n\nLand use features relate to `LandCover` features in the following way: land use is the human\nhuman activity being done with the land, while land cover is the physical thing that covers it.\n\nTODO: Explain relationship to `Land` features.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -397,7 +400,7 @@ "type": "array" }, "geometry": { - "description": "Classifications of the human use of a section of land. Translates `landuse` from OpenStreetMap tag from OpenStreetMap.", + "description": "Geometry of the land use area, which may be a point, line string, polygon, or\nmulti-polygon.", "oneOf": [ { "properties": { @@ -564,16 +567,17 @@ "$ref": "#/$defs/LandUseClass" }, "elevation": { - "description": "Elevation above sea level (in meters) of the feature.", + "description": "Elevation above sea level of the feature in meters.", "maximum": 9000, "minimum": -2147483648, "title": "Elevation", "type": "integer" }, "level": { + "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Level", "type": "integer" }, @@ -582,14 +586,14 @@ }, "source_tags": { "additionalProperties": true, - "description": "Any attributes/tags from the original source data that should be passed through.", + "description": "Key/value pairs imported directly from the source data without change.\n\nThis field provides access to raw OSM entity tags for features sourced from\nOpenStreetMap.", "title": "Source Tags", "type": "object" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-base-theme/tests/test_bathymetry_json_schema_baseline.py b/packages/overture-schema-base-theme/tests/test_bathymetry_json_schema_baseline.py index 3585ed070..83e29eeef 100644 --- a/packages/overture-schema-base-theme/tests/test_bathymetry_json_schema_baseline.py +++ b/packages/overture-schema-base-theme/tests/test_bathymetry_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.base import Bathymetry -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_bathymetry_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/tests/test_infrastructure_json_schema_baseline.py b/packages/overture-schema-base-theme/tests/test_infrastructure_json_schema_baseline.py index 0b8686ad1..1eef52106 100644 --- a/packages/overture-schema-base-theme/tests/test_infrastructure_json_schema_baseline.py +++ b/packages/overture-schema-base-theme/tests/test_infrastructure_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.base import Infrastructure -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_infrastructure_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/tests/test_land_cover_json_schema_baseline.py b/packages/overture-schema-base-theme/tests/test_land_cover_json_schema_baseline.py index c13b42dcd..f16534871 100644 --- a/packages/overture-schema-base-theme/tests/test_land_cover_json_schema_baseline.py +++ b/packages/overture-schema-base-theme/tests/test_land_cover_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.base import LandCover -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_land_cover_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/tests/test_land_json_schema_baseline.py b/packages/overture-schema-base-theme/tests/test_land_json_schema_baseline.py index c96ae10cb..1feccbbcd 100644 --- a/packages/overture-schema-base-theme/tests/test_land_json_schema_baseline.py +++ b/packages/overture-schema-base-theme/tests/test_land_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.base import Land -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_land_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/tests/test_land_use_json_schema_baseline.py b/packages/overture-schema-base-theme/tests/test_land_use_json_schema_baseline.py index 7dbdc5e4d..2f9dbf55d 100644 --- a/packages/overture-schema-base-theme/tests/test_land_use_json_schema_baseline.py +++ b/packages/overture-schema-base-theme/tests/test_land_use_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.base import LandUse -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_land_use_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/tests/test_water_json_schema_baseline.py b/packages/overture-schema-base-theme/tests/test_water_json_schema_baseline.py index 662030e7c..d85b1194d 100644 --- a/packages/overture-schema-base-theme/tests/test_water_json_schema_baseline.py +++ b/packages/overture-schema-base-theme/tests/test_water_json_schema_baseline.py @@ -4,7 +4,7 @@ import os from overture.schema.base import Water -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema def test_water_json_schema_baseline() -> None: diff --git a/packages/overture-schema-base-theme/tests/water_baseline_schema.json b/packages/overture-schema-base-theme/tests/water_baseline_schema.json index 60be51909..348e65aea 100644 --- a/packages/overture-schema-base-theme/tests/water_baseline_schema.json +++ b/packages/overture-schema-base-theme/tests/water_baseline_schema.json @@ -2,7 +2,7 @@ "$defs": { "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -17,7 +17,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -31,14 +31,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -49,6 +50,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -147,12 +149,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -164,34 +166,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -201,11 +204,11 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" }, "WaterClass": { - "description": "Further description of the type of water.", + "description": "Further description of the type of water body.\n\nThe water class adds detail to the broad classification of `WaterSubtype`.", "enum": [ "basin", "bay", @@ -247,7 +250,7 @@ "type": "string" }, "WaterSubtype": { - "description": "The type of water body such as an river, ocean or lake.", + "description": "The broad classification of water body such as river, ocean or lake.\n\nThis broad classification can be refined using `WaterClass`.", "enum": [ "canal", "human_made", @@ -267,7 +270,7 @@ } }, "additionalProperties": false, - "description": "Physical representations of inland and ocean marine surfaces.\n\nTranslates `natural` and `waterway` tags from OpenStreetMap.", + "description": "Water features represent ocean and inland water bodies.\n\nIn Overture data releases, water features are sourced from OpenStreetMap. There are two main\ncategories of water feature: ocean and inland water bodies.\n\nOcean\n-----\nThe `subytpe` value `\"ocean\"` indicates an ocean area feature whose geometry represents the\nsurface area of an ocean or part of an ocean. Ocean area may be tiled into many small polygons\nof consistent complexity to ensure manageable geometry. In Overture data releases, ocean area\nfeatures are created from OpenStreetMap coastlines data (`natural=coastline`) using a QA'd\nversion of the output from the OSMCoastline tool. In aggregate, all the ocean area features\nrepresent the inverse of the land features with subtype `\"land\"` and class `\"land\"`.\n\nThe names and recommended label position for oceans and seas can be found in features with the\nsubtype `\"physical\"` and the class `\"ocean\"` or `\"sea\"`.\n\nInland Water\n------------\nSubtypes other than `\"ocean\"` (and `\"physical\"`) represent inland water bodies. In Overture data\nreleases, these features are sourced from the OpenStreetMap tag `natural=*` where the tag value\nindicates a water body, as well as the tags `natural=water`, `waterway=*`,\nand `water=*`.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -280,7 +283,7 @@ "type": "array" }, "geometry": { - "description": "Geometry (Point, LineString, Polygon, or MultiPolygon)", + "description": "Geometry of the water feature, which may be a point, line string, polygon, or\nmulti-polygon.", "oneOf": [ { "properties": { @@ -448,19 +451,20 @@ "default": "water" }, "is_intermittent": { - "description": "Is it intermittent water or not", + "description": "Whether the water body exists intermittently, not permanently", "title": "Is Intermittent", "type": "boolean" }, "is_salt": { - "description": "Is it salt water or not", + "description": "Whether the water body contains salt water", "title": "Is Salt", "type": "boolean" }, "level": { + "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Level", "type": "integer" }, @@ -469,14 +473,14 @@ }, "source_tags": { "additionalProperties": true, - "description": "Any attributes/tags from the original source data that should be passed through.", + "description": "Key/value pairs imported directly from the source data without change.\n\nThis field provides access to raw OSM entity tags for features sourced from\nOpenStreetMap.", "title": "Source Tags", "type": "object" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-buildings-theme/pyproject.toml b/packages/overture-schema-buildings-theme/pyproject.toml index bc1612551..3663fb6c8 100644 --- a/packages/overture-schema-buildings-theme/pyproject.toml +++ b/packages/overture-schema-buildings-theme/pyproject.toml @@ -25,5 +25,5 @@ path = "src/overture/schema/buildings/__about__.py" packages = ["src/overture"] [project.entry-points."overture.models"] -"buildings.building" = "overture.schema.buildings.building.models:Building" -"buildings.building_part" = "overture.schema.buildings.building_part.models:BuildingPart" +"buildings.building" = "overture.schema.buildings:Building" +"buildings.building_part" = "overture.schema.buildings:BuildingPart" diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/__init__.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/__init__.py index 489c9051b..460b4fdaf 100644 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/__init__.py +++ b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/__init__.py @@ -6,7 +6,24 @@ __path__ = __import__("pkgutil").extend_path(__path__, __name__) -from .building import Building +from ._common import ( + Appearance, + FacadeMaterial, + RoofMaterial, + RoofOrientation, + RoofShape, +) +from .building import Building, BuildingClass, BuildingSubtype from .building_part import BuildingPart -__all__ = ["Building", "BuildingPart"] +__all__ = [ + "Appearance", + "Building", + "BuildingClass", + "BuildingPart", + "BuildingSubtype", + "FacadeMaterial", + "RoofMaterial", + "RoofOrientation", + "RoofShape", +] diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/_common.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/_common.py new file mode 100644 index 000000000..b2b558718 --- /dev/null +++ b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/_common.py @@ -0,0 +1,204 @@ +import textwrap +from enum import Enum +from typing import Annotated + +from pydantic import BaseModel, Field + +from overture.schema.system.doc import DocumentedEnum +from overture.schema.system.primitive import float64, int32 +from overture.schema.system.string import HexColor + + +class FacadeMaterial(str, Enum): + """The outer surface material of building facade.""" + + BRICK = "brick" + CEMENT_BLOCK = "cement_block" + CLAY = "clay" + CONCRETE = "concrete" + GLASS = "glass" + METAL = "metal" + PLASTER = "plaster" + PLASTIC = "plastic" + STONE = "stone" + TIMBER_FRAMING = "timber_framing" + WOOD = "wood" + + +class RoofMaterial(str, Enum): + """The outermost material of the roof.""" + + CONCRETE = "concrete" + COPPER = "copper" + ETERNIT = "eternit" + GLASS = "glass" + GRASS = "grass" + GRAVEL = "gravel" + METAL = "metal" + PLASTIC = "plastic" + ROOF_TILES = "roof_tiles" + SLATE = "slate" + SOLAR_PANELS = "solar_panels" + TAR_PAPER = "tar_paper" + THATCH = "thatch" + WOOD = "wood" + + +class RoofShape(str, Enum): + """The shape of the roof.""" + + DOME = "dome" + FLAT = "flat" + GABLED = "gabled" + GAMBREL = "gambrel" + HALF_HIPPED = "half_hipped" + HIPPED = "hipped" + MANSARD = "mansard" + ONION = "onion" + PYRAMIDAL = "pyramidal" + ROUND = "round" + SALTBOX = "saltbox" + SAWTOOTH = "sawtooth" + SKILLION = "skillion" + SPHERICAL = "spherical" + + +class RoofOrientation(str, DocumentedEnum): + """ + Orientation of the roof shape relative to the footprint shape. + + The members of this enumeration, `"across"` and `"along"`, are borrowed from the OpenStreetMap + `roof:orientation=*` tag and have the same meanings as they do in OSM. + """ + + ACROSS = ( + "across", + "The roof ridge runs perpendicular to the longer of the two building edges, parallel to the shorter", + ) + ALONG = ( + "along", + "The roof ridge runs parallel to the longer of the two building edges", + ) + + +class Appearance(BaseModel): + """Physical and visual properties of a building, including dimensions, materials, and colors.""" + + # Optional + + height: Annotated[ + float64 | None, + Field( + gt=0, + description=textwrap.dedent(""" + Height of the building or part in meters. + + This is the distance from the lowest point to the highest point. + """).strip(), + ), + ] = None + is_underground: Annotated[ + bool | None, + Field( + description=textwrap.dedent(""" + Whether the entire building or part is completely below ground. + + The underground flag is useful for display purposes. Buildings and building parts + that are entirely below ground can be styled differently or omitted from the + rendered image. + + This flag is conceptually different from the `level` field, which indicates + relative z-ordering and, notably, can be negative even if the building is entirely + above-ground. + """).strip(), + strict=True, + ), + ] = None + num_floors: Annotated[ + int32 | None, + Field( + gt=0, + description="Number of above-ground floors of the building or part.", + ), + ] = None + num_floors_underground: Annotated[ + int32 | None, + Field( + gt=0, + description="Number of below-ground floors of the building or part.", + ), + ] = None + min_height: Annotated[ + float64 | None, + Field( + description=textwrap.dedent(""" + Altitude above ground where the bottom of the building or building part starts. + + If present, this value indicates that the lowest part of the building or building + part starts is above ground level. + """).strip(), + ), + ] = None + min_floor: Annotated[ + int32 | None, + Field( + gt=0, + description=textwrap.dedent(""" + Start floor of this building or part. + + If present, this value indicates that the building or part is "floating" and its + bottom-most floor is above ground level, usually because it is part of a larger + building in which some parts do reach down to ground level. An example is a building + that has an entry road or driveway at ground level into an interior courtyard, where + part of the building bridges above the entry road. This property may sometimes be + populated when `min_height` is missing and in these cases can be used as a proxy for + `min_height`. + """).strip(), + ), + ] = None + facade_color: Annotated[ + HexColor | None, + Field( + description="Facade color in `#rgb` or `#rrggbb` hex notation", + ), + ] = None + facade_material: Annotated[ + FacadeMaterial | None, + Field(description="Outer surface material of the facade"), + ] = None + roof_material: Annotated[ + RoofMaterial | None, Field(description="Outer surface material of the roof") + ] = None + roof_shape: Annotated[RoofShape | None, Field(description="Shape of the roof")] = ( + None + ) + roof_direction: Annotated[ + float64 | None, + Field( + ge=0, + lt=360, + description="Bearing of the roof ridge line in degrees", + ), + ] = None + roof_orientation: Annotated[ + RoofOrientation | None, + Field( + description="""Orientation of the roof shape relative to the footprint shape""", + ), + ] = None + roof_color: Annotated[ + HexColor | None, + Field( + description="The roof color in `#rgb` or `#rrggbb` hex notation", + ), + ] = None + roof_height: Annotated[ + float64 | None, + Field( + description=textwrap.dedent(""" + Height of the roof in meters. + + This is the distance from the base of the roof to its highest point. + """).strip(), + ), + ] = None diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building.py new file mode 100644 index 000000000..3af49134a --- /dev/null +++ b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building.py @@ -0,0 +1,201 @@ +""" +The `Building` feature type model and supporting types. +""" + +import textwrap +from enum import Enum +from typing import Annotated, Literal + +from pydantic import ConfigDict, Field + +from overture.schema.buildings._common import Appearance +from overture.schema.core import OvertureFeature +from overture.schema.core.models import Stacked +from overture.schema.core.names import Named +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) + + +class BuildingSubtype(str, Enum): + """ + Broadest classification of the type and purpose of a building. + + This broad classification can be refined by `BuildingClass`. + """ + + AGRICULTURAL = "agricultural" + CIVIC = "civic" + COMMERCIAL = "commercial" + EDUCATION = "education" + ENTERTAINMENT = "entertainment" + INDUSTRIAL = "industrial" + MEDICAL = "medical" + MILITARY = "military" + OUTBUILDING = "outbuilding" + RELIGIOUS = "religious" + RESIDENTIAL = "residential" + SERVICE = "service" + TRANSPORTATION = "transportation" + + +class BuildingClass(str, Enum): + """ + Further classification of the type and purpose of a building. + + The building class adds detail to the broad classification of `BuildingSubtype`. + """ + + AGRICULTURAL = "agricultural" + ALLOTMENT_HOUSE = "allotment_house" + APARTMENTS = "apartments" + BARN = "barn" + BEACH_HUT = "beach_hut" + BOATHOUSE = "boathouse" + BRIDGE_STRUCTURE = "bridge_structure" + BUNGALOW = "bungalow" + BUNKER = "bunker" + CABIN = "cabin" + CARPORT = "carport" + CATHEDRAL = "cathedral" + CHAPEL = "chapel" + CHURCH = "church" + CIVIC = "civic" + COLLEGE = "college" + COMMERCIAL = "commercial" + COWSHED = "cowshed" + DETACHED = "detached" + DIGESTER = "digester" + DORMITORY = "dormitory" + DWELLING_HOUSE = "dwelling_house" + FACTORY = "factory" + FARM = "farm" + FARM_AUXILIARY = "farm_auxiliary" + FIRE_STATION = "fire_station" + GARAGE = "garage" + GARAGES = "garages" + GER = "ger" + GLASSHOUSE = "glasshouse" + GOVERNMENT = "government" + GRANDSTAND = "grandstand" + GREENHOUSE = "greenhouse" + GUARDHOUSE = "guardhouse" + HANGAR = "hangar" + HOSPITAL = "hospital" + HOTEL = "hotel" + HOUSE = "house" + HOUSEBOAT = "houseboat" + HUT = "hut" + INDUSTRIAL = "industrial" + KINDERGARTEN = "kindergarten" + KIOSK = "kiosk" + LIBRARY = "library" + MANUFACTURE = "manufacture" + MILITARY = "military" + MONASTERY = "monastery" + MOSQUE = "mosque" + OFFICE = "office" + OUTBUILDING = "outbuilding" + PARKING = "parking" + PAVILION = "pavilion" + POST_OFFICE = "post_office" + PRESBYTERY = "presbytery" + PUBLIC = "public" + RELIGIOUS = "religious" + RESIDENTIAL = "residential" + RETAIL = "retail" + ROOF = "roof" + SCHOOL = "school" + SEMI = "semi" + SEMIDETACHED_HOUSE = "semidetached_house" + SERVICE = "service" + SHED = "shed" + SHRINE = "shrine" + SILO = "silo" + SLURRY_TANK = "slurry_tank" + SPORTS_CENTRE = "sports_centre" + SPORTS_HALL = "sports_hall" + STABLE = "stable" + STADIUM = "stadium" + STATIC_CARAVAN = "static_caravan" + STILT_HOUSE = "stilt_house" + STORAGE_TANK = "storage_tank" + STY = "sty" + SUPERMARKET = "supermarket" + SYNAGOGUE = "synagogue" + TEMPLE = "temple" + TERRACE = "terrace" + TOILETS = "toilets" + TRAIN_STATION = "train_station" + TRANSFORMER_TOWER = "transformer_tower" + TRANSPORTATION = "transportation" + TRULLO = "trullo" + UNIVERSITY = "university" + WAREHOUSE = "warehouse" + WAYSIDE_SHRINE = "wayside_shrine" + + +class Building( + OvertureFeature[Literal["buildings"], Literal["building"]], + Named, + Stacked, + Appearance, +): + """ + Buildings are man-made structures with roofs that exists permanently in one place. + + A building's geometry represents the two-dimensional footprint of the building as viewed from + directly above, looking down. Fields such as `height` and `num_floors` allow the + three-dimensional shape to be approximated. Some buildings, identified by the `has_parts` field, + have associated `BuildingPart` features which can be used to generate a more representative 3D + model of the building. + """ + + model_config = ConfigDict(title="building") + + # Overture Feature + + geometry: Annotated[ + Geometry, + GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), + Field( + description="""The building's footprint or roofprint (if traced from aerial/satellite imagery).""", + ), + ] + + # Optional + + subtype: Annotated[ + BuildingSubtype | None, + Field( + description=textwrap.dedent(""" + A broad classification of the current use and purpose of the building. + + If the current use of the building no longer accords with the original built + purpose, the current use should be specified. For example, a building built as a + train station but later converted into a shopping mall would have the value + `"commercial"` rather than `"transportation"`. + """).strip() + ), + ] = None + class_: Annotated[ + BuildingClass | None, + Field( + alias="class", + description=textwrap.dedent(""" + A more specific classification of the current use and purpose of the building. + + If the current use of the building no longer accords with the original built + purpose, the current use should be specified. + """).strip(), + ), + ] = None + has_parts: Annotated[ + bool | None, + Field( + description="Whether the building has associated building part features", + strict=True, + ), + ] = None diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/__init__.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/__init__.py deleted file mode 100644 index 022661164..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .models import Building - -__all__ = [ - "Building", -] diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/enums.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/enums.py deleted file mode 100644 index 4e8fadd5c..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/enums.py +++ /dev/null @@ -1,115 +0,0 @@ -from enum import Enum - - -class Subtype(str, Enum): - """A broad category of the building type/purpose. - - When the current use of the building does not match the built purpose, the subtype - should be set to represent the current use of the building. - """ - - AGRICULTURAL = "agricultural" - CIVIC = "civic" - COMMERCIAL = "commercial" - EDUCATION = "education" - ENTERTAINMENT = "entertainment" - INDUSTRIAL = "industrial" - MEDICAL = "medical" - MILITARY = "military" - OUTBUILDING = "outbuilding" - RELIGIOUS = "religious" - RESIDENTIAL = "residential" - SERVICE = "service" - TRANSPORTATION = "transportation" - - -class BuildingClass(str, Enum): - """Further delineation of the building's built purpose.""" - - AGRICULTURAL = "agricultural" - ALLOTMENT_HOUSE = "allotment_house" - APARTMENTS = "apartments" - BARN = "barn" - BEACH_HUT = "beach_hut" - BOATHOUSE = "boathouse" - BRIDGE_STRUCTURE = "bridge_structure" - BUNGALOW = "bungalow" - BUNKER = "bunker" - CABIN = "cabin" - CARPORT = "carport" - CATHEDRAL = "cathedral" - CHAPEL = "chapel" - CHURCH = "church" - CIVIC = "civic" - COLLEGE = "college" - COMMERCIAL = "commercial" - COWSHED = "cowshed" - DETACHED = "detached" - DIGESTER = "digester" - DORMITORY = "dormitory" - DWELLING_HOUSE = "dwelling_house" - FACTORY = "factory" - FARM = "farm" - FARM_AUXILIARY = "farm_auxiliary" - FIRE_STATION = "fire_station" - GARAGE = "garage" - GARAGES = "garages" - GER = "ger" - GLASSHOUSE = "glasshouse" - GOVERNMENT = "government" - GRANDSTAND = "grandstand" - GREENHOUSE = "greenhouse" - GUARDHOUSE = "guardhouse" - HANGAR = "hangar" - HOSPITAL = "hospital" - HOTEL = "hotel" - HOUSE = "house" - HOUSEBOAT = "houseboat" - HUT = "hut" - INDUSTRIAL = "industrial" - KINDERGARTEN = "kindergarten" - KIOSK = "kiosk" - LIBRARY = "library" - MANUFACTURE = "manufacture" - MILITARY = "military" - MONASTERY = "monastery" - MOSQUE = "mosque" - OFFICE = "office" - OUTBUILDING = "outbuilding" - PARKING = "parking" - PAVILION = "pavilion" - POST_OFFICE = "post_office" - PRESBYTERY = "presbytery" - PUBLIC = "public" - RELIGIOUS = "religious" - RESIDENTIAL = "residential" - RETAIL = "retail" - ROOF = "roof" - SCHOOL = "school" - SEMI = "semi" - SEMIDETACHED_HOUSE = "semidetached_house" - SERVICE = "service" - SHED = "shed" - SHRINE = "shrine" - SILO = "silo" - SLURRY_TANK = "slurry_tank" - SPORTS_CENTRE = "sports_centre" - SPORTS_HALL = "sports_hall" - STABLE = "stable" - STADIUM = "stadium" - STATIC_CARAVAN = "static_caravan" - STILT_HOUSE = "stilt_house" - STORAGE_TANK = "storage_tank" - STY = "sty" - SUPERMARKET = "supermarket" - SYNAGOGUE = "synagogue" - TEMPLE = "temple" - TERRACE = "terrace" - TOILETS = "toilets" - TRAIN_STATION = "train_station" - TRANSFORMER_TOWER = "transformer_tower" - TRANSPORTATION = "transportation" - TRULLO = "trullo" - UNIVERSITY = "university" - WAREHOUSE = "warehouse" - WAYSIDE_SHRINE = "wayside_shrine" diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/models.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/models.py deleted file mode 100644 index 930183418..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building/models.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Building feature models for Overture Maps buildings theme.""" - -from typing import Annotated, Literal - -from pydantic import ConfigDict, Field - -from overture.schema.core import OvertureFeature -from overture.schema.core.models import Stacked -from overture.schema.core.names import Named -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) - -from ..models import Shape -from .enums import ( - BuildingClass, - Subtype, -) - - -class Building( - OvertureFeature[Literal["buildings"], Literal["building"]], Named, Stacked, Shape -): - """A building is a man-made structure with a roof that exists permanently in one - place. - - Buildings are compatible with GeoJSON Polygon features. - """ - - model_config = ConfigDict(title="building") - - # Core - geometry: Annotated[ - Geometry, - GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), - Field( - description="""The building's footprint or roofprint (if traced from aerial/satellite imagery).""", - ), - ] - - # Optional - - subtype: Subtype | None = None - class_: Annotated[BuildingClass | None, Field(alias="class")] = None - has_parts: Annotated[ - bool | None, - Field( - description="Flag indicating whether the building has parts", - strict=True, - ), - ] = None diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part.py new file mode 100644 index 000000000..980645101 --- /dev/null +++ b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part.py @@ -0,0 +1,56 @@ +""" +The `BuildingPart` feature type model and supporting types. +""" + +from typing import Annotated, Literal + +from pydantic import Field + +from overture.schema.buildings._common import Appearance +from overture.schema.buildings.building import Building +from overture.schema.core import OvertureFeature +from overture.schema.core.models import Stacked +from overture.schema.core.names import Named +from overture.schema.system.primitive import ( + Geometry, + GeometryType, + GeometryTypeConstraint, +) +from overture.schema.system.ref import Id, Reference, Relationship + + +class BuildingPart( + OvertureFeature[Literal["buildings"], Literal["building_part"]], + Named, + Stacked, + Appearance, +): + """ + Building parts represent parts of larger building features. They allow buildings to be modeled + in rich detail suitable for creating realistic 3D models. + + Every building part is associated with a parent `Building` feature via the `building_id` field. + In addition, a building part has a footprint geometry and may include additional details such as + its height, the number of floors, and the color and material of its facade and roof. + + Building parts can float or be stacked on top of each other. The `min_height`, `min_floor`, + `height`, and `num_floors`, fields can be used to arrange the parts correctly along the + vertical dimension. + """ + + # Core + geometry: Annotated[ + Geometry, + GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), + Field( + description="The footprint or roofprint of the building part.", + ), + ] + + # Required + + building_id: Annotated[ + Id, + Field(description="The building to which this part belongs"), + Reference(Relationship.BELONGS_TO, Building), + ] diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part/__init__.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part/__init__.py deleted file mode 100644 index 9a1571389..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .models import BuildingPart - -__all__ = ["BuildingPart"] diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part/models.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part/models.py deleted file mode 100644 index bb42d466f..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/building_part/models.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Building part feature models for Overture Maps buildings theme.""" - -from typing import Annotated, Literal - -from pydantic import Field - -from overture.schema.core import OvertureFeature -from overture.schema.core.models import Stacked -from overture.schema.core.names import Named -from overture.schema.system.primitive import ( - Geometry, - GeometryType, - GeometryTypeConstraint, -) -from overture.schema.system.ref import Id, Reference, Relationship - -from ..building.models import Building -from ..models import Shape - - -class BuildingPart( - OvertureFeature[Literal["buildings"], Literal["building_part"]], - Named, - Stacked, - Shape, -): - """A single building part. - - Parts describe their shape and color and other properties. Each building part must - refer to the building to which it belongs. - """ - - # Core - geometry: Annotated[ - Geometry, - GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON), - Field( - description="The part's geometry.", - ), - ] - - # Required - - building_id: Annotated[ - Id, - Field(description="The building ID to which this part belongs"), - Reference(Relationship.BELONGS_TO, Building), - ] diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/enums.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/enums.py deleted file mode 100644 index f6306572a..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/enums.py +++ /dev/null @@ -1,65 +0,0 @@ -from enum import Enum - - -class FacadeMaterial(str, Enum): - """The outer surface material of building facade.""" - - BRICK = "brick" - CEMENT_BLOCK = "cement_block" - CLAY = "clay" - CONCRETE = "concrete" - GLASS = "glass" - METAL = "metal" - PLASTER = "plaster" - PLASTIC = "plastic" - STONE = "stone" - TIMBER_FRAMING = "timber_framing" - WOOD = "wood" - - -class RoofMaterial(str, Enum): - """The outermost material of the roof.""" - - CONCRETE = "concrete" - COPPER = "copper" - ETERNIT = "eternit" - GLASS = "glass" - GRASS = "grass" - GRAVEL = "gravel" - METAL = "metal" - PLASTIC = "plastic" - ROOF_TILES = "roof_tiles" - SLATE = "slate" - SOLAR_PANELS = "solar_panels" - THATCH = "thatch" - TAR_PAPER = "tar_paper" - WOOD = "wood" - - -class RoofShape(str, Enum): - """The shape of the roof.""" - - DOME = "dome" - FLAT = "flat" - GABLED = "gabled" - GAMBREL = "gambrel" - HALF_HIPPED = "half_hipped" - HIPPED = "hipped" - MANSARD = "mansard" - ONION = "onion" - PYRAMIDAL = "pyramidal" - ROUND = "round" - SALTBOX = "saltbox" - SAWTOOTH = "sawtooth" - SKILLION = "skillion" - SPHERICAL = "spherical" - - -class RoofOrientation(str, Enum): - """Orientation of the roof shape relative to the footprint shape. - - Either "along" or "across". - """ - - ACROSS = "across" - ALONG = "along" diff --git a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/models.py b/packages/overture-schema-buildings-theme/src/overture/schema/buildings/models.py deleted file mode 100644 index 059d1179c..000000000 --- a/packages/overture-schema-buildings-theme/src/overture/schema/buildings/models.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import Annotated - -from pydantic import BaseModel, Field - -from overture.schema.buildings.enums import ( - FacadeMaterial, - RoofMaterial, - RoofOrientation, - RoofShape, -) -from overture.schema.system.primitive import float64, int32 -from overture.schema.system.string import HexColor - - -class Shape(BaseModel): - """Properties of the buildings shape, such as height or roof type.""" - - # Optional - - height: Annotated[ - float64 | None, - Field( - gt=0, - description="""Height of the building or part in meters. The height is the distance from the lowest point to the highest point.""", - ), - ] = None - is_underground: Annotated[ - bool | None, - Field( - description="""Whether the entire building or part is completely below ground. This is useful for rendering which typically omits these buildings or styles them differently because they are not visible above ground. This is different than the level column which is used to indicate z-ordering of elements and negative values may be above ground.""", - strict=True, - ), - ] = None - num_floors: Annotated[ - int32 | None, - Field( - gt=0, - description="Number of above-ground floors of the building or part.", - ), - ] = None - num_floors_underground: Annotated[ - int32 | None, - Field( - gt=0, - description="Number of below-ground floors of the building or part.", - ), - ] = None - min_height: Annotated[ - float64 | None, - Field( - description="The height of the bottom part of building in meters. Used if a building or part of building starts above the ground level.", - ), - ] = None - min_floor: Annotated[ - int32 | None, - Field( - gt=0, - description="""The "start" floor of this building or part. Indicates that the building or part is "floating" and its bottom-most floor is above ground level, usually because it is part of a larger building in which some parts do reach down to ground level. An example is a building that has an entry road or driveway at ground level into an interior courtyard, where part of the building bridges above the entry road. This property may sometimes be populated when min_height is missing and in these cases can be used as a proxy for min_height.""", - ), - ] = None - facade_color: Annotated[ - HexColor | None, - Field( - description="The color (name or color triplet) of the facade of a building or building part in hexadecimal", - ), - ] = None - facade_material: Annotated[ - FacadeMaterial | None, - Field(description="The outer surface material of building facade."), - ] = None - roof_material: Annotated[ - RoofMaterial | None, Field(description="The outermost material of the roof.") - ] = None - roof_shape: Annotated[ - RoofShape | None, Field(description="The shape of the roof") - ] = None - roof_direction: Annotated[ - float64 | None, - Field( - ge=0, - lt=360, - description="Bearing of the roof ridge line in degrees.", - ), - ] = None - roof_orientation: Annotated[ - RoofOrientation | None, - Field( - description="""Orientation of the roof shape relative to the footprint shape. Either "along" or "across".""", - ), - ] = None - roof_color: Annotated[ - HexColor | None, - Field( - description="The color (name or color triplet) of the roof of a building or building part in hexadecimal", - ), - ] = None - roof_height: Annotated[ - float64 | None, - Field( - description="""The height of the building roof in meters. This represents the distance from the base of the roof to the highest point of the roof.""", - ), - ] = None diff --git a/packages/overture-schema-buildings-theme/tests/building_baseline_schema.json b/packages/overture-schema-buildings-theme/tests/building_baseline_schema.json index c24f25ba3..d54ea0185 100644 --- a/packages/overture-schema-buildings-theme/tests/building_baseline_schema.json +++ b/packages/overture-schema-buildings-theme/tests/building_baseline_schema.json @@ -1,7 +1,7 @@ { "$defs": { "BuildingClass": { - "description": "Further delineation of the building's built purpose.", + "description": "Further classification of the type and purpose of a building.\n\nThe building class adds detail to the broad classification of `BuildingSubtype`.", "enum": [ "agricultural", "allotment_house", @@ -94,6 +94,26 @@ "title": "BuildingClass", "type": "string" }, + "BuildingSubtype": { + "description": "Broadest classification of the type and purpose of a building.\n\nThis broad classification can be refined by `BuildingClass`.", + "enum": [ + "agricultural", + "civic", + "commercial", + "education", + "entertainment", + "industrial", + "medical", + "military", + "outbuilding", + "religious", + "residential", + "service", + "transportation" + ], + "title": "BuildingSubtype", + "type": "string" + }, "FacadeMaterial": { "description": "The outer surface material of building facade.", "enum": [ @@ -114,7 +134,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -129,7 +149,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -143,14 +163,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -161,6 +182,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -264,15 +286,15 @@ "roof_tiles", "slate", "solar_panels", - "thatch", "tar_paper", + "thatch", "wood" ], "title": "RoofMaterial", "type": "string" }, "RoofOrientation": { - "description": "Orientation of the roof shape relative to the footprint shape.\n\nEither \"along\" or \"across\".", + "description": "Orientation of the roof shape relative to the footprint shape.\n\nThe members of this enumeration, `\"across\"` and `\"along\"`, are borrowed from the OpenStreetMap\n`roof:orientation=*` tag and have the same meanings as they do in OSM.", "enum": [ "across", "along" @@ -310,12 +332,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -327,34 +349,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -364,32 +387,12 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" - }, - "Subtype": { - "description": "A broad category of the building type/purpose.\n\nWhen the current use of the building does not match the built purpose, the subtype\nshould be set to represent the current use of the building.", - "enum": [ - "agricultural", - "civic", - "commercial", - "education", - "entertainment", - "industrial", - "medical", - "military", - "outbuilding", - "religious", - "residential", - "service", - "transportation" - ], - "title": "Subtype", - "type": "string" } }, "additionalProperties": false, - "description": "A building is a man-made structure with a roof that exists permanently in one\nplace.\n\nBuildings are compatible with GeoJSON Polygon features.", + "description": "Buildings are man-made structures with roofs that exists permanently in one place.\n\nA building's geometry represents the two-dimensional footprint of the building as viewed from\ndirectly above, looking down. Fields such as `height` and `num_floors` allow the\nthree-dimensional shape to be approximated. Some buildings, identified by the `has_parts` field,\nhave associated `BuildingPart` features which can be used to generate a more representative 3D\nmodel of the building.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -506,42 +509,45 @@ }, "properties": { "class": { - "$ref": "#/$defs/BuildingClass" + "$ref": "#/$defs/BuildingClass", + "description": "A more specific classification of the current use and purpose of the building.\n\nIf the current use of the building no longer accords with the original built\npurpose, the current use should be specified." }, "facade_color": { - "description": "The color (name or color triplet) of the facade of a building or building part in hexadecimal", + "description": "Facade color in `#rgb` or `#rrggbb` hex notation", "pattern": "^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$", "title": "Facade Color", "type": "string" }, "facade_material": { - "$ref": "#/$defs/FacadeMaterial" + "$ref": "#/$defs/FacadeMaterial", + "description": "Outer surface material of the facade" }, "has_parts": { - "description": "Flag indicating whether the building has parts", + "description": "Whether the building has associated building part features", "title": "Has Parts", "type": "boolean" }, "height": { - "description": "Height of the building or part in meters. The height is the distance from the lowest point to the highest point.", + "description": "Height of the building or part in meters.\n\nThis is the distance from the lowest point to the highest point.", "exclusiveMinimum": 0, "title": "Height", "type": "number" }, "is_underground": { - "description": "Whether the entire building or part is completely below ground. This is useful for rendering which typically omits these buildings or styles them differently because they are not visible above ground. This is different than the level column which is used to indicate z-ordering of elements and negative values may be above ground.", + "description": "Whether the entire building or part is completely below ground.\n\nThe underground flag is useful for display purposes. Buildings and building parts\nthat are entirely below ground can be styled differently or omitted from the\nrendered image.\n\nThis flag is conceptually different from the `level` field, which indicates\nrelative z-ordering and, notably, can be negative even if the building is entirely\nabove-ground.", "title": "Is Underground", "type": "boolean" }, "level": { + "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Level", "type": "integer" }, "min_floor": { - "description": "The \"start\" floor of this building or part. Indicates that the building or part is \"floating\" and its bottom-most floor is above ground level, usually because it is part of a larger building in which some parts do reach down to ground level. An example is a building that has an entry road or driveway at ground level into an interior courtyard, where part of the building bridges above the entry road. This property may sometimes be populated when min_height is missing and in these cases can be used as a proxy for min_height.", + "description": "Start floor of this building or part.\n\nIf present, this value indicates that the building or part is \"floating\" and its\nbottom-most floor is above ground level, usually because it is part of a larger\nbuilding in which some parts do reach down to ground level. An example is a building\nthat has an entry road or driveway at ground level into an interior courtyard, where\npart of the building bridges above the entry road. This property may sometimes be\npopulated when `min_height` is missing and in these cases can be used as a proxy for\n`min_height`.", "exclusiveMinimum": 0, "maximum": 2147483647, "minimum": -2147483648, @@ -549,7 +555,7 @@ "type": "integer" }, "min_height": { - "description": "The height of the bottom part of building in meters. Used if a building or part of building starts above the ground level.", + "description": "Altitude above ground where the bottom of the building or building part starts.\n\nIf present, this value indicates that the lowest part of the building or building\npart starts is above ground level.", "title": "Min Height", "type": "number" }, @@ -573,38 +579,39 @@ "type": "integer" }, "roof_color": { - "description": "The color (name or color triplet) of the roof of a building or building part in hexadecimal", + "description": "The roof color in `#rgb` or `#rrggbb` hex notation", "pattern": "^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$", "title": "Roof Color", "type": "string" }, "roof_direction": { - "description": "Bearing of the roof ridge line in degrees.", + "description": "Bearing of the roof ridge line in degrees", "exclusiveMaximum": 360, "minimum": 0, "title": "Roof Direction", "type": "number" }, "roof_height": { - "description": "The height of the building roof in meters. This represents the distance from the base of the roof to the highest point of the roof.", + "description": "Height of the roof in meters.\n\nThis is the distance from the base of the roof to its highest point.", "title": "Roof Height", "type": "number" }, "roof_material": { - "$ref": "#/$defs/RoofMaterial" + "$ref": "#/$defs/RoofMaterial", + "description": "Outer surface material of the roof" }, "roof_orientation": { "$ref": "#/$defs/RoofOrientation", - "description": "Orientation of the roof shape relative to the footprint shape. Either \"along\" or \"across\"." + "description": "Orientation of the roof shape relative to the footprint shape" }, "roof_shape": { "$ref": "#/$defs/RoofShape", - "description": "The shape of the roof" + "description": "Shape of the roof" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", @@ -612,7 +619,8 @@ "uniqueItems": true }, "subtype": { - "$ref": "#/$defs/Subtype" + "$ref": "#/$defs/BuildingSubtype", + "description": "A broad classification of the current use and purpose of the building.\n\nIf the current use of the building no longer accords with the original built\npurpose, the current use should be specified. For example, a building built as a\ntrain station but later converted into a shopping mall would have the value\n`\"commercial\"` rather than `\"transportation\"`." }, "theme": { "const": "buildings", diff --git a/packages/overture-schema-buildings-theme/tests/building_part_baseline_schema.json b/packages/overture-schema-buildings-theme/tests/building_part_baseline_schema.json index efb42c862..1962ff3c6 100644 --- a/packages/overture-schema-buildings-theme/tests/building_part_baseline_schema.json +++ b/packages/overture-schema-buildings-theme/tests/building_part_baseline_schema.json @@ -20,7 +20,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -35,7 +35,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -49,14 +49,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -67,6 +68,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -170,15 +172,15 @@ "roof_tiles", "slate", "solar_panels", - "thatch", "tar_paper", + "thatch", "wood" ], "title": "RoofMaterial", "type": "string" }, "RoofOrientation": { - "description": "Orientation of the roof shape relative to the footprint shape.\n\nEither \"along\" or \"across\".", + "description": "Orientation of the roof shape relative to the footprint shape.\n\nThe members of this enumeration, `\"across\"` and `\"along\"`, are borrowed from the OpenStreetMap\n`roof:orientation=*` tag and have the same meanings as they do in OSM.", "enum": [ "across", "along" @@ -216,12 +218,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -233,34 +235,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -270,12 +273,12 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, "additionalProperties": false, - "description": "A single building part.\n\nParts describe their shape and color and other properties. Each building part must\nrefer to the building to which it belongs.", + "description": "Building parts represent parts of larger building features. They allow buildings to be modeled\nin rich detail suitable for creating realistic 3D models.\n\nEvery building part is associated with a parent `Building` feature via the `building_id` field.\nIn addition, a building part has a footprint geometry and may include additional details such as\nits height, the number of floors, and the color and material of its facade and roof.\n\nBuilding parts can float or be stacked on top of each other. The `min_height`, `min_floor`,\n`height`, and `num_floors`, fields can be used to arrange the parts correctly along the\nvertical dimension.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -288,7 +291,7 @@ "type": "array" }, "geometry": { - "description": "The part's geometry.", + "description": "The footprint or roofprint of the building part.", "oneOf": [ { "properties": { @@ -392,41 +395,43 @@ }, "properties": { "building_id": { - "description": "The building ID to which this part belongs", + "description": "The building to which this part belongs", "minLength": 1, "pattern": "^\\S+$", "title": "Building Id", "type": "string" }, "facade_color": { - "description": "The color (name or color triplet) of the facade of a building or building part in hexadecimal", + "description": "Facade color in `#rgb` or `#rrggbb` hex notation", "pattern": "^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$", "title": "Facade Color", "type": "string" }, "facade_material": { - "$ref": "#/$defs/FacadeMaterial" + "$ref": "#/$defs/FacadeMaterial", + "description": "Outer surface material of the facade" }, "height": { - "description": "Height of the building or part in meters. The height is the distance from the lowest point to the highest point.", + "description": "Height of the building or part in meters.\n\nThis is the distance from the lowest point to the highest point.", "exclusiveMinimum": 0, "title": "Height", "type": "number" }, "is_underground": { - "description": "Whether the entire building or part is completely below ground. This is useful for rendering which typically omits these buildings or styles them differently because they are not visible above ground. This is different than the level column which is used to indicate z-ordering of elements and negative values may be above ground.", + "description": "Whether the entire building or part is completely below ground.\n\nThe underground flag is useful for display purposes. Buildings and building parts\nthat are entirely below ground can be styled differently or omitted from the\nrendered image.\n\nThis flag is conceptually different from the `level` field, which indicates\nrelative z-ordering and, notably, can be negative even if the building is entirely\nabove-ground.", "title": "Is Underground", "type": "boolean" }, "level": { + "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Level", "type": "integer" }, "min_floor": { - "description": "The \"start\" floor of this building or part. Indicates that the building or part is \"floating\" and its bottom-most floor is above ground level, usually because it is part of a larger building in which some parts do reach down to ground level. An example is a building that has an entry road or driveway at ground level into an interior courtyard, where part of the building bridges above the entry road. This property may sometimes be populated when min_height is missing and in these cases can be used as a proxy for min_height.", + "description": "Start floor of this building or part.\n\nIf present, this value indicates that the building or part is \"floating\" and its\nbottom-most floor is above ground level, usually because it is part of a larger\nbuilding in which some parts do reach down to ground level. An example is a building\nthat has an entry road or driveway at ground level into an interior courtyard, where\npart of the building bridges above the entry road. This property may sometimes be\npopulated when `min_height` is missing and in these cases can be used as a proxy for\n`min_height`.", "exclusiveMinimum": 0, "maximum": 2147483647, "minimum": -2147483648, @@ -434,7 +439,7 @@ "type": "integer" }, "min_height": { - "description": "The height of the bottom part of building in meters. Used if a building or part of building starts above the ground level.", + "description": "Altitude above ground where the bottom of the building or building part starts.\n\nIf present, this value indicates that the lowest part of the building or building\npart starts is above ground level.", "title": "Min Height", "type": "number" }, @@ -458,38 +463,39 @@ "type": "integer" }, "roof_color": { - "description": "The color (name or color triplet) of the roof of a building or building part in hexadecimal", + "description": "The roof color in `#rgb` or `#rrggbb` hex notation", "pattern": "^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$", "title": "Roof Color", "type": "string" }, "roof_direction": { - "description": "Bearing of the roof ridge line in degrees.", + "description": "Bearing of the roof ridge line in degrees", "exclusiveMaximum": 360, "minimum": 0, "title": "Roof Direction", "type": "number" }, "roof_height": { - "description": "The height of the building roof in meters. This represents the distance from the base of the roof to the highest point of the roof.", + "description": "Height of the roof in meters.\n\nThis is the distance from the base of the roof to its highest point.", "title": "Roof Height", "type": "number" }, "roof_material": { - "$ref": "#/$defs/RoofMaterial" + "$ref": "#/$defs/RoofMaterial", + "description": "Outer surface material of the roof" }, "roof_orientation": { "$ref": "#/$defs/RoofOrientation", - "description": "Orientation of the roof shape relative to the footprint shape. Either \"along\" or \"across\"." + "description": "Orientation of the roof shape relative to the footprint shape" }, "roof_shape": { "$ref": "#/$defs/RoofShape", - "description": "The shape of the roof" + "description": "Shape of the roof" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-buildings-theme/tests/test_building_json_schema_baseline.py b/packages/overture-schema-buildings-theme/tests/test_building_json_schema_baseline.py index 8ba87cf3b..917cc9b15 100644 --- a/packages/overture-schema-buildings-theme/tests/test_building_json_schema_baseline.py +++ b/packages/overture-schema-buildings-theme/tests/test_building_json_schema_baseline.py @@ -3,8 +3,8 @@ import json import os -from overture.schema.buildings import Building -from overture.schema.core import json_schema +from overture.schema.buildings.building import Building +from overture.schema.system.json_schema import json_schema def test_building_json_schema_baseline() -> None: diff --git a/packages/overture-schema-buildings-theme/tests/test_building_part_json_schema_baseline.py b/packages/overture-schema-buildings-theme/tests/test_building_part_json_schema_baseline.py index 924e37a5a..9150966be 100644 --- a/packages/overture-schema-buildings-theme/tests/test_building_part_json_schema_baseline.py +++ b/packages/overture-schema-buildings-theme/tests/test_building_part_json_schema_baseline.py @@ -3,8 +3,8 @@ import json import os -from overture.schema.buildings import BuildingPart -from overture.schema.core import json_schema +from overture.schema.buildings.building_part import BuildingPart +from overture.schema.system.json_schema import json_schema def test_building_part_json_schema_baseline() -> None: diff --git a/packages/overture-schema-core/src/overture/schema/core/__init__.py b/packages/overture-schema-core/src/overture/schema/core/__init__.py index 69096a294..735c794c7 100644 --- a/packages/overture-schema-core/src/overture/schema/core/__init__.py +++ b/packages/overture-schema-core/src/overture/schema/core/__init__.py @@ -1,14 +1,15 @@ -from . import scoping -from .json_schema import json_schema -from .models import OvertureFeature -from .parser import parse_feature +from . import cartography, names, scoping, sources +from .models import OvertureFeature, ThemeT, TypeT from .scoping import Scope, scoped __all__ = [ - "json_schema", + "cartography", + "names", "OvertureFeature", - "parse_feature", "Scope", "scoped", "scoping", + "sources", + "ThemeT", + "TypeT", ] diff --git a/packages/overture-schema-core/src/overture/schema/core/_cache.py b/packages/overture-schema-core/src/overture/schema/core/_cache.py deleted file mode 100644 index 06e0f7768..000000000 --- a/packages/overture-schema-core/src/overture/schema/core/_cache.py +++ /dev/null @@ -1,20 +0,0 @@ -from types import UnionType - -from pydantic import BaseModel, TypeAdapter - -# Shared cache for TypeAdapter instances to avoid recreating them -_TYPE_ADAPTER_CACHE: dict[type[BaseModel] | UnionType | type, TypeAdapter] = {} - - -def get_type_adapter(model_type: type[BaseModel] | UnionType | type) -> TypeAdapter: - """Get a cached TypeAdapter instance for the given model type. - - Args: - model_type: The type to create/retrieve a TypeAdapter for - - Returns: - TypeAdapter instance for the given type - """ - if model_type not in _TYPE_ADAPTER_CACHE: - _TYPE_ADAPTER_CACHE[model_type] = TypeAdapter(model_type) - return _TYPE_ADAPTER_CACHE[model_type] diff --git a/packages/overture-schema-core/src/overture/schema/core/cartography.py b/packages/overture-schema-core/src/overture/schema/core/cartography.py new file mode 100644 index 000000000..bdf009eb6 --- /dev/null +++ b/packages/overture-schema-core/src/overture/schema/core/cartography.py @@ -0,0 +1,109 @@ +""" +Specify cartographic hints for features and fields of features. +""" + +import textwrap +from typing import Annotated, NewType + +from pydantic import BaseModel, Field + +from overture.schema.system.model_constraint import no_extra_fields +from overture.schema.system.primitive import uint8 + +Prominence = NewType( + "Prominence", + Annotated[ + uint8, + Field( + ge=1, + le=100, + description=textwrap.dedent(""" + Subjective scale of feature significance or importance, with 1 being the least, and + 100 being the most, significant. + + This value can be used to help drive decisions about how and when to display a + feature, and how to treat it relative to neighboring features. + + When populated by Overture, this value is derived from various factors including, + but not limited to: feature and subtype, population, and capital status. + """).strip(), + ), + ], +) + +MinZoom = NewType( + "MinZoom", + Annotated[ + uint8, + Field( + ge=0, + le=23, + description=textwrap.dedent(""" + Recommended minimum tile zoom level in which this feature should be displayed. + + It is recommended that the feature be hidden at zoom levels below this value. + + Zoom levels follow the Slippy Maps convention, documented in the following + references: + - https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames + - https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection + """).strip(), + ), + ], +) + +MaxZoom = NewType( + "MaxZoom", + Annotated[ + uint8, + Field( + ge=0, + le=23, + description=textwrap.dedent(""" + Recommended maximum tile zoom level in which this feature should be displayed. + + It is recommended that the feature be hidden at zoom levels above this value. + + Zoom levels follow the Slippy Maps convention, documented in the following + references: + - https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames + - https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection + """).strip(), + ), + ], +) + +SortKey = NewType( + "SortKey", + Annotated[ + uint8, + Field( + description=textwrap.dedent(""" + Integer indicating the recommended order in which to draw features. + + Features with a lower number should be drawn "in front" of features with a higher + number. + """).strip(), + ), + ], +) + + +@no_extra_fields +class CartographicHints(BaseModel): + """Cartographic hints for optimal use of Overture features in map-making.""" + + # Optional + + prominence: Prominence | None = None + min_zoom: MinZoom | None = None + max_zoom: MaxZoom | None = None + sort_key: SortKey | None = None + + +class CartographicallyHinted(BaseModel): + """ + Properties for adding cartographic hints to a model. + """ + + cartography: Annotated[CartographicHints | None, Field(title="cartography")] = None diff --git a/packages/overture-schema-core/src/overture/schema/core/discovery.py b/packages/overture-schema-core/src/overture/schema/core/discovery.py index 729fafaeb..3719e76a5 100644 --- a/packages/overture-schema-core/src/overture/schema/core/discovery.py +++ b/packages/overture-schema-core/src/overture/schema/core/discovery.py @@ -6,7 +6,6 @@ def discover_models() -> dict[tuple[str, str], type[BaseModel]]: """Discover all registered Overture models via entry points.""" - models = {} try: for entry_point in importlib.metadata.entry_points(group="overture.models"): @@ -30,7 +29,6 @@ def get_registered_model(theme: str, feature_type: str) -> type[BaseModel] | Non This uses setuptools entry points for registration. """ - entry_point_name = f"{theme}.{feature_type}" try: for entry_point in importlib.metadata.entry_points(group="overture.models"): diff --git a/packages/overture-schema-core/src/overture/schema/core/json_schema.py b/packages/overture-schema-core/src/overture/schema/core/json_schema.py deleted file mode 100644 index 9ffd2b33e..000000000 --- a/packages/overture-schema-core/src/overture/schema/core/json_schema.py +++ /dev/null @@ -1,80 +0,0 @@ -from types import UnionType -from typing import Any, get_origin - -from pydantic import BaseModel -from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue -from pydantic_core import core_schema - -from ._cache import get_type_adapter - - -# TODO: Vic - I think we can remove this once `Omitable[T]` is applied everywhere (and once the -# @model_constraints are made `Omitable`-aware). -class EnhancedJsonSchemaGenerator(GenerateJsonSchema): - """Enhanced JSON Schema generator with optional field support. - - This generator enhances the default Pydantic generator with the following: - - - Optional field handling: simplifies nullable fields by removing null from anyOf - and removing null defaults to make fields truly optional. - """ - - def nullable_schema(self, schema: core_schema.NullableSchema) -> JsonSchemaValue: - """Generates a JSON schema that matches a nullable schema. - - Args: - schema: The core schema. - - Returns: - The generated JSON schema. - """ - # Generate the default nullable schema first - json_schema = super().nullable_schema(schema) - - # Remove null from anyOf to make field truly optional - if "anyOf" in json_schema: - json_schema["anyOf"] = [ - x for x in json_schema["anyOf"] if x.get("type") != "null" - ] - - if len(json_schema["anyOf"]) == 1: - json_schema = json_schema["anyOf"][0] - - # Remove null defaults to make fields truly optional - if json_schema.get("default") is None: - json_schema.pop("default", None) - - return json_schema - - def model_field_schema(self, schema: core_schema.ModelField) -> JsonSchemaValue: - """Override model field schema generation to remove null defaults.""" - json_schema = super().model_field_schema(schema) - - # Remove null defaults to make fields truly optional - if json_schema.get("default") is None: - json_schema.pop("default", None) - - return json_schema - - -def json_schema(models: type[BaseModel] | UnionType | type) -> dict[str, Any]: - """Generate JSON schema for a Pydantic model or union of models. - - Args: - models: Either a Pydantic BaseModel class or a union type (possibly - annotated with discriminator information) of BaseModels. - - Returns: - dict: JSON schema representation of the model(s). - - Raises: - TypeError: If models is not a BaseModel or union type. - """ - if isinstance(models, type) and issubclass(models, BaseModel): - return models.model_json_schema(schema_generator=EnhancedJsonSchemaGenerator) - - if get_origin(models) is not None: - adapter = get_type_adapter(models) - return adapter.json_schema(schema_generator=EnhancedJsonSchemaGenerator) - - raise TypeError(f"Expected BaseModel or union type, got {type(models)}") diff --git a/packages/overture-schema-core/src/overture/schema/core/models.py b/packages/overture-schema-core/src/overture/schema/core/models.py index ee999b3a2..6460d26ba 100644 --- a/packages/overture-schema-core/src/overture/schema/core/models.py +++ b/packages/overture-schema-core/src/overture/schema/core/models.py @@ -1,5 +1,5 @@ import textwrap -from typing import Annotated, Generic, NewType, TypeVar +from typing import Annotated, Generic, TypeVar from pydantic import ( BaseModel, @@ -12,6 +12,7 @@ from pydantic_core import core_schema from typing_extensions import Self +from overture.schema.core.sources import Sources from overture.schema.system.feature import Feature from overture.schema.system.field_constraint import UniqueItemsConstraint from overture.schema.system.model_constraint import no_extra_fields @@ -21,94 +22,12 @@ from overture.schema.system.ref import Id, Identified from overture.schema.system.string import ( CountryCodeAlpha2, - JsonPointer, - StrippedString, ) from .enums import PerspectiveMode -from .scoping.lr import LinearlyReferencedRange -from .scoping.side import Side from .types import ( - ConfidenceScore, - FeatureUpdateTime, FeatureVersion, Level, - MaxZoom, - MinZoom, - Prominence, - SortKey, -) - - -@no_extra_fields -class GeometricRangeScope(BaseModel): - """Geometric scoping properties defining the range of positions on the segment where - something is physically located or where a rule is active.""" - - model_config = ConfigDict(frozen=True) - - # Optional - - between: LinearlyReferencedRange | None = None - - def __hash__(self) -> int: - """Make GeometricRangeScope hashable.""" - return hash((tuple(self.between) if self.between is not None else None,)) - - -@no_extra_fields -class SideScope(BaseModel): - """Geometric scoping properties defining the side of a road modeled when moving - along the line from beginning to end.""" - - # Optional - - side: Side | None = None - - -@no_extra_fields -class SourcePropertyItem(GeometricRangeScope): - """An object storing the source for a specified property. - - The property is a reference to the property element within this Feature, and will be - referenced using JSON Pointer Notation RFC 6901 ( - https://datatracker.ietf.org/doc/rfc6901/). - The source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization. - """ - - # Required - - property: JsonPointer - dataset: str - - # Optional - - license: Annotated[ - StrippedString | None, - Field( - description="License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", - ), - ] = None - record_id: Annotated[ - str | None, - Field( - description="Refers to the specific record within the dataset that was used.", - ), - ] = None - update_time: FeatureUpdateTime | None = None - confidence: ConfidenceScore | None = None - - -Sources = NewType( - "Sources", - Annotated[ - list[SourcePropertyItem], - Field( - min_length=1, - description="""The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.""", - ), - UniqueItemsConstraint(), - ], ) ThemeT = TypeVar("ThemeT", bound=str) @@ -127,7 +46,7 @@ class OvertureFeature(Identified, Feature, Generic[ThemeT, TypeT]): description="A feature ID. This may be an ID associated with the Global Entity Reference System (GERS) if—and-only-if the feature represents an entity that is part of GERS." ) # type: ignore[assignment] theme: ThemeT - # this is an enum in the JSON Schema, but that prevents Feature from being extended + # this is an enum in the JSON Schema, but that prevents OvertureFeature from being extended type: TypeT geometry: Geometry version: FeatureVersion @@ -137,7 +56,7 @@ class OvertureFeature(Identified, Feature, Generic[ThemeT, TypeT]): sources: Sources | None = None @model_validator(mode="after") - def validate_model(self) -> Self: + def __validate_ext_fields__(self) -> Self: extra = self.model_extra invalid_extra_fields = ( [f for f in extra.keys() if not f.startswith("ext_")] if extra else () @@ -204,20 +123,4 @@ class Perspectives(BaseModel): class Stacked(BaseModel): """Properties defining feature Z-order, i.e., stacking order.""" - level: Level | None = None - - -@no_extra_fields -class CartographicHints(BaseModel): - """Defines cartographic hints for optimal use of Overture features in map-making.""" - - # Optional - - prominence: Prominence | None = None - min_zoom: MinZoom | None = None - max_zoom: MaxZoom | None = None - sort_key: SortKey | None = None - - -class CartographicallyHinted(BaseModel): - cartography: Annotated[CartographicHints | None, Field(title="cartography")] = None + level: Level | None = 0 # type: ignore[assignment] diff --git a/packages/overture-schema-core/src/overture/schema/core/names.py b/packages/overture-schema-core/src/overture/schema/core/names.py index 7d454319a..e968b24a7 100644 --- a/packages/overture-schema-core/src/overture/schema/core/names.py +++ b/packages/overture-schema-core/src/overture/schema/core/names.py @@ -1,10 +1,83 @@ -from enum import Enum +""" +Names for features and their child attributes. + +This module includes all Overture's standard naming types. It supports multi-language names, +multiple name variants, and naming rules for specifying conditional or partial names to things. + +Examples +-------- +Create a feature type that can have a name: + +>>> from typing import Literal +>>> from overture.schema.core import OvertureFeature +>>> from overture.schema.system.primitive import Geometry +>>> class MyFeature(OvertureFeature[Literal["mytheme"], Literal["mytype"]], Named): +... pass +... +>>> my_feature = MyFeature( +... id='12345678-1234-5678-9abc-123456789012', +... geometry=Geometry.from_wkt('POINT(0 0)'), +... theme='mytheme', +... type='mytype', +... version=1, +... names=Names(primary='my feature primary name') +... ) + +Create an arbitrary Pydantic model that can have a name: + +>>> from pydantic import BaseModel +>>> from overture.schema.system.model_constraint import no_extra_fields +>>> @no_extra_fields +... class MyModel(Named): +... myfield: int +... +>>> MyModel(names=Names(primary='foo'), myfield=42) +MyModel(names=Names(primary='foo', common=None, rules=None), myfield=42) + +Create a simple names structure with names in multiple languages: + +>>> names = Names( +... primary='Le Léman', +... common={ +... 'de': 'Genfersee', +... 'en': 'Lake Geneva', +... 'fr': 'Le Léman', +... } +... ) + +Create a name structure with official, alternate, and short names: + +>>> names = Names( +... primary='City of New York', +... rules=[ +... NameRule(value='New York', variant='official', language='en'), +... NameRule(value='New York City', variant='alternate', language='en'), +... NameRule(value='The Big Apple', variant='alternate', language='en'), +... NameRule(value='NYC', variant='alternate'), +... ] +... ) + +Create a name structure for a street where the name changes based on the side of the street. + +>>> from overture.schema.core.scoping import Side +>>> names = Names( +... primary='Fir St', +... rules=[ +... NameRule(value='2 Ave', variant='common', between=[0, 0.3], side=Side.LEFT), +... NameRule(value='Fir St', variant='common', between=[0.3, 1], side=Side.LEFT), +... NameRule(value='Fir St', variant='common', side=Side.RIGHT), +... ] +... ) +""" + +import textwrap from typing import Annotated, NewType from pydantic import BaseModel, Field from overture.schema.core.models import Perspectives from overture.schema.core.scoping import Scope, scoped +from overture.schema.system.doc import DocumentedEnum from overture.schema.system.model_constraint import no_extra_fields from overture.schema.system.string import ( LanguageTag, @@ -18,9 +91,18 @@ Annotated[ LanguageTag, Field( - description="""Each entry consists of a key that is an IETF-BCP47 language tag; and a value that reflects the common name in the language represented by the key's language tag. - -The validating regular expression for this property follows the pattern described in https://www.rfc-editor.org/rfc/bcp/bcp47.txt with the exception that private use tags are not supported.""" + description=textwrap.dedent(""" + A mapping from language to the most commonly used or recognized name in that + language. + + Each entry consists of a key that is an IETF BCP 47 language tag; and a + value that reflects the common name in the language represented by the key's + language tag. + + The validating regular expression for this property follows the pattern + described in https://www.rfc-editor.org/rfc/bcp/bcp47.txt with the exception + that private use tags are not supported. + """).strip(), ), ], StrippedString, @@ -28,28 +110,85 @@ Field(json_schema_extra={"additionalProperties": False}), ], ) - - -class NameVariant(str, Enum): - COMMON = "common" - OFFICIAL = "official" - ALTERNATE = "alternate" - SHORT = "short" +"""A mapping from language to the most commonly used or recognized name in that language.""" + + +class NameVariant(str, DocumentedEnum): + """ + Name variant used in a `NameRule`. + """ + + COMMON = ( + "common", + textwrap.dedent(""" + The most commonly used or recognized name for a feature in the specified language. + + In a `Names` value, most common names will appear in the `Names.common` field and will + not need to be specified as `NameRule` values in `Names.rules`. This member of the + enumeration should only be used to construct a `NameRule` if the common name needs to + be scoped in some way and therefore cannot be accurately represented in `CommonNames`. + """).strip(), + ) + OFFICIAL = ( + "official", + textwrap.dedent(""" + The legally or administratively recognized name, often used by government agencies or + official documents. + """).strip(), + ) + ALTERNATE = ( + "alternate", + textwrap.dedent(""" + An alternative name, which may be a historical name, a local colloquial name, or some + other well-known name is not the common name. + """).strip(), + ) + SHORT = ( + "short", + textwrap.dedent(""" + An abbreviated or shortened version of the name, which may be an acronym or some other + commonly-used short form. An example is "NYC" for New York City. + """).strip(), + ) @no_extra_fields @scoped(Scope.GEOMETRIC_RANGE, Scope.SIDE) class NameRule(BaseModel): - """Name rule with variant and language specification.""" + """ + A rule that can be evaluated to determine the name in advanced scenarios. + + Name rules are used for cases where the primary name is not sufficient; the common name is not + the right fit for the use case and another variant is needed; or where the name only applies in + certain specific circumstances. + + Examples might include: + - An official, alternate, or short name. + - A name that only applies to part of a linear path like a road segment (geometric range + scoping). + - A name that only applies to the left or right side of a linear path like a road segment (side + scoping). + - A name that is only accepted by some political perspectives. + """ # Required - value: Annotated[StrippedString, Field(min_length=1)] - variant: NameVariant + value: Annotated[ + StrippedString, Field(description="The actual name value.", min_length=1) + ] + variant: NameVariant = Field(description="The name variant for this name rule.") # Optional - language: LanguageTag | None = None + language: Annotated[ + LanguageTag | None, + Field( + description=textwrap.dedent(""" + The language in which the name `value` is specified, if known, as an IETF BCP 47 + language tag. + """).strip() + ), + ] = None perspectives: ( Annotated[ Perspectives, @@ -83,6 +222,6 @@ class Names(BaseModel): class Named(BaseModel): - """Properties defining the names of a feature.""" + """Properties defining the names of a model.""" names: Names | None = None diff --git a/packages/overture-schema-core/src/overture/schema/core/parser.py b/packages/overture-schema-core/src/overture/schema/core/parser.py deleted file mode 100644 index 6fa831089..000000000 --- a/packages/overture-schema-core/src/overture/schema/core/parser.py +++ /dev/null @@ -1,53 +0,0 @@ -from types import UnionType -from typing import Any, cast - -from pydantic import BaseModel - -from ._cache import get_type_adapter - - -def parse_feature( - feature: dict[str, Any], - model_type: type[BaseModel] | UnionType | type, - mode: str = "json", -) -> dict[str, Any] | None: - """Parse and validate a feature using the provided model type. - - Args: - feature: Feature data (GeoJSON or flattened format) - model_type: Pydantic model type or union type to validate against - mode: Output mode - "json" for GeoJSON format, "python" for flattened format - - Returns: - Parsed feature in the specified format - - Supports both GeoJSON format (with nested properties) and flattened format. - """ - - try: - # Basic structure validation - if not isinstance(feature, dict): - raise ValueError("Feature must be an object") - - # Detect format and normalize to flattened structure - if "properties" in feature and feature.get("type") == "Feature": - # GeoJSON format - flatten it - flattened_feature = { - "id": feature["id"], - "geometry": feature["geometry"], - **feature["properties"], # Flatten properties into top level - } - else: - # Already flattened format - flattened_feature = feature.copy() - - adapter = get_type_adapter(model_type) - parsed_model = adapter.validate_python(flattened_feature) - - # Return using the requested mode - return cast( - dict[str, Any], - parsed_model.model_dump(exclude_unset=True, mode=mode, by_alias=True), - ) - except Exception as e: - raise ValueError(str(e)) from e diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/heading.py b/packages/overture-schema-core/src/overture/schema/core/scoping/heading.py index 01d3966b0..07de6647a 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/heading.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/heading.py @@ -1,3 +1,7 @@ +""" +Types supporting the heading scope. +""" + from enum import Enum diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/lr.py b/packages/overture-schema-core/src/overture/schema/core/scoping/lr.py index 6c548bdaa..d1bc03b00 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/lr.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/lr.py @@ -1,3 +1,7 @@ +""" +Linear referencing types. +""" + from typing import Annotated, Any, NewType from pydantic import Field, GetJsonSchemaHandler, ValidationError, ValidationInfo @@ -6,9 +10,6 @@ from overture.schema.system.field_constraint import CollectionConstraint from overture.schema.system.primitive import float64 -# One possible advantage to using percentages over absolute distances is being able to -# trivially validate that the position lies "on" its segment (i.e. is between zero and -# one). Of course, this level of validity doesn't mean the number isn't nonsense LinearlyReferencedPosition = NewType( "LinearlyReferencedPosition", Annotated[ diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/opening_hours.py b/packages/overture-schema-core/src/overture/schema/core/scoping/opening_hours.py index 87d569f54..a8efc8c16 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/opening_hours.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/opening_hours.py @@ -1,7 +1,7 @@ -# Validating the opening hours value is going to have to happen outside of JSON Schema. -# -# Reasons for using the OSM opening hours specification for transportation rule time -# restrictions are documented in https://github.com/OvertureMaps/schema-wg/pull/10 +""" +OpenStreetMap opening hours type. +""" + from typing import Annotated, NewType from pydantic import Field @@ -15,3 +15,7 @@ ), ], ) +""" +Time span or time spans during which something is open or active, specified in the OpenStreetMap +opening hours specification: https://wiki.openstreetmap.org/wiki/Key:opening_hours/specification. +""" diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/purpose_of_use.py b/packages/overture-schema-core/src/overture/schema/core/scoping/purpose_of_use.py index 3419f7e76..9a33ae802 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/purpose_of_use.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/purpose_of_use.py @@ -1,9 +1,15 @@ +""" +Types supporting the purpose of use scope. +""" + from enum import Enum class PurposeOfUse(str, Enum): - """Reason why a person or entity travelling on the transportation network is using a - particular location.""" + """ + Reason why a person or entity travelling on the transportation network is using a particular + location. + """ AS_CUSTOMER = "as_customer" AT_DESTINATION = "at_destination" diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/recognized_status.py b/packages/overture-schema-core/src/overture/schema/core/scoping/recognized_status.py index 12894be07..e88f25da0 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/recognized_status.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/recognized_status.py @@ -1,9 +1,15 @@ +""" +Types supporting the recognized status scope. +""" + from enum import Enum class RecognizedStatus(str, Enum): - """Status of the person or entity travelling as recognized by authorities - controlling the particular location.""" + """ + Status of the person or entity travelling as recognized by authorities controlling the particular + location. + """ AS_PERMITTED = "as_permitted" AS_PRIVATE = "as_private" diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/scoped.py b/packages/overture-schema-core/src/overture/schema/core/scoping/scoped.py index 6ef935f5f..c95795325 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/scoped.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/scoped.py @@ -1,3 +1,5 @@ +"""Provides the `@scoped` decorator and `Scope` enumeration.""" + from collections.abc import ( Callable, Iterable, @@ -35,9 +37,7 @@ class Scope(str, Enum): - """ - A scope type supported by the `scoped` decorator. - """ + """A scope type supported by the `scoped` decorator.""" GEOMETRIC_POSITION = "geometric_position" """ @@ -307,7 +307,7 @@ def scoped( required: Scope | Iterable[Scope] | None = None, ) -> Callable: """ - Returns a decorator to decorate a Pydantic model class with one or more scoping attributes. + Return a decorator to decorate a Pydantic model class with one or more scoping attributes. At least one `Scope` must be given either in `optional` or `required` or both. A scope may be optional or required, but not both. diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/side.py b/packages/overture-schema-core/src/overture/schema/core/scoping/side.py index eb218e8a9..6a360b6b6 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/side.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/side.py @@ -1,3 +1,7 @@ +""" +Types supporting the side scope. +""" + from overture.schema.system.doc import DocumentedEnum diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/travel_mode.py b/packages/overture-schema-core/src/overture/schema/core/scoping/travel_mode.py index 6de69e23e..20c068506 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/travel_mode.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/travel_mode.py @@ -1,3 +1,7 @@ +""" +Types supporting the trravel mode scope. +""" + from enum import Enum diff --git a/packages/overture-schema-core/src/overture/schema/core/scoping/vehicle.py b/packages/overture-schema-core/src/overture/schema/core/scoping/vehicle.py index f5746808b..d21fe24d6 100644 --- a/packages/overture-schema-core/src/overture/schema/core/scoping/vehicle.py +++ b/packages/overture-schema-core/src/overture/schema/core/scoping/vehicle.py @@ -1,3 +1,7 @@ +""" +Types supporting the vehicle scope. +""" + from enum import Enum from typing import Annotated, Literal @@ -55,7 +59,7 @@ class VehicleHeightSelector(BaseModel): value: Annotated[ float32, Field( - ge=0, decription="Vehicle height selection threshold in the given `unit`" + ge=0, description="Vehicle height selection threshold in the given `unit`" ), ] unit: LengthUnit = Field(description="Height unit in which `value` is expressed") diff --git a/packages/overture-schema-core/src/overture/schema/core/sources.py b/packages/overture-schema-core/src/overture/schema/core/sources.py new file mode 100644 index 000000000..068cc4f07 --- /dev/null +++ b/packages/overture-schema-core/src/overture/schema/core/sources.py @@ -0,0 +1,101 @@ +""" +Document source data from which features and fields of features are derived. +""" + +import textwrap +from datetime import datetime +from typing import Annotated, NewType + +from pydantic import BaseModel, Field + +from overture.schema.core.scoping import Scope, scoped +from overture.schema.core.types import ConfidenceScore +from overture.schema.system.field_constraint import UniqueItemsConstraint +from overture.schema.system.model_constraint import no_extra_fields +from overture.schema.system.string import JsonPointer, StrippedString + + +@no_extra_fields +@scoped(Scope.GEOMETRIC_RANGE) +class SourceItem(BaseModel): + """ + Specifies the source of the data used for a feature or one of its properties. + """ + + # Required + + property: JsonPointer = Field( + description=textwrap.dedent(""" + A JSON Pointer identifying the property (field) that this source information applies to. + + The root document value `""` indicates that this source information applies to the + entire feature, excepting properties (fields) for which a dedicated source information + record exists. + + Any other JSON Pointer apart from `""` indicates that this source record provides + dedicated source information for the property at the path in the JSON Pointer. As an + example, the value `"/names/common/en"` indicates that the source information applies to + the English common name of a named feature, while the value `"/geometry"` indicates that + it applies to the feature geometry. + """).strip() + ) + dataset: str = Field( + description=textwrap.dedent(""" + Name of the dataset where the source data can be found. + """).strip() + ) + + # Optional + + license: Annotated[ + StrippedString | None, + Field( + description=textwrap.dedent(""" + Source data license name. + + This should be a valid SPDX license identifier when available. + + If omitted, contact the data provider for more license information. + """).strip() + ), + ] = None + record_id: Annotated[ + str | None, + Field( + description=textwrap.dedent( + """ + Identifies the specific record within the source dataset where the source data can + be found. + + The format of record identifiers is dataset-specific. + """ + ).strip() + ), + ] = None + update_time: Annotated[ + datetime | None, + Field(description="Last update time of the source data record."), + ] = None + confidence: Annotated[ + ConfidenceScore | None, + Field( + description=textwrap.dedent(""" + Confidence value from the source dataset. + + This is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data. + """).strip() + ), + ] = None + + +Sources = NewType( + "Sources", + Annotated[ + list[SourceItem], + Field( + min_length=1, + description="""Information about the source data used to assemble the feature.""", + ), + UniqueItemsConstraint(), + ], +) diff --git a/packages/overture-schema-core/src/overture/schema/core/types.py b/packages/overture-schema-core/src/overture/schema/core/types.py index e4c3f3a35..8cf9e8cc8 100644 --- a/packages/overture-schema-core/src/overture/schema/core/types.py +++ b/packages/overture-schema-core/src/overture/schema/core/types.py @@ -1,46 +1,16 @@ -from datetime import datetime -from typing import Annotated, Any, NewType +from typing import Annotated, NewType from pydantic import ( Field, - GetCoreSchemaHandler, - GetJsonSchemaHandler, ) -from pydantic_core import core_schema - -from overture.schema.system.field_constraint import ( - FieldConstraint, -) -from overture.schema.system.primitive import float32, int32 - - -class ConfidenceScoreConstraint(FieldConstraint): - """Constraint for confidence/probability scores (0.0 to 1.0).""" - - def __get_pydantic_core_schema__( - self, source: type[Any], handler: GetCoreSchemaHandler - ) -> core_schema.CoreSchema: - # Use built-in constraints for validation - return core_schema.float_schema(ge=0.0, le=1.0) - - def __get_pydantic_json_schema__( - self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler - ) -> dict[str, Any]: - json_schema = handler(core_schema) - json_schema["minimum"] = 0.0 - json_schema["maximum"] = 1.0 - json_schema["description"] = "Confidence score between 0.0 and 1.0" - return json_schema +from overture.schema.system.primitive import float32, int16, int32 ConfidenceScore = NewType( "ConfidenceScore", Annotated[ float32, - ConfidenceScoreConstraint(), - Field( - description="Confidence value from the source dataset, particularly relevant for ML-derived data." - ), + Field(description="Confidence score between 0.0 and 1.0", ge=0.0, le=1.0), ], ) @@ -48,89 +18,15 @@ def __get_pydantic_json_schema__( Level = NewType( "Level", Annotated[ - int32, - Field(default=0, description="Z-order of the feature where 0 is visual level"), + int16, + Field(description="Z-order of the feature where 0 is visual level"), ], ) -# It might be reasonable to combine "update_time" and "version" in a single -# "updateVersion" field which gives the last Overture version number in which the -# feature changed. The downside to doing this is that the number would cease to be -# indicative of the "rate of change" of the feature. FeatureVersion = NewType( "FeatureVersion", Annotated[int32, Field(ge=0, description="")] ) -# A somewhat more compact approach would be to reference the Overture version where the -# feature last changed instead of the update time, and expect clients to do a lookup if -# they really care about the time -FeatureUpdateTime = NewType( - "FeatureUpdateTime", - Annotated[ - datetime, - Field( - description="Timestamp when the feature was last updated", - ), - ], -) - -Prominence = NewType( - "Prominence", - Annotated[ - int, - Field( - ge=1, - lt=100, - description="Represents Overture's view of a place's significance or importance. This value can be used to help drive cartographic display of a place and is derived from various factors including, but not limited to: population, capital status, place tags, and type.", - ), - ], -) - -MinZoom = NewType( - "MinZoom", - Annotated[ - int, - Field( - ge=0, - le=23, - description="""Recommended minimum tile zoom per the Slippy Maps convention. - -The Slippy Maps zooms are explained in the following references: -- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames -- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection""", - ), - ], -) - -MaxZoom = NewType( - "MaxZoom", - Annotated[ - int, - Field( - ge=0, - le=23, - description="""Recommended maximum tile zoom per the Slippy Maps convention. - -The Slippy Maps zooms are explained in the following references: -- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames -- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection""", - ), - ], -) - -# FIXME: Use of `default` on this "floating" type declaration results in a Pydantic warning that the -# default has no effect. Default value should be migrated to site usage in the actual models. -SortKey = NewType( - "SortKey", - Annotated[ - int, - Field( - default=0, - description="An ascending numeric that defines the recommended order features should be drawn in. Features with lower number should be shown on top of features with a higher number.", - ), - ], -) - # this is an enum in the JSON Schema, but that prevents OvertureFeature from being extended Theme = Annotated[ str, Field(description="Top-level Overture theme this feature belongs to") @@ -141,13 +37,8 @@ def __get_pydantic_json_schema__( __all__ = [ "ConfidenceScore", - "FeatureUpdateTime", "FeatureVersion", "Level", - "MaxZoom", - "MinZoom", - "Prominence", - "SortKey", "Theme", "Type", ] diff --git a/packages/overture-schema-core/src/overture/schema/core/unit.py b/packages/overture-schema-core/src/overture/schema/core/unit.py index 109327cec..64d7f1275 100644 --- a/packages/overture-schema-core/src/overture/schema/core/unit.py +++ b/packages/overture-schema-core/src/overture/schema/core/unit.py @@ -1,42 +1,46 @@ -from enum import Enum +""" +Measurement units for speed, length, weight, and the like. +""" +from overture.schema.system.doc import DocumentedEnum -class SpeedUnit(str, Enum): + +class SpeedUnit(str, DocumentedEnum): """Unit of speed.""" - MPH = "mph" - KPH = "km/h" + MPH = ("mph", "Miles per hour") + KMH = ("km/h", "Kilometers per hour") -class LengthUnit(str, Enum): +class LengthUnit(str, DocumentedEnum): """Unit of length.""" # Keep in sync with `combobulib/measure.py`. # Imperial units. - IN = "in" # Imperial: Inch. - FT = "ft" # Imperial: Foot. - YD = "yd" # Imperial: Yard. - MI = "mi" # Imperial: Mile. + IN = ("in", "One inch in the imperial and US customary systems") + FT = ("ft", "One foot in the imperial and US customary systems (12 inches)") + YD = ("yd", "One yard in the imperial and US customary systems (three feet)") + MI = ("mi", "One mile in the imperial and US customary systems (1,760 yards)") # SI units. - CM = "cm" # SI: centimeter. - M = "m" # SI: meter. - KM = "km" # SI: kilometer. + CM = ("cm", "One centimeter in the metric and SI systems") + M = ("m", "One meter in the metric and SI systems") + KM = ("km", "One kilometer in the metric and SI systems") -class WeightUnit(str, Enum): +class WeightUnit(str, DocumentedEnum): """Unit of weight.""" # Keep in sync with `combobulib/measure.py`. # Imperial units. - OZ = "oz" # Imperial: Ounce. - LB = "lb" # Imperial: Pound. - ST = "st" # Imperial: Short Ton. - LT = "lt" # Imperial: Long Ton. + OZ = ("oz", "One ounce in the imperial and US customary systems") + LB = ("lb", "One pound in the imperial and US customary systems") + ST = ("st", "One short ton, or one ton in the US customary system (2,000 pounds)") + LT = ("lt", "One long ton, or one ton in the imperial system (2,400 pounds)") # SI units. - G = "g" # SI: gram. - KG = "kg" # SI: kilogram. - T = "t" # SI: tonne. + G = ("g", "One gram in the metric and SI systems") + KG = ("kg", "One kilogram in the metric and SI systems") + T = ("t", "One tonne in the metric and SI systems") diff --git a/packages/overture-schema-core/tests/test_json_schema_for_primitive_types.py b/packages/overture-schema-core/tests/test_json_schema_for_primitive_types.py deleted file mode 100644 index d8d115f2d..000000000 --- a/packages/overture-schema-core/tests/test_json_schema_for_primitive_types.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Tests for JSON Schema generation with primitive types.""" - -from overture.schema.core.json_schema import json_schema -from overture.schema.system.primitive import ( - float32, - float64, - int32, - uint8, -) -from pydantic import BaseModel - - -class TestJsonSchemaForPrimitiveTypes: - """Test JSON Schema generation for models using primitive types.""" - - def test_uint8_json_schema(self) -> None: - """Test JSON Schema generation for UInt8 fields.""" - - class TestModel(BaseModel): - value: uint8 - optional_value: uint8 | None = None - - schema = json_schema(TestModel) - - # Check required field - value_prop = schema["properties"]["value"] - assert value_prop["type"] == "integer" - assert value_prop["minimum"] == 0 - assert value_prop["maximum"] == 255 - - # Check optional field - optional_prop = schema["properties"]["optional_value"] - assert optional_prop["type"] == "integer" - assert optional_prop["minimum"] == 0 - assert optional_prop["maximum"] == 255 - assert "default" not in optional_prop # Should be truly optional - - # Check required fields - assert schema["required"] == ["value"] - - def test_int32_json_schema(self) -> None: - """Test JSON Schema generation for Int32 fields.""" - - class TestModel(BaseModel): - value: int32 - - schema = json_schema(TestModel) - - value_prop = schema["properties"]["value"] - assert value_prop["type"] == "integer" - assert value_prop["minimum"] == -(2**31) - assert value_prop["maximum"] == 2**31 - 1 - - def test_float_types_json_schema(self) -> None: - """Test JSON Schema generation for Float32 and Float64.""" - - class TestModel(BaseModel): - f32: float32 - f64: float64 - - schema = json_schema(TestModel) - - # Float32 should have explicit type override - f32_prop = schema["properties"]["f32"] - assert f32_prop["type"] == "number" - - # Float64 should have explicit type override - f64_prop = schema["properties"]["f64"] - assert f64_prop["type"] == "number" - - def test_mixed_primitive_types_json_schema(self) -> None: - """Test JSON Schema generation for model with mixed primitive types.""" - - class MixedModel(BaseModel): - id: uint8 - score: float32 - count: int32 | None = None - - schema = json_schema(MixedModel) - - # Verify all properties exist - expected_props = {"id", "score", "count"} - assert set(schema["properties"].keys()) == expected_props - - # Verify required fields - assert set(schema["required"]) == {"id", "score"} - - # Spot check a few properties - assert schema["properties"]["id"]["type"] == "integer" - assert schema["properties"]["id"]["minimum"] == 0 - assert schema["properties"]["id"]["maximum"] == 255 - - assert schema["properties"]["score"]["type"] == "number" - - # Optional field should not have default - assert "default" not in schema["properties"]["count"] diff --git a/packages/overture-schema-core/tests/test_models.py b/packages/overture-schema-core/tests/test_models.py index f4cf64f46..c674ab5e1 100644 --- a/packages/overture-schema-core/tests/test_models.py +++ b/packages/overture-schema-core/tests/test_models.py @@ -4,8 +4,8 @@ import pytest from deepdiff import DeepDiff -from overture.schema.core.json_schema import EnhancedJsonSchemaGenerator from overture.schema.core.models import OvertureFeature +from overture.schema.system.json_schema import GenerateOmitNullableOptionalJsonSchema from overture.schema.system.primitive import ( BBox, Geometry, @@ -34,13 +34,15 @@ def prune_json_schema(data: dict[str, Any]) -> dict[str, Any]: def test_feature_json_schema() -> None: actual = prune_json_schema( - OvertureFeature.model_json_schema(schema_generator=EnhancedJsonSchemaGenerator) + OvertureFeature.model_json_schema( + schema_generator=GenerateOmitNullableOptionalJsonSchema + ) ) print(json.dumps(actual, indent=2)) expect = { "$defs": { - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": False, "properties": { "between": { @@ -413,7 +415,7 @@ def test_feature_json_schema() -> None: "type": {"type": "string"}, "version": {"maximum": 2147483647, "minimum": 0, "type": "integer"}, "sources": { - "items": {"$ref": "#/$defs/SourcePropertyItem"}, + "items": {"$ref": "#/$defs/SourceItem"}, "minItems": 1, "type": "array", "uniqueItems": True, @@ -434,61 +436,77 @@ def test_feature_json_schema() -> None: assert diff == {} -@pytest.mark.parametrize( - "feature, expect", - [ - ( - OvertureFeature( # type: ignore[call-arg] - id="foo", - theme="bar", - type="baz", - geometry=Geometry(Point(-1, 1)), - version=1, - ), - { - "type": "Feature", - "id": "foo", - "geometry": { - "type": "Point", - "coordinates": [-1, 1], - }, - "properties": { - "theme": "bar", - "type": "baz", - "version": 1, - "sources": None, - }, - }, +FEATURE_WITH_GEO_JSON: tuple[tuple[OvertureFeature, dict[str, Any]], ...] = ( + ( + OvertureFeature( # type: ignore[call-arg] + id="foo", + theme="bar", + type="baz", + geometry=Geometry(Point(-1, 1)), + version=1, ), - ( - OvertureFeature( - id="foo", - theme="bar", - type="baz", - bbox=BBox(0, 0, 1, 1), - geometry=Geometry(LineString(((0, 0), (1, 1)))), - version=2, - ), - { - "type": "Feature", - "id": "foo", - "bbox": [0, 0, 1, 1], - "geometry": { - "type": "LineString", - "coordinates": [ - [0, 0], - [1, 1], - ], - }, - "properties": { - "theme": "bar", - "type": "baz", - "version": 2, - "sources": None, - }, + { + "type": "Feature", + "id": "foo", + "geometry": { + "type": "Point", + "coordinates": [-1, 1], + }, + "properties": { + "theme": "bar", + "type": "baz", + "version": 1, + "sources": None, }, + }, + ), + ( + OvertureFeature( + id="foo", + theme="bar", + type="baz", + bbox=BBox(0, 0, 1, 1), + geometry=Geometry(LineString(((0, 0), (1, 1)))), + version=2, ), - ], + { + "type": "Feature", + "id": "foo", + "bbox": [0, 0, 1, 1], + "geometry": { + "type": "LineString", + "coordinates": [ + [0, 0], + [1, 1], + ], + }, + "properties": { + "theme": "bar", + "type": "baz", + "version": 2, + "sources": None, + }, + }, + ), ) -def test_feature_json(feature: OvertureFeature, expect: dict[str, Any]) -> None: - assert feature.model_dump(mode="json") == expect + + +@pytest.mark.parametrize("feature, geo_json", FEATURE_WITH_GEO_JSON) +def test_feature_dump_json(feature: OvertureFeature, geo_json: dict[str, Any]) -> None: + """Ensure GeoJSON serialization inherited from `Feature` continues to work correctly.""" + assert feature.model_dump(mode="json") == geo_json + + +@pytest.mark.parametrize("feature, geo_json", FEATURE_WITH_GEO_JSON) +def test_feature_validate_json( + feature: OvertureFeature, geo_json: dict[str, Any] +) -> None: + """ + Ensure validation from a GeoJSON string continues, functionality inherited from `Feature`, + continues to work correctly. + """ + validated: OvertureFeature = OvertureFeature.model_validate_json( + json.dumps(geo_json) + ) + + assert feature == validated diff --git a/packages/overture-schema-core/tests/test_serde.py b/packages/overture-schema-core/tests/test_serde.py deleted file mode 100644 index c60a17c9b..000000000 --- a/packages/overture-schema-core/tests/test_serde.py +++ /dev/null @@ -1,351 +0,0 @@ -"""Tests for serialization/deserialization functionality in overture-schema-core. - -Tests mode switching and geometry format support with synthetic data. -""" - -import json -from typing import Any, Literal - -import pytest -from deepdiff import DeepDiff -from overture.schema.core import OvertureFeature, parse_feature -from pydantic import Field -from shapely.geometry import Point - - -class Place(OvertureFeature): - """Simple stubbed place model for testing serde functionality.""" - - theme: Literal["places"] - type: Literal["place"] - - names: dict[str, str] | None = Field(None, description="Place names") - categories: dict[str, str] | None = Field(None, description="Place categories") - confidence: float | None = Field(None, description="Confidence score") - - -# Note: StubPlace is not registered in the parser union type, -# so it will only work if it matches the existing Place model structure - - -def deep_compare_dicts( - original: dict[str, Any], parsed: dict[str, Any] -) -> tuple[bool, str]: - """Perform deep comparison between original and parsed dictionaries. - - Returns (is_equal, differences_report). - """ - diff = DeepDiff(original, parsed, ignore_order=True, significant_digits=15) - - if not diff: - return True, "" - - # Format differences for readable output - differences = [] - - if "values_changed" in diff: - differences.append("Value changes:") - for key, change in diff["values_changed"].items(): - differences.append( - f" {key}: {change['old_value']} -> {change['new_value']}" - ) - - if "dictionary_item_added" in diff: - differences.append("Added items:") - for item in diff["dictionary_item_added"]: - differences.append(f" {item}") - - if "dictionary_item_removed" in diff: - differences.append("Removed items:") - for item in diff["dictionary_item_removed"]: - differences.append(f" {item}") - - if "type_changes" in diff: - differences.append("Type changes:") - for key, change in diff["type_changes"].items(): - differences.append(f" {key}: {change['old_type']} -> {change['new_type']}") - - return False, "\n".join(differences) - - -# Synthetic test data -SAMPLE_FLAT_FEATURE = { - "id": "test-feature-123", - "geometry": {"type": "Point", "coordinates": [-122.4194, 37.7749]}, - "theme": "places", - "type": "place", - "version": 1, - "names": {"primary": "Test Place"}, - "categories": {"primary": "restaurant"}, - "confidence": 0.95, -} - -SAMPLE_GEOJSON_FEATURE = { - "type": "Feature", - "id": "test-feature-123", - "geometry": {"type": "Point", "coordinates": [-122.4194, 37.7749]}, - "properties": { - "theme": "places", - "type": "place", - "version": 1, - "names": {"primary": "Test Place"}, - "categories": {"primary": "restaurant"}, - "confidence": 0.95, - }, -} - - -class TestSerializationModes: - """Test serialization mode functionality.""" - - def test_python_mode_output_structure(self) -> None: - """Test that Python mode returns flattened structure.""" - result = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="python") - assert result is not None - - # Should be flattened (no properties key) - assert "properties" not in result - assert "id" in result - assert "geometry" in result - assert "theme" in result - assert "type" in result - assert result["id"] == "test-feature-123" - assert result["theme"] == "places" - assert result["type"] == "place" - - def test_json_mode_output_structure(self) -> None: - """Test that JSON mode returns GeoJSON structure.""" - result = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="json") - assert result is not None - - # Should be GeoJSON - assert result["type"] == "Feature" - assert "properties" in result - assert "id" in result - assert "geometry" in result - - # Properties should contain theme and type - properties = result["properties"] - assert properties is not None - assert "theme" in properties - assert "type" in properties - assert properties["theme"] == "places" - assert properties["type"] == "place" - - def test_mode_data_consistency(self) -> None: - """Test that both modes contain the same data, just structured differently.""" - python_result = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="python") - json_result = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="json") - assert python_result is not None - assert json_result is not None - - # Flatten JSON result for comparison - flattened_from_json = { - "id": json_result["id"], - "geometry": json_result["geometry"], - **json_result["properties"], - } - - # Handle geometry objects for comparison - python_normalized = python_result.copy() - if "geometry" in python_normalized and hasattr( - python_normalized["geometry"], "to_geo_json" - ): - python_normalized["geometry"] = python_normalized["geometry"].to_geo_json() - - # Normalize both through JSON for comparison - python_json = json.loads(json.dumps(python_normalized, default=str)) - flattened_json = json.loads(json.dumps(flattened_from_json, default=str)) - - is_equal, diff_report = deep_compare_dicts(python_json, flattened_json) - assert is_equal, ( - f"Python and JSON modes should contain same data:\n{diff_report}" - ) - - def test_roundtrip_consistency(self) -> None: - """Test that Python->JSON->Python roundtrip preserves data.""" - # Parse in Python mode - python_output = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="python") - - # Parse in JSON mode, then back to Python - json_output = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="json") - assert json_output is not None - roundtrip_output = parse_feature(json_output, Place, mode="python") - - assert python_output is not None - assert roundtrip_output is not None - - # Normalize both for comparison - python_normalized = python_output.copy() - if "geometry" in python_normalized and hasattr( - python_normalized["geometry"], "to_geo_json" - ): - python_normalized["geometry"] = python_normalized["geometry"].to_geo_json() - python_normalized = json.loads(json.dumps(python_normalized, default=str)) - - roundtrip_normalized = roundtrip_output.copy() - if "geometry" in roundtrip_normalized and hasattr( - roundtrip_normalized["geometry"], "to_geo_json" - ): - roundtrip_normalized["geometry"] = roundtrip_normalized[ - "geometry" - ].to_geo_json() - roundtrip_normalized = json.loads(json.dumps(roundtrip_normalized, default=str)) - - is_equal, diff_report = deep_compare_dicts( - python_normalized, roundtrip_normalized - ) - assert is_equal, f"Roundtrip should preserve data:\n{diff_report}" - - def test_input_format_independence(self) -> None: - """Test that flat vs GeoJSON input produces same output.""" - # Parse both input formats in both modes - python_from_flat = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="python") - json_from_flat = parse_feature(SAMPLE_FLAT_FEATURE, Place, mode="json") - python_from_geojson = parse_feature( - SAMPLE_GEOJSON_FEATURE, Place, mode="python" - ) - json_from_geojson = parse_feature(SAMPLE_GEOJSON_FEATURE, Place, mode="json") - - assert python_from_flat is not None - assert json_from_flat is not None - assert python_from_geojson is not None - assert json_from_geojson is not None - - # Results should be identical regardless of input format - is_equal, diff_report = deep_compare_dicts( - python_from_flat, python_from_geojson - ) - assert is_equal, ( - f"Python mode should produce same result from flat/GeoJSON input:\n{diff_report}" - ) - - is_equal, diff_report = deep_compare_dicts(json_from_flat, json_from_geojson) - assert is_equal, ( - f"JSON mode should produce same result from flat/GeoJSON input:\n{diff_report}" - ) - - -class TestGeometryFormats: - """Test geometry format support.""" - - def test_geojson_geometry_input(self) -> None: - """Test parsing with GeoJSON geometry dict.""" - feature = SAMPLE_FLAT_FEATURE.copy() - geometry_dict = feature["geometry"] - assert isinstance(geometry_dict, dict) - expected_coords = geometry_dict["coordinates"] - - result = parse_feature(feature, Place, mode="python") - assert result is not None - assert "geometry" in result - - # Check that geometry is properly parsed - geometry = result["geometry"] - assert hasattr(geometry, "to_geo_json") - geo_json = geometry.to_geo_json() - assert geo_json["type"] == "Point" - assert list(geo_json["coordinates"]) == expected_coords - - def test_shapely_geometry_input(self) -> None: - """Test parsing with Shapely geometry objects.""" - feature = SAMPLE_FLAT_FEATURE.copy() - geometry_dict = feature["geometry"] - assert isinstance(geometry_dict, dict) - expected_coords = geometry_dict["coordinates"] - feature["geometry"] = Point(expected_coords[0], expected_coords[1]) - - result = parse_feature(feature, Place, mode="python") - assert result is not None - assert "geometry" in result - - # Check that geometry is properly parsed - geometry = result["geometry"] - assert hasattr(geometry, "to_geo_json") - geo_json = geometry.to_geo_json() - assert geo_json["type"] == "Point" - assert list(geo_json["coordinates"]) == expected_coords - - def test_wkb_geometry_input(self) -> None: - """Test parsing with WKB bytes.""" - feature = SAMPLE_FLAT_FEATURE.copy() - geometry_dict = feature["geometry"] - assert isinstance(geometry_dict, dict) - expected_coords = geometry_dict["coordinates"] - point = Point(expected_coords[0], expected_coords[1]) - feature["geometry"] = point.wkb - - result = parse_feature(feature, Place, mode="python") - assert result is not None - assert "geometry" in result - - # Check that geometry is properly parsed - geometry = result["geometry"] - assert hasattr(geometry, "to_geo_json") - geo_json = geometry.to_geo_json() - assert geo_json["type"] == "Point" - assert list(geo_json["coordinates"]) == expected_coords - - def test_wkt_geometry_input(self) -> None: - """Test parsing with WKT strings.""" - feature = SAMPLE_FLAT_FEATURE.copy() - geometry_dict = feature["geometry"] - assert isinstance(geometry_dict, dict) - expected_coords = geometry_dict["coordinates"] - point = Point(expected_coords[0], expected_coords[1]) - feature["geometry"] = point.wkt - - result = parse_feature(feature, Place, mode="python") - assert result is not None - assert "geometry" in result - - # Check that geometry is properly parsed - geometry = result["geometry"] - assert hasattr(geometry, "to_geo_json") - geo_json = geometry.to_geo_json() - assert geo_json["type"] == "Point" - assert list(geo_json["coordinates"]) == expected_coords - - def test_different_geometry_types(self) -> None: - """Test parsing with different Point geometry coordinates.""" - base_feature = { - "id": "test-geom", - "theme": "places", - "type": "place", - "version": 1, - "names": {"primary": "Test"}, - "categories": {"primary": "restaurant"}, - } - - # Test different Point coordinates - test_cases = [ - [-122.4, 37.7], - [0.0, 0.0], - [180.0, -90.0], - [-180.0, 90.0], - ] - - for point_coords in test_cases: - point_feature = base_feature.copy() - point_feature["geometry"] = Point(point_coords[0], point_coords[1]) - result = parse_feature(point_feature, Place, mode="python") - assert result is not None - geometry = result["geometry"] - assert hasattr(geometry, "to_geo_json") - geo_json = geometry.to_geo_json() - assert geo_json["type"] == "Point" - assert list(geo_json["coordinates"]) == point_coords - - def test_invalid_geometry_formats_fail(self) -> None: - """Test that invalid geometry formats are rejected.""" - feature = SAMPLE_FLAT_FEATURE.copy() - - # Invalid GeoJSON geometry - feature["geometry"] = {"type": "InvalidType", "coordinates": [1, 2]} - with pytest.raises(ValueError): - parse_feature(feature, Place) - - # Invalid WKT - feature["geometry"] = "INVALID WKT STRING" - with pytest.raises(ValueError): - parse_feature(feature, Place) diff --git a/packages/overture-schema-core/tests/test_types.py b/packages/overture-schema-core/tests/test_types.py deleted file mode 100644 index 290b55476..000000000 --- a/packages/overture-schema-core/tests/test_types.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import Annotated - -import pytest -from overture.schema.core.scoping.lr import LinearReferenceRangeConstraint -from overture.schema.core.types import ( - ConfidenceScoreConstraint, -) -from pydantic import BaseModel, ValidationError - - -class TestNumericConstraints: - """Test all numeric constraints.""" - - def test_confidence_score_constraint_valid(self) -> None: - """Test ConfidenceScoreConstraint with valid scores (0.0 to 1.0).""" - - class TestModel(BaseModel): - confidence: Annotated[float, ConfidenceScoreConstraint()] - - valid_scores = [0.0, 0.1, 0.5, 0.9, 1.0, 0.123456] - - for score in valid_scores: - model = TestModel(confidence=score) - assert model.confidence == score - - def test_confidence_score_constraint_invalid(self) -> None: - """Test ConfidenceScoreConstraint with invalid scores.""" - - class TestModel(BaseModel): - confidence: Annotated[float, ConfidenceScoreConstraint()] - - invalid_scores = [-0.1, 1.1, 2.0, -1.0, 10.0] - - for score in invalid_scores: - with pytest.raises(ValidationError) as exc_info: - TestModel(confidence=score) - # Check for Pydantic's built-in error messages - assert "greater than or equal to 0" in str( - exc_info.value - ) or "less than or equal to 1" in str(exc_info.value) - - -class TestSpecializedConstraints: - """Test specialized constraints.""" - - def test_linear_reference_range_constraint_valid(self) -> None: - """Test LinearReferenceRangeConstraint with valid ranges.""" - - class TestModel(BaseModel): - range_val: Annotated[list[float], LinearReferenceRangeConstraint()] - - valid_ranges = [ - [0.0, 1.0], - [0.1, 0.9], - [0.0, 0.5], - [0.25, 0.75], - ] - - for range_val in valid_ranges: - model = TestModel(range_val=range_val) - assert model.range_val == range_val - - def test_linear_reference_range_constraint_invalid(self) -> None: - """Test LinearReferenceRangeConstraint with invalid ranges.""" - - class TestModel(BaseModel): - range_val: Annotated[list[float], LinearReferenceRangeConstraint()] - - invalid_ranges = [ - [0.9, 0.1], # start > end - [-0.1, 0.5], # start < 0 - [0.5, 1.1], # end > 1 - [0.5, 0.5], # start == end - [0.0], # Wrong length - [0.0, 0.5, 1.0], # Wrong length - ] - - for range_val in invalid_ranges: - with pytest.raises(ValidationError) as exc_info: - TestModel(range_val=range_val) - # Check that validation fails with appropriate error - assert len(exc_info.value.errors()) > 0 diff --git a/packages/overture-schema-divisions-theme/src/overture/schema/divisions/division/models.py b/packages/overture-schema-divisions-theme/src/overture/schema/divisions/division/models.py index fb5d4bbfe..a8227d3e6 100644 --- a/packages/overture-schema-divisions-theme/src/overture/schema/divisions/division/models.py +++ b/packages/overture-schema-divisions-theme/src/overture/schema/divisions/division/models.py @@ -7,8 +7,8 @@ from overture.schema.core import ( OvertureFeature, ) +from overture.schema.core.cartography import CartographicallyHinted from overture.schema.core.models import ( - CartographicallyHinted, Perspectives, ) from overture.schema.core.names import CommonNames, Named, Names diff --git a/packages/overture-schema-divisions-theme/src/overture/schema/divisions/enums.py b/packages/overture-schema-divisions-theme/src/overture/schema/divisions/enums.py index cd594142a..8634f0f5c 100644 --- a/packages/overture-schema-divisions-theme/src/overture/schema/divisions/enums.py +++ b/packages/overture-schema-divisions-theme/src/overture/schema/divisions/enums.py @@ -4,8 +4,10 @@ class PlaceType(str, Enum): - """Category of the division from a finite, hierarchical, ordered list of categories - (e.g. country, region, locality, etc.) similar to a Who's on First placetype.""" + """ + Category of the division from a finite, hierarchical, ordered list of categories (e.g., country, + region, locality, etc.) similar to a Who's on First placetype. + """ # Largest unit of independent sovereignty, e.g. the United States, France. COUNTRY = "country" diff --git a/packages/overture-schema-divisions-theme/tests/division_area_baseline_schema.json b/packages/overture-schema-divisions-theme/tests/division_area_baseline_schema.json index 9ebf27707..b4ba3f48f 100644 --- a/packages/overture-schema-divisions-theme/tests/division_area_baseline_schema.json +++ b/packages/overture-schema-divisions-theme/tests/division_area_baseline_schema.json @@ -11,7 +11,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -26,7 +26,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -40,14 +40,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -58,6 +59,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -148,7 +150,7 @@ "type": "object" }, "PlaceType": { - "description": "Category of the division from a finite, hierarchical, ordered list of categories\n(e.g. country, region, locality, etc.) similar to a Who's on First placetype.", + "description": "Category of the division from a finite, hierarchical, ordered list of categories (e.g., country,\nregion, locality, etc.) similar to a Who's on First placetype.", "enum": [ "country", "dependency", @@ -175,12 +177,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -192,34 +194,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -229,7 +232,7 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, @@ -406,9 +409,9 @@ "type": "string" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-divisions-theme/tests/division_baseline_schema.json b/packages/overture-schema-divisions-theme/tests/division_baseline_schema.json index 0cc3005b2..53434bd29 100644 --- a/packages/overture-schema-divisions-theme/tests/division_baseline_schema.json +++ b/packages/overture-schema-divisions-theme/tests/division_baseline_schema.json @@ -24,31 +24,33 @@ }, "CartographicHints": { "additionalProperties": false, - "description": "Defines cartographic hints for optimal use of Overture features in map-making.", + "description": "Cartographic hints for optimal use of Overture features in map-making.", "properties": { "max_zoom": { - "description": "Recommended maximum tile zoom per the Slippy Maps convention.\n\nThe Slippy Maps zooms are explained in the following references:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", + "description": "Recommended maximum tile zoom level in which this feature should be displayed.\n\nIt is recommended that the feature be hidden at zoom levels above this value.\n\nZoom levels follow the Slippy Maps convention, documented in the following\nreferences:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", "maximum": 23, "minimum": 0, "title": "Max Zoom", "type": "integer" }, "min_zoom": { - "description": "Recommended minimum tile zoom per the Slippy Maps convention.\n\nThe Slippy Maps zooms are explained in the following references:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", + "description": "Recommended minimum tile zoom level in which this feature should be displayed.\n\nIt is recommended that the feature be hidden at zoom levels below this value.\n\nZoom levels follow the Slippy Maps convention, documented in the following\nreferences:\n- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames\n- https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection", "maximum": 23, "minimum": 0, "title": "Min Zoom", "type": "integer" }, "prominence": { - "description": "Represents Overture's view of a place's significance or importance. This value can be used to help drive cartographic display of a place and is derived from various factors including, but not limited to: population, capital status, place tags, and type.", - "exclusiveMaximum": 100, + "description": "Subjective scale of feature significance or importance, with 1 being the least, and\n100 being the most, significant.\n\nThis value can be used to help drive decisions about how and when to display a\nfeature, and how to treat it relative to neighboring features.\n\nWhen populated by Overture, this value is derived from various factors including,\nbut not limited to: feature and subtype, population, and capital status.", + "maximum": 100, "minimum": 1, "title": "Prominence", "type": "integer" }, "sort_key": { - "description": "An ascending numeric that defines the recommended order features should be drawn in. Features with lower number should be shown on top of features with a higher number.", + "description": "Integer indicating the recommended order in which to draw features.\n\nFeatures with a lower number should be drawn \"in front\" of features with a higher\nnumber.", + "maximum": 255, + "minimum": 0, "title": "Sort Key", "type": "integer" } @@ -100,7 +102,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -115,7 +117,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -129,14 +131,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -147,6 +150,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -249,7 +253,7 @@ "type": "object" }, "PlaceType": { - "description": "Category of the division from a finite, hierarchical, ordered list of categories\n(e.g. country, region, locality, etc.) similar to a Who's on First placetype.", + "description": "Category of the division from a finite, hierarchical, ordered list of categories (e.g., country,\nregion, locality, etc.) similar to a Who's on First placetype.", "enum": [ "country", "dependency", @@ -276,12 +280,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -293,34 +297,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -330,7 +335,7 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, @@ -537,9 +542,9 @@ "type": "string" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-divisions-theme/tests/division_boundary_baseline_schema.json b/packages/overture-schema-divisions-theme/tests/division_boundary_baseline_schema.json index 1e19452e9..c76a177e8 100644 --- a/packages/overture-schema-divisions-theme/tests/division_boundary_baseline_schema.json +++ b/packages/overture-schema-divisions-theme/tests/division_boundary_baseline_schema.json @@ -48,7 +48,7 @@ "type": "object" }, "PlaceType": { - "description": "Category of the division from a finite, hierarchical, ordered list of categories\n(e.g. country, region, locality, etc.) similar to a Who's on First placetype.", + "description": "Category of the division from a finite, hierarchical, ordered list of categories (e.g., country,\nregion, locality, etc.) similar to a Who's on First placetype.", "enum": [ "country", "dependency", @@ -66,12 +66,12 @@ "title": "PlaceType", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -83,34 +83,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -120,7 +121,7 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, @@ -336,9 +337,9 @@ "type": "string" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-divisions-theme/tests/test_division_area_json_schema_baseline.py b/packages/overture-schema-divisions-theme/tests/test_division_area_json_schema_baseline.py index 71dd9e1c6..ae55d0666 100644 --- a/packages/overture-schema-divisions-theme/tests/test_division_area_json_schema_baseline.py +++ b/packages/overture-schema-divisions-theme/tests/test_division_area_json_schema_baseline.py @@ -3,8 +3,8 @@ import json import os -from overture.schema.core import json_schema from overture.schema.divisions import DivisionArea +from overture.schema.system.json_schema import json_schema def test_division_area_json_schema_baseline() -> None: diff --git a/packages/overture-schema-divisions-theme/tests/test_division_boundary_json_schema_baseline.py b/packages/overture-schema-divisions-theme/tests/test_division_boundary_json_schema_baseline.py index 269ef53cf..4f43dbcb2 100644 --- a/packages/overture-schema-divisions-theme/tests/test_division_boundary_json_schema_baseline.py +++ b/packages/overture-schema-divisions-theme/tests/test_division_boundary_json_schema_baseline.py @@ -3,8 +3,8 @@ import json import os -from overture.schema.core import json_schema from overture.schema.divisions import DivisionBoundary +from overture.schema.system.json_schema import json_schema def test_division_boundary_json_schema_baseline() -> None: diff --git a/packages/overture-schema-divisions-theme/tests/test_division_json_schema_baseline.py b/packages/overture-schema-divisions-theme/tests/test_division_json_schema_baseline.py index c9b5e306e..99ab0779d 100644 --- a/packages/overture-schema-divisions-theme/tests/test_division_json_schema_baseline.py +++ b/packages/overture-schema-divisions-theme/tests/test_division_json_schema_baseline.py @@ -3,8 +3,8 @@ import json import os -from overture.schema.core import json_schema from overture.schema.divisions import Division +from overture.schema.system.json_schema import json_schema def test_division_json_schema_baseline() -> None: diff --git a/packages/overture-schema-places-theme/src/overture/schema/places/__init__.py b/packages/overture-schema-places-theme/src/overture/schema/places/__init__.py index cb055ea31..6f77edc42 100644 --- a/packages/overture-schema-places-theme/src/overture/schema/places/__init__.py +++ b/packages/overture-schema-places-theme/src/overture/schema/places/__init__.py @@ -6,6 +6,6 @@ __path__ = __import__("pkgutil").extend_path(__path__, __name__) -from .place import Place +from .place import Address, Brand, Categories, OperatingStatus, Place -__all__ = ["Place"] +__all__ = ["Address", "Brand", "Categories", "OperatingStatus", "Place"] diff --git a/packages/overture-schema-places-theme/src/overture/schema/places/place.py b/packages/overture-schema-places-theme/src/overture/schema/places/place.py index 595c46db7..e9b3b49e3 100644 --- a/packages/overture-schema-places-theme/src/overture/schema/places/place.py +++ b/packages/overture-schema-places-theme/src/overture/schema/places/place.py @@ -1,4 +1,6 @@ -"""Place feature models for Overture Maps places theme.""" +""" +The `Place` feature type model and supporting types. +""" import textwrap from enum import Enum @@ -122,12 +124,12 @@ class Address(BaseModel): class Place(OvertureFeature[Literal["places"], Literal["place"]], Named): """ - A Place is a point representation of a real-world facility, service, or amenity. + Places are point representations of real-world facilities, businesses, services, or amenities. """ model_config = ConfigDict(title="place") - # Required + # Overture Feature geometry: Annotated[ Geometry, @@ -136,6 +138,9 @@ class Place(OvertureFeature[Literal["places"], Literal["place"]], Named): description="Position of the place. Places are point geometries.", ), ] + + # Required + operating_status: Annotated[ OperatingStatus, Field( diff --git a/packages/overture-schema-places-theme/tests/place_baseline_schema.json b/packages/overture-schema-places-theme/tests/place_baseline_schema.json index 790c4e477..c43f25c17 100644 --- a/packages/overture-schema-places-theme/tests/place_baseline_schema.json +++ b/packages/overture-schema-places-theme/tests/place_baseline_schema.json @@ -86,7 +86,7 @@ }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -101,7 +101,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -115,14 +115,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -133,6 +134,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -241,12 +243,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -258,34 +260,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -295,12 +298,12 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, "additionalProperties": false, - "description": "A Place is a point representation of a real-world facility, service, or amenity.", + "description": "Places are point representations of real-world facilities, businesses, services, or amenities.", "properties": { "bbox": { "description": "An optional bounding box for the feature", @@ -437,9 +440,9 @@ "uniqueItems": true }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-places-theme/tests/test_place_json_schema_baseline.py b/packages/overture-schema-places-theme/tests/test_place_json_schema_baseline.py index 756372bd8..0b142a712 100644 --- a/packages/overture-schema-places-theme/tests/test_place_json_schema_baseline.py +++ b/packages/overture-schema-places-theme/tests/test_place_json_schema_baseline.py @@ -3,8 +3,8 @@ import json import os -from overture.schema.core import json_schema from overture.schema.places import Place +from overture.schema.system.json_schema import json_schema def test_place_json_schema_baseline() -> None: diff --git a/packages/overture-schema-system/src/overture/schema/system/__init__.py b/packages/overture-schema-system/src/overture/schema/system/__init__.py index 3ebdb93e6..c2ade1b8e 100644 --- a/packages/overture-schema-system/src/overture/schema/system/__init__.py +++ b/packages/overture-schema-system/src/overture/schema/system/__init__.py @@ -13,6 +13,8 @@ - :mod:`field_constraint ` Constraints that can be annotated onto Pydantic model fields to force them to conform to well-known rules, for example "a collection that contains unique items" or "a string that is a valid country code". +- :mod:`json_schema ` Overture-flavored JSON Schema generation + for Pydantic models. - :mod:`model_constraint ` Constraints that can be decorated onto Pydantic model classes to add cross-field validation rules, for example "these two fields are mutually-exclusive" or "if this field is set, then that field must also be set". @@ -87,7 +89,7 @@ >>> from overture.schema.system.field_constraint import PatternConstraint >>> OsmIdConstraint = PatternConstraint( ... pattern=r"^[nwr]\d+$", -... error_message="Invalid OSM ID format: {value}. Must be n123, w123, or r123." +... error_message="invalid OSM ID format: {value}. Must be n123, w123, or r123." ... ) >>> >>> from pydantic import BaseModel, Field @@ -98,7 +100,7 @@ >>> try: ... MyModel(**{"osm_id": "foo"}) ... except ValidationError as e: -... assert "Invalid OSM ID format: foo. Must be n123, w123, or r123." in str(e) +... assert "invalid OSM ID format: foo. Must be n123, w123, or r123." in str(e) ... print("Validation failed") Validation failed @@ -145,6 +147,7 @@ doc, feature, field_constraint, + json_schema, metadata, model_constraint, optionality, @@ -159,6 +162,7 @@ "doc", "feature", "field_constraint", + "json_schema", "metadata", "model_constraint", "optionality", diff --git a/packages/overture-schema-system/src/overture/schema/system/_json_schema.py b/packages/overture-schema-system/src/overture/schema/system/_json_schema.py index 56213ee5e..c9afce006 100644 --- a/packages/overture-schema-system/src/overture/schema/system/_json_schema.py +++ b/packages/overture-schema-system/src/overture/schema/system/_json_schema.py @@ -5,7 +5,25 @@ from pydantic.json_schema import JsonSchemaValue, JsonValue -def get_static_json_schema(config: ConfigDict) -> JsonSchemaValue: +def get_static_json_schema_extra(config: ConfigDict) -> JsonSchemaValue: + """ + Get the static *extra* JSON Schema from a Pydantic model config dictionary. + + Parameters + ---------- + config : ConfigDict + Config dictionary + + Returns + ------- + JsonSchemaValue + Extra JSON Schema from `config`, or `{}` if `config` has no extra JSON Schema + + Raises + ------ + ValueError + If `config` contains dynamic extra JSON Schema (`JsonSchemaExtraCallable`) + """ json_schema: ( JsonSchemaValue | Callable[[JsonSchemaValue], None] @@ -27,6 +45,19 @@ def get_static_json_schema(config: ConfigDict) -> JsonSchemaValue: def put_all_of(json_schema: JsonSchemaValue, operands: list[JsonSchemaValue]) -> None: + """ + Insert an `"allOf"` schema composition clause into a JSON Schema. + + If the target JSON Schema already contains an `"allOf"` clause, the `operands` are added to the + existing `"allOf"` clause. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + operands : list[JsonSchemaValue] + Non-empty list of operands for the `"allOf"` clause + """ _verify_json_schema_value(("json_schema", json_schema)) _verify_operands_not_empty(JsonSchemaValue, operands) if "allOf" not in json_schema: @@ -42,6 +73,20 @@ def put_all_of(json_schema: JsonSchemaValue, operands: list[JsonSchemaValue]) -> def put_any_of(json_schema: JsonSchemaValue, operands: list[JsonSchemaValue]) -> None: + """ + Insert an `"anyOf"` schema composition clause into a JSON Schema. + + If the target JSON Schema already contains an `"anyOf"` clause, the existing clause is retained + and the new one is added by adding both new and existing `"anyOf"` clauses to an `"allOf"` + clause using `put_all_of`. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + operands : list[JsonSchemaValue] + Non-empty list of operands for the `"anyOf"` clause + """ _verify_json_schema_value(("json_schema", json_schema)) _verify_operands_not_empty(JsonSchemaValue, operands) prev: JsonSchemaValue = {} @@ -53,6 +98,20 @@ def put_any_of(json_schema: JsonSchemaValue, operands: list[JsonSchemaValue]) -> def put_one_of(json_schema: JsonSchemaValue, operands: list[JsonSchemaValue]) -> None: + """ + Insert a `"oneOf"` schema composition clause into a JSON Schema. + + If the target JSON Schema already contains a `"oneOf"` clause, the existing clause is retained + and the new one is added by adding both new and existing `"oneOf"` clauses to an `"allOf"` + clause using `put_all_of`. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + operands : list[JsonSchemaValue] + Non-empty list of operands for the `"allOf"` clause + """ _verify_json_schema_value(("json_schema", json_schema)) _verify_operands_not_empty(JsonSchemaValue, operands) prev: JsonSchemaValue = {} @@ -64,6 +123,21 @@ def put_one_of(json_schema: JsonSchemaValue, operands: list[JsonSchemaValue]) -> def put_not(json_schema: JsonSchemaValue, operand: JsonSchemaValue) -> None: + """ + Insert a `"not"` schema composition clause into a JSON Schema. + + If the target JSON Schema already contains a `"not"` clause, the existing clause is retained + and the new one is added by refactoring the schema. Several refactorings are possible but in + all cases the `"not"` clause will remain and will contain an `"anyOf"` clause as its direct + child, and `operand` as a child of the `"anyOf"` clause. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + operand : JsonSchemaValue + Operand for the `"not"` clause + """ _verify_json_schema_value(("json_schema", json_schema), ("operand", operand)) prev: JsonSchemaValue = {} try_move("not", json_schema, prev) @@ -106,6 +180,27 @@ def put_if( when_true: JsonSchemaValue | None, when_false: JsonSchemaValue | None = None, ) -> None: + """ + Insert `"if"`/`"then"` conditional schema application elements with an optional `"else"` clause + into a JSON Schema. + + If the target JSON Schema does not already contain an `"if"`/`"then"`/`"else"` elements, the new + elements are added directly into the JSON Schema. If it does already contain them, then the + existing `"if"`/`"then"`/`"else"` elements are moved into a separate object, the new ones are + inserted into a second separate object, and both of these objects are added into the JSON + Schema using `put_all_of`. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + condition : JsonSchemaValue | None + Operand for the `"if"` clause + when_true : JsonSchemaValue | None + Operand for the `"then"` clause + when_false : JsonSchemaValue | None + Operand for the `"else"` clause + """ _verify_json_schema_value(("json_schema", json_schema)) if condition is not None: _verify_json_schema_value(("condition", condition)) @@ -135,6 +230,20 @@ def _put(dst: JsonSchemaValue) -> JsonSchemaValue: def put_required(json_schema: JsonSchemaValue, operands: list[str]) -> None: + """ + Insert a `"required"` validation clause into a JSON Schema. + + If the target JSON Schema already contains a `"required"` clause, the existing clause is + retained and `operands` is merged into it by appending the items that aren't already in the + `"required"` clause to the end of it, in the order in which they appear in `operands`. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + operands : list[str] + Operands for the `"required"` clause + """ _verify_json_schema_value(("json_schema", json_schema)) _verify_operands_not_empty(str, operands) if "required" in json_schema: @@ -149,6 +258,28 @@ def put_properties( json_schema: JsonSchemaValue, new_properties: JsonSchemaValue, ) -> None: + """ + Insert members into the `"properties"` applicator keyword within the schema for a value of type + `"object"`. + + If the target JSON Schema already contains a `"properties"` clause, the new properties from + `new_properties` are merged into it. Otherwise, a new `"properties"` clause is inserted into + `json_schema` and and all properties from `new_properties` are inserted into it. + + Parameters + ---------- + json_schema : JsonSchemaValue + Target JSON Schema + new_properties : JsonSchemaValue + New properties to add to the `"properties"` clause within `json_schema` + + Raises + ------ + ValueError + If a property entry in `new_properties` can't be merged into the `"properties"` clause of + `json_schema` because there's an existing `"properties"` clause that contains a property + with the same name but a different value + """ _verify_json_schema_value( ("json_schema", json_schema), ("new_properties", new_properties) ) @@ -177,6 +308,21 @@ def put_properties( def try_move(key: str, src: JsonSchemaValue, dst: JsonSchemaValue) -> None: + """ + Move a key (that may not exist) from one JSON Schema to another one. + + Removes the key `key` and its value from `src` and inserts them into `dst`. If `src` does not + contain the key `key`, nothing happens. + + Parameters + ---------- + key : str + Key to move from `src` to `dst` + src : JsonSchemaValue + Source JSON Schema from which to move `key` and its value + dst : JsonSchemaValue + Destination JSON Schema into which to move `key` and its value + """ try: value = src[key] dst[key] = value diff --git a/packages/overture-schema-system/src/overture/schema/system/create_model.py b/packages/overture-schema-system/src/overture/schema/system/create_model.py index c7022ea54..be810dbc3 100644 --- a/packages/overture-schema-system/src/overture/schema/system/create_model.py +++ b/packages/overture-schema-system/src/overture/schema/system/create_model.py @@ -1,3 +1,7 @@ +""" +Dynamic Pydantic model creation with preservation of Overture metadata. +""" + from collections.abc import Callable from typing import Any, TypeVar @@ -23,7 +27,7 @@ def create_model( **field_definitions: Any | tuple[str, Any], ) -> type[ModelT]: """ - Dynamically creates and returns a new Pydantic model, preserving Overture metadata. + Dynamically create and return a new Pydantic model, preserving Overture metadata. Use `create_model` to dynamically create a subclass of any `BaseModel` while preserving Overture `Metadata`. diff --git a/packages/overture-schema-system/src/overture/schema/system/doc.py b/packages/overture-schema-system/src/overture/schema/system/doc.py index 8f72ce08e..4ff5c0525 100644 --- a/packages/overture-schema-system/src/overture/schema/system/doc.py +++ b/packages/overture-schema-system/src/overture/schema/system/doc.py @@ -1,3 +1,10 @@ +""" +Documentation support. + +This module enables documenting things that "native Python" doesn't have a documentation solution +for. +""" + from enum import Enum from typing import TypeVar, cast @@ -27,7 +34,6 @@ class DocumentedEnum(Enum): Examples -------- - A documented enumeration: >>> class Status(str, DocumentedEnum): diff --git a/packages/overture-schema-system/src/overture/schema/system/feature.py b/packages/overture-schema-system/src/overture/schema/system/feature.py index 001ec2629..de3f14df6 100644 --- a/packages/overture-schema-system/src/overture/schema/system/feature.py +++ b/packages/overture-schema-system/src/overture/schema/system/feature.py @@ -1,9 +1,15 @@ +""" +Geospatial feature model with GeoJSON-compatible JSON Schema. +""" + +import inspect from enum import Enum from functools import reduce from typing import Any from pydantic import ( BaseModel, + Discriminator, Field, GetJsonSchemaHandler, ModelWrapValidatorHandler, @@ -84,6 +90,82 @@ class Feature(BaseModel): ... ] .. _GeoJSON format: https://datatracker.ietf.org/doc/html/rfc7946 + + Because the GeoJSON format moves feature fields to a place Pydantic does not expect them (the + `"properties"` block of the JSON object), a naive use of Pydantic discriminated unions will not + work when deserializing from JSON with `model_validate_json`. To create a robust discriminated + union, use the `field_discriminator` method: + + >>> from typing import Annotated, Literal + >>> from overture.schema.system.primitive import float32 + >>> import pydantic + >>> + >>> class Field(Feature): + ... geometry: Annotated[ + ... Geometry, + ... GeometryTypeConstraint(GeometryType.POLYGON, GeometryType.MULTI_POLYGON) + ... ] + ... type: Literal['field'] + ... + >>> class Fence(Feature): + ... geometry: Annotated[ + ... Geometry, + ... GeometryTypeConstraint(GeometryType.LINE_STRING) + ... ] + ... type: Literal['fence'] + ... subtype: Literal[ + ... 'chain_link', 'barb_wire_3', 'barb_wire_4', 'barb_wire_5', 'electric_1', + ... 'electric_2', 'electric_3', 'electric_4', 'split_rail', 'woven_wire' + ... ] + ... height: float32 | None = pydantic.Field( + ... default=None, + ... description='Optional fence height in meters' + ... ) + ... + >>> FarmFeature = pydantic.TypeAdapter( + ... Annotated[ + ... Annotated[Field, pydantic.Tag('field')] + ... | Annotated[Fence, pydantic.Tag('fence')], + ... pydantic.Field( + ... discriminator=Feature.field_discriminator('type', Field, Fence) + ... ), + ... ] + ... ) + >>> + >>> FarmFeature.validate_json('''{ + ... "type": "Feature", + ... "geometry": { + ... "type": "LineString", + ... "coordinates": [[0, 0], [0, 0.01]] + ... }, + ... "properties": { + ... "type": "fence", + ... "subtype": "barb_wire_4" + ... } + ... }''') + Fence(id=, bbox=, geometry=<>, type='fence', subtype='barb_wire_4', height=None) + + You can model classes that are not `Feature` subclasses in `field_discriminator` to enable + discriminated unions between features and non-features, as long as at least one model class is + a `Feature`: + + >>> class Farmer(BaseModel): + ... type: Literal['farmer'] + ... name: str + ... + >>> FarmModel = pydantic.TypeAdapter( + ... Annotated[ + ... Annotated[Farmer, pydantic.Tag('farmer')] + ... | Annotated[Field, pydantic.Tag('field')] + ... | Annotated[Fence, pydantic.Tag('fence')], + ... pydantic.Field( + ... discriminator=Feature.field_discriminator('type', Field, Fence) + ... ), + ... ] + ... ) + >>> + >>> FarmModel.validate_json('{"type":"farmer","name":"John Deere"}') + Farmer(type='farmer', name='John Deere') """ id: Omitable[Id] = Field(description="An optional unique ID for the feature") @@ -100,12 +182,128 @@ class Feature(BaseModel): field and annotating it with a `GeometryTypeConstraint`. """ + @staticmethod + def field_discriminator( + field: str, *model_classes: type[BaseModel] + ) -> Discriminator: + """ + Return a discriminator that can be used in a Pydantic `Field` to support tagged unions of + features. + + Use this method to generate a Pydantic discriminator that works *both* with Python-style + flat data *and* GeoJSON. Note that at least one member of `model_classes` must be a + `Feature` (if no feature models are involved, you don't need this method and should build + your discriminated union using Pydantic's standard discriminator facilities). + + Parameters + ---------- + field : str + Field name, which must be present in all models + *model_classes : type[BaseModel] + One or more Pydantic model classes, at least one of which must be a subclass of the + `Feature` class + + Returns + ------- + Discriminator + Discriminator that enables discriminated unions that include features + + Raises + ------ + TypeError + If any member of `model_classes` is not a subclass of `BaseModel`. + TypeError + If no member of `model_classes` is a subclass of `Feature`. + TypeError + If any member of `model_classes` does not have a field named `field`. + ValueError + If `field` names one of the core GeoJSON feature fields that cannot be discriminated: + `"bbox"`, `"geometry`", or `"id"`. + ValueError + If `model_classes` has length less than 2. + """ + if not isinstance(field, str): + raise TypeError( + f"`field` must be a `str`, but {repr(field)} has type `{type(field).__name__}`" + ) + elif field in ["bbox", "geometry", "id"]: + raise ValueError( + f"`field` value {repr(field)} is not allowed because it is one of the core GeoJSON " + "feature properties: 'bbox', 'geometry', and 'id' - use a different discriminator " + "field!" + ) + elif len(model_classes) < 2: + raise ValueError( + f"`model_classes` must have at least two items, but {repr(model_classes)} has length {len(model_classes)}" + ) + + non_models = [ + x + for x in model_classes + if not isinstance(x, type) or not issubclass(x, BaseModel) + ] + if non_models: + raise TypeError( + "`model_classes` contains at least one non-model class: the value(s) " + f"{repr(non_models)} should be subclasses of {BaseModel.__name__} but the type(s) " + f"are {', '.join([f'`{x.__name__ if isinstance(x, type) else type(x).__name__}`' for x in non_models])}, " + f"respectively, which are not subclasses of `{BaseModel.__name__}`..." + ) + + missing_field = [ + f"`{t.__name__}`" for t in model_classes if field not in t.model_fields + ] + if missing_field: + raise TypeError( + "`model_classes` contains at least one model class that does not have a field " + f"named {repr(field)}: {', '.join(missing_field)}" + ) + + if not any(t for t in model_classes if issubclass(t, Feature)): + frame = inspect.currentframe() + method_name = ( + f"{Feature.__name__}.{frame.f_code.co_name if frame else '???'}" + ) + raise TypeError( + f"`model_classes` does not contain any subclasses of `{Feature.__name__}` - " + f"you don't need `{method_name}(...)` unless you have at least one " + f"`{Feature.__name__}` model - use standard Pydantic discriminators instead" + ) + + def get_discriminator_value(data: object) -> Any: + # Pydantic doesn't have a facility to tell the dynamic discriminator function whether + # the context is 'python' or 'json', so we just have to use heuristics. If the input is + # a `dict` with the mandatory attributes `"type": "Feature"`, `"geometry"`, and + # `"properties"`, we assume we're in GeoJSON-land, otherwise not. + # + # If the data doesn't contain the discriminator field at all, we return `None` to tell + # Pydantic proceed to try the next variant in the union, if there is one. This is + # equivalent to how Pydantic behaves with static unions. + if ( + isinstance(data, dict) + and all(f in data for f in ["geometry", "properties", "type"]) + and data["type"] == "Feature" + ): + properties: Any = data["properties"] + if not isinstance(properties, dict): + return None + else: + return properties.get(field, None) + else: + return ( + data.get(field, None) + if isinstance(data, dict) + else getattr(data, field, None) + ) + + return Discriminator(get_discriminator_value) + @model_serializer(mode="wrap") - def serialize_model( + def __serialize_with_geo_json_support__( self, serializer: SerializerFunctionWrapHandler, info: SerializationInfo ) -> Any: """ - Serializes to GeoJSON when the mode is JSON, otherwise to Pydantic's standard Python mode. + Serialize to GeoJSON when the mode is JSON, otherwise to Pydantic's standard Python mode. """ data = serializer(self) @@ -122,19 +320,18 @@ def serialize_model( @model_validator(mode="wrap") @classmethod - def validate_model( + def __validate_with_geo_json_support__( cls, data: Any, handler: ModelWrapValidatorHandler[Self], info: ValidationInfo ) -> Self: """ - Validates the model as GeoJSON when the mode is JSON, otherwise applies Pydantic's standard + Validate the model as GeoJSON when the mode is JSON, otherwise applies Pydantic's standard validation. """ - if not isinstance(data, dict): - raise TypeError( - f"feature data must be a `dict`, but {repr(data)} is a `{type(data).__name__}`" - ) - if info.mode == "json": + if not isinstance(data, dict): + raise TypeError( + f"feature data must be a `dict` when validating JSON, but {repr(data)} is a `{type(data).__name__}`" + ) def validation_error( type: str, input: object, error: str, *loc: str @@ -245,7 +442,7 @@ def __get_pydantic_json_schema__( handler: GetJsonSchemaHandler, ) -> JsonSchemaValue: """ - Generates a JSON Schema that validates the feature as GeoJSON. + Generate a JSON Schema that validates the feature as GeoJSON. """ json_schema = handler(schema) diff --git a/packages/overture-schema-system/src/overture/schema/system/field_constraint/collection.py b/packages/overture-schema-system/src/overture/schema/system/field_constraint/collection.py index 827c7b1eb..a39159217 100644 --- a/packages/overture-schema-system/src/overture/schema/system/field_constraint/collection.py +++ b/packages/overture-schema-system/src/overture/schema/system/field_constraint/collection.py @@ -1,3 +1,7 @@ +""" +Constraints on fields with collection types, such as lists and dictionaries. +""" + from collections.abc import Collection from typing import Any, get_origin diff --git a/packages/overture-schema-system/src/overture/schema/system/field_constraint/field_constraint.py b/packages/overture-schema-system/src/overture/schema/system/field_constraint/field_constraint.py index d47ac0fbb..d96e1cc4f 100644 --- a/packages/overture-schema-system/src/overture/schema/system/field_constraint/field_constraint.py +++ b/packages/overture-schema-system/src/overture/schema/system/field_constraint/field_constraint.py @@ -1,3 +1,13 @@ +""" +Interface for constraints that apply to a single Pydantic field. + +- If you are authoring new field-level constraints, this module is for you: you will very likely + want to derive a subclass of `FieldConstraint` (or of a more specific base class such as + `CollectionConstraint`). +- If you are looking to reuse existing constraints, this module is too low-level for you. You need + one of the peer modules that implements a specific constraint type. +""" + from abc import ABC, abstractmethod from typing import Any @@ -22,7 +32,8 @@ def __get_pydantic_core_schema__( def __get_pydantic_json_schema__( self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler ) -> dict[str, Any]: - """Generate JSON schema. + """ + Generate JSON schema. Override in subclasses for custom schema. """ diff --git a/packages/overture-schema-system/src/overture/schema/system/field_constraint/string.py b/packages/overture-schema-system/src/overture/schema/system/field_constraint/string.py index a512be3e1..8c2d90415 100644 --- a/packages/overture-schema-system/src/overture/schema/system/field_constraint/string.py +++ b/packages/overture-schema-system/src/overture/schema/system/field_constraint/string.py @@ -1,3 +1,7 @@ +""" +Constraints on fields with string values. +""" + import re from typing import Any diff --git a/packages/overture-schema-system/src/overture/schema/system/json_schema.py b/packages/overture-schema-system/src/overture/schema/system/json_schema.py new file mode 100644 index 000000000..1f41838c3 --- /dev/null +++ b/packages/overture-schema-system/src/overture/schema/system/json_schema.py @@ -0,0 +1,204 @@ +""" +JSON Schemas of Overture-based Pydantic models. +""" + +from enum import Enum +from types import UnionType +from typing import Annotated, Any, Union, cast, get_args, get_origin + +from pydantic import BaseModel, TypeAdapter +from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue +from pydantic_core import core_schema +from typing_extensions import override + + +class GenerateOmitNullableOptionalJsonSchema(GenerateJsonSchema): + """ + Generates a JSON Schema in which optional values that allow `None` are omitted instead of being + assigned a `null` value. + + Example + ------- + >>> import json + >>> from pydantic import BaseModel, Field + >>> + >>> class FooModel(BaseModel): + ... foo: str | None = None + ... + >>> print(json.dumps(FooModel.model_json_schema( + ... schema_generator=GenerateOmitNullableOptionalJsonSchema + ... ), indent=2)) + { + "properties": { + "foo": { + "title": "Foo", + "type": "string" + } + }, + "title": "FooModel", + "type": "object" + } + + + ⚠️ Warning + ---------- + When using this class to generate a JSON Schema, you must dump your model JSON carefully to + ensure that it matches the JSON Schema. + + When using `BaseModel.model_dump_json` or `TypeAdapter.dump_json`, use the argument + `exclude_unset=True` to ensure unset optional fields are omitted from the dumped JSON. Failing + to do so will result in JSON that includes explicit `null` values that the JSON Schema + generated with this generator class does not allow, as shown below: + + >>> print(FooModel().model_dump_json()) + {"foo":null} + >>> print(FooModel().model_dump_json(exclude_unset=True)) + {} + + Background + ---------- + An optional field in Pydantic is a field with a default value. If a model field has a specified + default value, the field does not need to be explicitly set on a model instance. Optional + fields, and only optional fields, can be "unset" in a Pydantic model. + + A nullable field in Pydantic is a field that can hold the value `None`. + + A field may be optional only, nullable only, or both optional and nullable (or neither). + + The default Pydantic behavior for nullable optional fields is to give them a JSON Schema in + which the field may hold the JSON literal value `null`. + + >>> FooModel.model_fields['foo'].is_required() + False + >>> FooModel().model_fields_set + set() + >>> FooModel(foo='bar').model_fields_set + {'foo'} + >>> print(json.dumps(FooModel.model_json_schema(), indent=2)) + { + "properties": { + "foo": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Foo" + } + }, + "title": "FooModel", + "type": "object" + } + + This default Pydantic behavior is reasonable, and aligns well with the Python internal + representation. However, it doesn't take advantage of a quiet advantage of JSON and JSON Schema, + which is that there's another concept of optionality that does not require the value `null` at + all. In this paradigm, if the field is there, it has a set value; and if the field is missing, + it does not. Pydantic does enable this alternative paradigm via the experimental `MISSING` + sentinel, but this sentinel is not yet a full-fledged feature and has some rough edges. + + The purpose of this class is to generate a JSON Schema that aligns with the alternative paradigm + where nullable optional fields are omitted from the JSON rather than assigned the literal value + `null`. + + Transformations + --------------- + This class makes the following changes to the default Pydantic JSON Schema for nullable optional + fields only: + + 1. The JSON Schema type `null` is removed as an allowed type from the field's schema, which will + usually result in eliminating the `"anyOf"` composition keyword from the field's schema. + 2. If the field's default value is `None` (JSON `null`), the default value is removed. + """ + + @override + def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue: + match GenerateOmitNullableOptionalJsonSchema._redact_nullable_schema(schema): + case redacted if redacted and schema["default"] is None: + return self.generate_inner(redacted["schema"]) + case redacted if redacted: + return self.generate_inner(redacted) + case _: + return super().default_schema(schema) + + @staticmethod + def _redact_nullable_schema( + schema: core_schema.CoreSchema, + ) -> core_schema.CoreSchema | None: + match schema.get("schema", None): + case sub_schema if sub_schema and sub_schema["type"] == "nullable": + return {**schema, "schema": sub_schema["schema"]} + case sub_schema if sub_schema: + redacted = ( + GenerateOmitNullableOptionalJsonSchema._redact_nullable_schema( + sub_schema + ) + ) + return None if not redacted else {**schema, "schema": redacted} + case _: + return None + + +def json_schema(thing: object) -> JsonSchemaValue: + """ + Generate JSON Schema for a Pydantic model or union of models. + + Parameters + ---------- + thing : object + Either a Pydantic model or a union of Pydantic models + + Returns + ------- + JsonSchemaValue + JSON Schema for the model or union of models + + Raises + ------ + TypeError + If `models` is not a Pydantic model or union of Pydantic models + """ + match _Kind.of(thing): + case _Kind.BASE_MODEL: + return cast(BaseModel, thing).model_json_schema( + schema_generator=GenerateOmitNullableOptionalJsonSchema + ) + case _Kind.UNION: + tap: TypeAdapter = TypeAdapter(thing) + return tap.json_schema( + schema_generator=GenerateOmitNullableOptionalJsonSchema + ) + case _: + raise TypeError( + f"`models` must be a subclass of `BaseModel` or a union of subclasses of " + f"`BaseModel`, but {repr(thing)} is a " + f"`{thing.__name__ if isinstance(thing, type) else type(thing).__name__}`" + ) + + +class _Kind(str, Enum): + BASE_MODEL = "base_model" + UNION = "union" + + @staticmethod + def of(thing: Any) -> Union["_Kind", None]: + if isinstance(thing, type) and issubclass(thing, BaseModel): + return _Kind.BASE_MODEL + else: + match get_origin(thing): + case a if a is Annotated: + return _Kind.of(get_args(thing)[0]) + case u if (u is UnionType or u is Union) and _Kind._union_args(thing): + return _Kind.UNION + case _: + return None + + @staticmethod + def _union_args(thing: Any) -> bool: + return all( + _Kind.of(a) in [_Kind.BASE_MODEL, _Kind.UNION] for a in get_args(thing) + ) diff --git a/packages/overture-schema-system/src/overture/schema/system/metadata.py b/packages/overture-schema-system/src/overture/schema/system/metadata.py index 6c2abd75d..26c946802 100644 --- a/packages/overture-schema-system/src/overture/schema/system/metadata.py +++ b/packages/overture-schema-system/src/overture/schema/system/metadata.py @@ -1,3 +1,9 @@ +""" +Metadata that can be attached to arbitrary Python values. + +Overture Metadata is primarily used to supply metadata for code generation use cases. +""" + from collections.abc import ( Hashable, ItemsView, @@ -210,7 +216,7 @@ def items(self) -> ItemsView[Key, object]: def copy(self) -> "Metadata": """ - Returns a shallow copy of this metadata. + Return a shallow copy of this metadata. """ return Metadata(self.__wrapped.copy()) @@ -225,8 +231,8 @@ def update( data: Mapping[Key, object] | Iterable[tuple[Key, object]] | None = None, ) -> None: """ - Updates this metadata by inserting values from `data`. In the case of a key conflict, the - new value from `data` replaces the old value. + Update this metadata by inserting values from `data`. In the case of a key conflict, the new + value from `data` replaces the old value. Parameters ---------- @@ -238,7 +244,7 @@ def update( def attach_to(self, target: object) -> None: """ - Attaches this metadata to the given target value. The metadata can be retrieved by calling + Attach this metadata to the given target value. The metadata can be retrieved by calling `retrieve_from`. Parameters @@ -260,7 +266,7 @@ def retrieve_from( default: Mapping[Key, object] | Iterable[tuple[Key, object]] | None = None, ) -> Union["Metadata", None]: """ - Retrieves the metadata attached go a given source value, if it exists. Metadata can be + Retrieve the metadata attached go a given source value, if it exists. Metadata can be attached by calling `attach_to`. Parameters diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/forbid_if.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/forbid_if.py index 0dc04af1f..ab0a8a329 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/forbid_if.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/forbid_if.py @@ -1,9 +1,14 @@ +""" +Prohibit every field in a group of fields from having a value explicitly set, but only if a +condition is true. +""" + from collections.abc import Callable from pydantic import BaseModel, ConfigDict from typing_extensions import override -from .._json_schema import get_static_json_schema, put_if +from .._json_schema import get_static_json_schema_extra, put_if from .model_constraint import ( Condition, OptionalFieldGroupConstraint, @@ -16,7 +21,7 @@ def forbid_if( condition: Condition, ) -> Callable[[type[BaseModel]], type[BaseModel]]: """ - Decorates a Pydantic model class with a constraint forbidding any of the named fields from + Decorate a Pydantic model class with a constraint forbidding any of the named fields from holding an explicitly-assigned value, but only if a field value condition is true. To ensure parity between Python and JSON Schema validation, a field's value must be explicitly @@ -59,7 +64,6 @@ def forbid_if( ... print('Validation failed') Validation failed """ - model_constraint = ForbidIfConstraint._create_internal( f"@{forbid_if.__name__}", field_names, @@ -133,7 +137,7 @@ def validate_instance(self, model_instance: BaseModel) -> None: def edit_config(self, model_class: type[BaseModel], config: ConfigDict) -> None: super().edit_config(model_class, config) - json_schema = get_static_json_schema(config) + json_schema = get_static_json_schema_extra(config) put_if( json_schema, diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/min_fields_set.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/min_fields_set.py index 915623040..7462e6a8b 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/min_fields_set.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/min_fields_set.py @@ -1,15 +1,19 @@ +""" +Require some minimum number of fields to be set to a non-`None` value. +""" + from collections.abc import Callable from pydantic import BaseModel, ConfigDict from typing_extensions import override -from .._json_schema import get_static_json_schema +from .._json_schema import get_static_json_schema_extra from .model_constraint import ModelConstraint def min_fields_set(count: int) -> Callable[[type[BaseModel]], type[BaseModel]]: """ - Decorates a Pydantic model class with a constraint that requires a minimum number of fields in + Decorate a Pydantic model class with a constraint that requires a minimum number of fields in the model to be set to a non-`None` value. This function is the decorator version of the `MinFieldsSetConstraint` class. @@ -114,7 +118,7 @@ def validate_instance(self, model_instance: BaseModel) -> None: def edit_config(self, model_class: type[BaseModel], config: ConfigDict) -> None: super().edit_config(model_class, config) - json_schema = get_static_json_schema(config) + json_schema = get_static_json_schema_extra(config) try: prev = json_schema["minProperties"] diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/model_constraint.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/model_constraint.py index 64def571a..9b8e0c924 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/model_constraint.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/model_constraint.py @@ -1,3 +1,14 @@ +""" +Interfaces for constraints that apply to an entire Pydantic model, not just one field. + +- If you are authoring new model-level constraints, this module is for you: you will very likely + want to derive a subclass of `ModelConstraint` or a more specific base class such as + `FieldGroupConstraint` or `OptionalFieldGroupConstraint`. +- If you are looking to reuse existing constraints, this module is too low-level for you. You need + one of the peer modules that implements a specific constraint type, such as: `forbid_if`, + `min_fields_set`, `no_extra_fields`, or `radio_group`. +""" + from abc import ABC, abstractmethod from collections import Counter from collections.abc import Callable @@ -57,8 +68,8 @@ def name(self) -> str: @final def decorate(self, model_class: type[BaseModel]) -> type[BaseModel]: """ - Decorates a Pydantic model with this constraint, returning a new version of the model that - has this constraint applied to it. + Decorate a Pydantic model, returning a new version of the model that has this constraint + applied to it. This is a final method and should not be overridden by subclasses. @@ -103,7 +114,6 @@ def decorate(self, model_class: type[BaseModel]) -> type[BaseModel]: ... print("Validation failed") Validation failed """ - if not isinstance(model_class, type): raise TypeError(f"`{self.name}` can only be applied to classes") if not issubclass(model_class, BaseModel): @@ -134,7 +144,7 @@ def decorate(self, model_class: type[BaseModel]) -> type[BaseModel]: def validate_class(self, model_class: type[BaseModel]) -> None: """ - Validates that the constraint is appropriate for the model class. + Validate that the constraint is appropriate for the model class. This method is called by the `decorate` method to ensure this constraint is applicable to the class being decorated with it. @@ -153,7 +163,7 @@ def validate_class(self, model_class: type[BaseModel]) -> None: def validate_instance(self, model_instance: BaseModel) -> None: """ - Validates the model instance against this constraint. + Validate the model instance against this constraint. Parameters ---------- @@ -169,7 +179,7 @@ def validate_instance(self, model_instance: BaseModel) -> None: def edit_config(self, model_class: type[BaseModel], config: ConfigDict) -> None: """ - Makes any changes to an existing config dictionary needed to reflect this constraint's + Make any changes to an existing config dictionary needed to reflect this constraint's validations. The existing config dictionary may already have been edited by other model constraints @@ -197,7 +207,7 @@ def get_model_constraints( cls: type["ModelConstraint"], model_class: type[BaseModel] ) -> tuple["ModelConstraint", ...]: """ - Returns the model constraints that have been applied to the given Pydantic model class. + Return the model constraints that have been applied to the given Pydantic model class. This is a final method and should not be overridden by subclasses. @@ -338,6 +348,16 @@ def validate_class(self, model_class: type[BaseModel]) -> None: class Condition(ABC): + """ + Interface for a condition expression that evaluates to a boolean value, `True` or `False`. + + Conditions may be used to control the behavior of conditional constraints. + + Conditions can be negated using the `negate` method, the convenience operator `~`, or by + explicitly instantiating an instance of `Not` that wraps `c`. In other words, for any condition + `c`, the conditions `c.negate()`, `~c`, and `Not(c)` are `True` whenever `c` is `False`. + """ + @final def __invert__(self) -> "Condition": return self.negate() @@ -345,7 +365,7 @@ def __invert__(self) -> "Condition": @abstractmethod def validate_class(self, model_class: type[BaseModel]) -> None: """ - Validates that the constraint is appropriate for the model class. + Validate that the constraint is appropriate for the model class. Parameters ---------- @@ -362,10 +382,10 @@ def validate_class(self, model_class: type[BaseModel]) -> None: @abstractmethod def eval(self, model_instance: BaseModel) -> bool: """ - Evaluates the condition against a Pydantic model instance. + Evaluate the condition against a Pydantic model instance. - This method must only be called on model instances where `validate_class` does not raise - an exception on the instance's model class. + This method must only be called on model instances where `validate_class` does not raise an + exception on the instance's model class. Parameters ---------- @@ -381,7 +401,7 @@ def eval(self, model_instance: BaseModel) -> bool: def negate(self) -> "Condition": """ - Returns a condition that represents the logical negation of this condition. + Return a condition that represents the logical negation of this condition. Examples -------- @@ -397,8 +417,7 @@ def negate(self) -> "Condition": def json_schema(self, model_class: type[BaseModel]) -> JsonDict: """ - Returns a JSON Schema that models the condition value with respect to a Pydantic model - class. + Return a JSON Schema that models the condition value with respect to a Pydantic model class. This method must only be called on model classes for which `validate_class` does not raise an exception. @@ -418,6 +437,10 @@ def json_schema(self, model_class: type[BaseModel]) -> JsonDict: @dataclass(frozen=True, slots=True) class Not(Condition): + """ + A negated condition. + """ + inner: Condition def __repr__(self) -> str: @@ -454,7 +477,7 @@ def __post_init__(self) -> None: @override def validate_class(self, model_class: type[BaseModel]) -> None: """ - Validates that the constraint is appropriate for the model class. + Validate that the constraint is appropriate for the model class. Parameters ---------- diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/no_extra_fields.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/no_extra_fields.py index fbd93fcfb..4d24f92d7 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/no_extra_fields.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/no_extra_fields.py @@ -1,3 +1,7 @@ +""" +Prohibit extra fields that aren't explicitly part of the model. +""" + from pydantic import BaseModel, ConfigDict from typing_extensions import override @@ -6,7 +10,7 @@ def no_extra_fields(model_class: type[BaseModel]) -> type[BaseModel]: """ - Decorates a Pydantic model class with a constraint that forbids extra fields that aren't + Decorate a Pydantic model class with a constraint that forbids extra fields that aren't explicitly part of the model. This function is the decorator version of the `NoExtraFieldsConstraint` class. It is syntax diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/radio_group.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/radio_group.py index c9e1bcaa1..20e7a1202 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/radio_group.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/radio_group.py @@ -1,3 +1,7 @@ +""" +Require at least one field in a group of `bool` fields to have the value `True`. +""" + from collections.abc import Callable from types import NoneType, UnionType from typing import Annotated, Any, Union, get_args, get_origin @@ -6,14 +10,14 @@ from pydantic.json_schema import JsonDict from typing_extensions import override -from .._json_schema import get_static_json_schema, put_one_of +from .._json_schema import get_static_json_schema_extra, put_one_of from .model_constraint import FieldGroupConstraint, apply_alias def radio_group(*field_names: str) -> Callable[[type[BaseModel]], type[BaseModel]]: """ - Decorates a Pydantic model class with a constraint requiring that exactly one field in a group - of `bool` fields has the value `True`. + Decorate a Pydantic model class with a constraint requiring that exactly one field in a group of + `bool` fields has the value `True`. This function is the decorator version of the `RadioGroupConstraint` class. @@ -145,7 +149,7 @@ def validate_instance(self, model_instance: BaseModel) -> None: def edit_config(self, model_class: type[BaseModel], config: ConfigDict) -> None: super().edit_config(model_class, config) - json_schema = get_static_json_schema(config) + json_schema = get_static_json_schema_extra(config) def has_true_value(field_name: str) -> JsonDict: return { diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_any_of.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_any_of.py index 988358a8b..e131c0a66 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_any_of.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_any_of.py @@ -1,16 +1,20 @@ +""" +Require at least one named field to have a value explicitly set. +""" + from collections.abc import Callable from pydantic import BaseModel, ConfigDict from pydantic.json_schema import JsonDict from typing_extensions import override -from .._json_schema import get_static_json_schema, put_any_of +from .._json_schema import get_static_json_schema_extra, put_any_of from .model_constraint import OptionalFieldGroupConstraint, apply_alias def require_any_of(*field_names: str) -> Callable[[type[BaseModel]], type[BaseModel]]: """ - Decorates a Pydantic model class with a constraint requiring that at least one of the named + Decorate a Pydantic model class with a constraint requiring that at least one of the named fields has a value explicitly set. This function is the decorator version of the `RequireAnyOfConstraint` class. @@ -104,7 +108,7 @@ def validate_instance(self, model_instance: BaseModel) -> None: def edit_config(self, model_class: type[BaseModel], config: ConfigDict) -> None: super().edit_config(model_class, config) - json_schema = get_static_json_schema(config) + json_schema = get_static_json_schema_extra(config) def required(field_name: str) -> JsonDict: return {"required": [apply_alias(model_class, field_name)]} diff --git a/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_if.py b/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_if.py index 72f6d2520..4cb5b138e 100644 --- a/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_if.py +++ b/packages/overture-schema-system/src/overture/schema/system/model_constraint/require_if.py @@ -1,9 +1,14 @@ +""" +Require every field in a group of fields to have a value explicitly set, but only if a condition is +true. +""" + from collections.abc import Callable from pydantic import BaseModel, ConfigDict from typing_extensions import override -from .._json_schema import get_static_json_schema, put_if +from .._json_schema import get_static_json_schema_extra, put_if from .model_constraint import ( Condition, OptionalFieldGroupConstraint, @@ -16,8 +21,8 @@ def require_if( condition: Condition, ) -> Callable[[type[BaseModel]], type[BaseModel]]: """ - Decorates a Pydantic model class with a constraint requiring all of the named fields to have a - value explicitly set, but only if a field value condition is true. + Decorate a Pydantic model class with a constraint requiring all of the named fields to have a + value explicitly set, but only if a condition is true. To ensure parity between Python and JSON Schema validation, a field's value must be explicitly set to satisfy the constraint. This means in particular that fields whose value was set by @@ -61,7 +66,6 @@ def require_if( ... print('Validation failed') Validation failed """ - model_constraint = RequireIfConstraint._create_internal( f"@{require_if.__name__}", field_names, @@ -134,7 +138,7 @@ def validate_instance(self, model_instance: BaseModel) -> None: def edit_config(self, model_class: type[BaseModel], config: ConfigDict) -> None: super().edit_config(model_class, config) - json_schema = get_static_json_schema(config) + json_schema = get_static_json_schema_extra(config) put_if( json_schema, diff --git a/packages/overture-schema-system/src/overture/schema/system/primitive/bbox.py b/packages/overture-schema-system/src/overture/schema/system/primitive/bbox.py index 55239487e..ac0eef391 100644 --- a/packages/overture-schema-system/src/overture/schema/system/primitive/bbox.py +++ b/packages/overture-schema-system/src/overture/schema/system/primitive/bbox.py @@ -1,3 +1,7 @@ +""" +Bounding box primitive. +""" + from typing import Any from pydantic import ( @@ -105,28 +109,48 @@ def __str__(self) -> str: @property def xmin(self) -> float | int: """ - float | int: Minimum X-coordinate + Minimum X-coordinate of the bounding box. + + Returns + ------- + float | int + Minimum X-coordinate """ return self._xmin @property def ymin(self) -> float | int: """ - float | int: Minimum Y-coordinate + Minimum Y-coordinate of the bounding box. + + Returns + ------- + float | int + Minimum Y-coordinate """ return self._ymin @property def xmax(self) -> float | int: """ - float | int: Maximum X-coordinate + Maximum X-coordinate of the bounding box + + Returns + ------- + float | int + Maximum X-coordinate """ return self._xmax @property def ymax(self) -> float | int: """ - float | int: Maximum Y-coordinate + Maximum Y-coordinate of the bounding box + + Returns + ------- + float | int + Maximum Y-coordinate """ return self._ymax diff --git a/packages/overture-schema-system/src/overture/schema/system/primitive/geom.py b/packages/overture-schema-system/src/overture/schema/system/primitive/geom.py index f22c2845a..281abe59e 100644 --- a/packages/overture-schema-system/src/overture/schema/system/primitive/geom.py +++ b/packages/overture-schema-system/src/overture/schema/system/primitive/geom.py @@ -1,3 +1,36 @@ +""" +Geometry primitive and geometry type constraint. + +Use `Geometry` as the type for fields containing geometry values. Use `GeometryTypeConstraint` if +you need to constrain allowed types of geometries. + +Example +------- +Create a Pydantic model with a geometry field that is constrained to only allow point geometries. + +>>> from typing import Annotated +>>> from pydantic import BaseModel +>>> from overture.schema.system.primitive import float32 +>>> class Peak(BaseModel): +... position: Annotated[ +... Geometry, +... GeometryTypeConstraint(GeometryType.POINT) +... ] +... elevation: float32 +... +>>> fuji = Peak(position=Geometry.from_wkt('POINT(138.7274 35.3606)'), elevation=3_776) + +Non-point geometries will be rejected with a validation error. + +>>> from pydantic import ValidationError +>>> try: +... Peak(position=Geometry.from_wkt('LINESTRING(0 0, 1 1)'), elevation=0) +... except ValidationError as e: +... assert "geometry type not allowed" in str(e) +... print("Validation failed") +Validation failed +""" + from dataclasses import dataclass from enum import Enum from typing import Any @@ -443,7 +476,7 @@ def __get_pydantic_json_schema__( ######################################################################## -def geometry_json_schema( +def _geometry_json_schema( geometry_type: str, coordinates: dict[str, Any] | None = None, geometries: dict[str, Any] | None = None, @@ -469,31 +502,31 @@ def geometry_json_schema( } -_LINE_STRING_GEOMETRY_JSON_SCHEMA = geometry_json_schema( +_LINE_STRING_GEOMETRY_JSON_SCHEMA = _geometry_json_schema( "LineString", coordinates=_LINE_STRING_COORDINATES_JSON_SCHEMA ) -_POINT_GEOMETRY_JSON_SCHEMA = geometry_json_schema( +_POINT_GEOMETRY_JSON_SCHEMA = _geometry_json_schema( "Point", coordinates=_POINT_COORDINATES_JSON_SCHEMA ) -_POLYGON_GEOMETRY_JSON_SCHEMA = geometry_json_schema( +_POLYGON_GEOMETRY_JSON_SCHEMA = _geometry_json_schema( "Polygon", coordinates=_POLYGON_COORDINATES_JSON_SCHEMA ) -_MULTI_LINE_STRING_GEOMETRY_JSON_SCHEMA = geometry_json_schema( +_MULTI_LINE_STRING_GEOMETRY_JSON_SCHEMA = _geometry_json_schema( "MultiLineString", coordinates=_MULTI_LINE_STRING_COORDINATES_JSON_SCHEMA ) -_MULTI_POINT_GEOMETRY_JSON_SCHEMA = geometry_json_schema( +_MULTI_POINT_GEOMETRY_JSON_SCHEMA = _geometry_json_schema( "MultiPoint", coordinates=_MULTI_POINT_COORDINATES_JSON_SCHEMA ) -_MULTI_POLYGON_GEOMETRY_JSON_SCHEMA = geometry_json_schema( +_MULTI_POLYGON_GEOMETRY_JSON_SCHEMA = _geometry_json_schema( "MultiPolygon", coordinates=_MULTI_POLYGON_COORDINATES_JSON_SCHEMA ) -_GEOMETRY_COLLECTION_JSON_SCHEMA = geometry_json_schema( +_GEOMETRY_COLLECTION_JSON_SCHEMA = _geometry_json_schema( "GeometryCollection", geometries={ "oneOf": [ diff --git a/packages/overture-schema-system/src/overture/schema/system/ref/id.py b/packages/overture-schema-system/src/overture/schema/system/ref/id.py index 8e95a9d0b..eb467f142 100644 --- a/packages/overture-schema-system/src/overture/schema/system/ref/id.py +++ b/packages/overture-schema-system/src/overture/schema/system/ref/id.py @@ -1,3 +1,7 @@ +""" +Unique identifiers and models with unique IDs. +""" + from typing import Annotated, NewType from pydantic import BaseModel, Field diff --git a/packages/overture-schema-system/src/overture/schema/system/ref/ref.py b/packages/overture-schema-system/src/overture/schema/system/ref/ref.py index 8906560d4..86c8d19d6 100644 --- a/packages/overture-schema-system/src/overture/schema/system/ref/ref.py +++ b/packages/overture-schema-system/src/overture/schema/system/ref/ref.py @@ -1,3 +1,7 @@ +""" +Relationships and references between related entities. +""" + from dataclasses import dataclass from enum import Enum diff --git a/packages/overture-schema-system/tests/test___json_schema.py b/packages/overture-schema-system/tests/test___json_schema.py index 50ada6ce7..85769b805 100644 --- a/packages/overture-schema-system/tests/test___json_schema.py +++ b/packages/overture-schema-system/tests/test___json_schema.py @@ -6,7 +6,7 @@ from pydantic.json_schema import JsonSchemaValue from overture.schema.system._json_schema import ( - get_static_json_schema, + get_static_json_schema_extra, put_all_of, put_any_of, put_if, @@ -34,7 +34,7 @@ def test_get_static_json_schema_success( config: ConfigDict, expect: JsonSchemaValue ) -> None: - actual = get_static_json_schema(config) + actual = get_static_json_schema_extra(config) assert expect == actual assert actual is config["json_schema_extra"] @@ -45,7 +45,7 @@ def test_get_static_json_schema_error_invalid_type() -> None: ValueError, match='expected value of config\'s "json_schema_extra" key to be a `dict`, but it is a `function`', ): - get_static_json_schema(ConfigDict(json_schema_extra=lambda _: None)) + get_static_json_schema_extra(ConfigDict(json_schema_extra=lambda _: None)) #################################################################################################### diff --git a/packages/overture-schema-system/tests/test_feature.py b/packages/overture-schema-system/tests/test_feature.py index 81efb207c..49dfbe85f 100644 --- a/packages/overture-schema-system/tests/test_feature.py +++ b/packages/overture-schema-system/tests/test_feature.py @@ -1,11 +1,21 @@ import json import re from copy import deepcopy -from typing import Annotated, cast +from enum import Enum +from typing import Annotated, Any, Literal, cast import pytest -from pydantic import ConfigDict, ValidationError, create_model +from pydantic import ( + BaseModel, + ConfigDict, + Field, + Tag, + TypeAdapter, + ValidationError, + create_model, +) from pydantic.json_schema import JsonSchemaValue, JsonValue +from pytest_subtests import SubTests from util import assert_subset from overture.schema.system.feature import Feature, _FieldLevel, _maybe_refactor_schema @@ -25,6 +35,401 @@ ) +class TestFieldDiscriminator: + @pytest.mark.parametrize("field", ["hello", "type", "properties"]) + def test_validation_success_simple(self, field: str, subtests: SubTests) -> None: + """ + Test the discriminated union success case for a discriminator that is a simple string. + + The test parameters include edge case tests for the discriminator field where the + discriminator field is one of the two core GeoJSON properties, "type" and "properties", + that `Feature` allows to be used as fields. + """ + foo_feature_fields: dict[str, Any] = { + field: str, + "foo": Annotated[int | None, Field(default=None)], + } + FooFeature = create_model( + "FooFeature", + __base__=Feature, + **foo_feature_fields, + ) + + bar_model_fields: dict[str, Any] = {field: str, "bar": int} + BarModel = create_model("BarModel", **bar_model_fields) + + baz_feature_fields: dict[str, Any] = { + field: str, + "baz": Annotated[bool | None, Field(default=None)], + } + BazFeature = create_model( + "BazFeature", + __base__=Feature, + **baz_feature_fields, + ) + + tap: TypeAdapter = TypeAdapter( + Annotated[ + Annotated[FooFeature, Tag("foo_feature")] + | Annotated[BarModel, Tag("bar_model")] + | Annotated[BazFeature, Tag("baz_feature")], + Field( + discriminator=Feature.field_discriminator( + field, FooFeature, BarModel, BazFeature + ) + ), + ] + ) + + MODEL_INSTANCES = ( + FooFeature( + **{ + "id": "foo_feature_1", + "geometry": Geometry.from_wkt("POINT(0 1)"), + field: "foo_feature", + } + ), + FooFeature( + **{ + "id": "foo_feature_2", + "geometry": Geometry.from_wkt("LINESTRING(0 0, 1 1)"), + field: "foo_feature", + "foo": 42, + } + ), + FooFeature( + **{ + "bbox": BBox( + 0, + 0, + 2, + 2, + ), + "geometry": Geometry.from_wkt("LINESTRING(0 0, 2 2)"), + field: "foo_feature", + } + ), + BarModel(**{field: "bar_model", "bar": 42}), + BazFeature( + **{ + "id": "baz_feature_1", + "geometry": Geometry.from_wkt("POINT(0 1)"), + field: "baz_feature", + } + ), + BazFeature( + **{ + "id": "baz_feature_2", + "geometry": Geometry.from_wkt("LINESTRING(0 0, 1 1)"), + field: "baz_feature", + "baz": True, + } + ), + BazFeature( + **{ + "id": "baz_feature_3", + "geometry": Geometry.from_wkt("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"), + field: "baz_feature", + "baz": False, + } + ), + ) + + for expect in MODEL_INSTANCES: + with subtests.test(msg="model_validate_json", expect=expect): + json_data = expect.model_dump_json() + actual = tap.validate_json(json_data) + assert expect == actual + + with subtests.test(msg="model_validate from dict", expect=expect): + data = expect.model_dump() + actual = tap.validate_python(data) + assert expect == actual + + with subtests.test(msg="model_validate from model", expect=expect): + actual = tap.validate_python(expect) + assert expect == actual + + def test_validation_success_convert(self, subtests: SubTests) -> None: + """ + Test the discriminated union success case where the discriminator value is of a variety of + types. + """ + + class TestEnum(str, Enum): + VALUE1 = "value1" + VALUE2 = "value2" + + DISCRIMINATORS: tuple[tuple[object, object], ...] = ( + (TestEnum.VALUE1, TestEnum.VALUE2), + (TestEnum.VALUE2, 42), + (0.01, "foo"), + ) + + for value1, value2 in DISCRIMINATORS: + with subtests.test(value1=value1, value2=value2): + Model1 = create_model( + "Model1", __base__=Feature, discriminator_field=Literal[value1] + ) + Model2 = create_model( + "Model2", + discriminator_field=Annotated[ + Literal[value2], "random value", Field(description="something") + ], + ) + + tap: TypeAdapter = TypeAdapter( + Annotated[ + Annotated[Model1, Tag(value1)] | Annotated[Model2, Tag(value2)], # type: ignore[arg-type] + Field( + discriminator=Feature.field_discriminator( + "discriminator_field", + Model1, + Model2, + ) + ), + ] + ) + + model1_expect = Model1( + id="model1", + bbox=BBox(0, 0, 0, 0), + geometry=Geometry.from_wkt("POINT(0 0)"), + discriminator_field=value1, + ) + model2_expect = Model2(discriminator_field=value2) + + with subtests.test( + msg="model_validate_json", + model1_expect=model1_expect, + model2_expect=model2_expect, + ): + model1_actual = tap.validate_json(model1_expect.model_dump_json()) + assert model1_expect == model1_actual + + model2_actual = tap.validate_json(model2_expect.model_dump_json()) + assert model2_expect == model2_actual + + with subtests.test( + msg="model_validate from dict", + model1_expect=model1_expect, + model2_expect=model2_expect, + ): + model1_actual = tap.validate_python(model1_expect.model_dump()) + assert model1_expect == model1_actual + + with subtests.test( + msg="model_validate from model", + model1_expect=model1_expect, + model2_expect=model2_expect, + ): + model1_actual = tap.validate_python(model1_expect) + assert model1_expect == model1_actual + + def test_validation_success_missing_discriminator(self, subtests: SubTests) -> None: + """ + Tests a union of discriminated unions against an input that doesn't contain the + contain the discriminator field of the first union, but does contain the discriminator field + for the second one. + """ + + class Union1ModelA(BaseModel): + mia: Literal["1A"] + + class Union1ModelB(Feature): + mia: Literal["1B"] + + union1 = Annotated[ + Annotated[Union1ModelA, Tag("1A")] | Annotated[Union1ModelB, Tag("1B")], + Field( + discriminator=Feature.field_discriminator( + "mia", Union1ModelA, Union1ModelB + ) + ), + ] + + class Union2ModelA(Feature): + here: Literal["2A"] + + class Union2ModelB(BaseModel): + here: Literal["2B"] + + union2 = Annotated[ + Annotated[Union2ModelA, Tag("2A")] | Annotated[Union2ModelB, Tag("2B")], + Field( + discriminator=Feature.field_discriminator( + "here", Union2ModelA, Union2ModelB + ) + ), + ] + + tap: TypeAdapter = TypeAdapter(union1 | union2) + + expect = Union2ModelB(here="2B") + + with subtests.test(msg="model_validate_json"): + json_data = expect.model_dump_json() + actual = tap.validate_json(json_data) + assert expect == actual + + with subtests.test(msg="model_validate from dict"): + data = expect.model_dump() + actual = tap.validate_python(data) + assert expect == actual + + with subtests.test(msg="model_validate from model", expect=expect): + actual = tap.validate_python(expect) + assert expect == actual + + @pytest.mark.parametrize( + "data", + [ + {"type": "Feature", "geometry": None, "properties": None}, + {"type": "Feature", "geometry": None, "properties": {}}, + {"type": "Feature", "geometry": 42, "properties": {"foo": "baz"}}, + {}, + {"foo": "baz"}, + {"type": "Feature"}, + {"geometry": {"type": "Point", "coordinates": [0, 0]}}, + {"properties": None}, + {"properties": {}}, + {"type": "Feature", "geometry": {}}, + {"type": "Feature", "properties": None}, + {"type": "Feature", "properties": {}}, + {"geometry": {}, "properties": None}, + {"geometry": {}, "properties": {}}, + 42, + ], + ) + def test_validation_error_cant_find_field( + self, + data: object, + ) -> None: + class BarFeature(Feature): + bar: str + + class BarBazFeature(BarFeature): + baz: bool + + class BarModel(BaseModel): + bar: str + + tap: TypeAdapter = TypeAdapter( + Annotated[ + Annotated[BarFeature, Tag("bar")] + | Annotated[BarBazFeature, Tag("bar_baz")] + | Annotated[BarModel, Tag("bar_baz")], + Field( + discriminator=Feature.field_discriminator( + "bar", BarBazFeature, BarFeature, BarModel + ) + ), + ] + ) + + with pytest.raises( + ValidationError, match="Unable to extract tag using discriminator" + ): + tap.validate_json(json.dumps(data)) + + def test_error_field_not_str(self) -> None: + with pytest.raises( + TypeError, match="`field` must be a `str`, but 42 has type `int`" + ): + Feature.field_discriminator(cast(str, 42)) + + @pytest.mark.parametrize("field", ["bbox", "geometry", "id"]) + def test_error_field_name_not_allowed(self, field: str) -> None: + with pytest.raises( + ValueError, match=f"`field` value {repr(field)} is not allowed" + ): + Feature.field_discriminator(field) + + @pytest.mark.parametrize( + "model_classes", + [ + (), + (BaseModel,), + (Feature,), + ], + ) + def test_error_model_classes_length_not_at_least_2( + self, model_classes: tuple[type[BaseModel], ...] + ) -> None: + with pytest.raises( + ValueError, match="`model_classes` must have at least two items" + ): + Feature.field_discriminator("foo", *model_classes) + + @pytest.mark.parametrize( + "model_classes", + [ + (42, BaseModel), + (BaseModel, 42), + (int, BaseModel), + (BaseModel, int), + (Feature, int), + ("foo", BaseModel), + (BaseModel, "bar"), + (str, BaseModel), + (BaseModel, str), + (Feature, dict), + (Feature, BaseModel, {}), + ], + ) + def test_error_non_model_classes(self, model_classes: tuple[object, ...]) -> None: + with pytest.raises( + TypeError, match="`model_classes` contains at least one non-model class" + ): + Feature.field_discriminator( + "foo", *[cast(type[BaseModel], x) for x in model_classes] + ) + + @pytest.mark.parametrize( + "model_classes", + [ + (BaseModel, Feature), + (Feature, BaseModel), + ( + create_model("Foo", __base__=Feature, foo=int), + create_model("Bar", __base__=Feature, bar=int), + ), + ( + create_model("Bar", __base__=Feature, bar=str), + create_model("Foo", __base__=Feature, foo=str), + ), + ( + create_model("Foo", __base__=BaseModel, foo=str), + create_model("FooBar", __base__=Feature, foo=str, bar=int), + BaseModel, + ), + ], + ) + def test_error_model_class_missing_field( + self, model_classes: tuple[type[BaseModel], ...] + ) -> None: + with pytest.raises( + TypeError, + match="`model_classes` contains at least one model class that does not have a field named 'foo'", + ): + Feature.field_discriminator("foo", *model_classes) + + def test_error_no_feature_classes(self) -> None: + class BarBaz(BaseModel): + bar: int + baz: bool + + class BarQux(BaseModel): + bar: int + qux: str + + with pytest.raises( + TypeError, + match="`model_classes` does not contain any subclasses of `Feature`", + ): + Feature.field_discriminator("bar", BarBaz, BarQux) + + class TestSerializeModel: @pytest.mark.parametrize( "feature,expect", @@ -180,9 +585,9 @@ class TestValidateModel: "geometry": {"type": "Point", "coordinates": [1.0, 2.0]}, "properties": {}, }, - Feature( # type: ignore[call-arg] + Feature( bbox=BBox(0, 1, 0, 2), geometry=Geometry.from_wkt("POINT(1 2)") - ), + ), # type: ignore[call-arg] ), ( { @@ -205,6 +610,12 @@ def test_simple_json(self, json_dict: dict[str, object], expect: Feature) -> Non assert expect == actual + def test_simple_python_feature(self) -> None: + expect = Feature(geometry=Geometry.from_wkt("POINT(1 2)")) # type: ignore[call-arg] + actual = Feature.model_validate(expect) + + assert expect == actual + @pytest.mark.parametrize( "python_dict,expect", [ @@ -244,7 +655,7 @@ def test_simple_json(self, json_dict: dict[str, object], expect: Feature) -> Non ), ], ) - def test_simple_python( + def test_simple_python_dict( self, python_dict: dict[str, object], expect: Feature ) -> None: actual = Feature.model_validate(python_dict) @@ -310,14 +721,14 @@ class SubFeature(Feature): assert extra == sub_feature.model_extra - def test_error_data_not_dict(self) -> None: - with pytest.raises( - TypeError, match="feature data must be a `dict`, but 'foo' is a `str`" - ): + def test_error_python_data_not_feature_or_dict(self) -> None: + with pytest.raises(ValidationError): Feature.model_validate("foo") + def test_error_json_data_not_dict(self) -> None: with pytest.raises( - TypeError, match="feature data must be a `dict`, but 'bar' is a `str`" + TypeError, + match="feature data must be a `dict` when validating JSON, but 'bar' is a `str`", ): Feature.model_validate_json('"bar"') diff --git a/packages/overture-schema-core/tests/test_json_schema.py b/packages/overture-schema-system/tests/test_json_schema.py similarity index 82% rename from packages/overture-schema-core/tests/test_json_schema.py rename to packages/overture-schema-system/tests/test_json_schema.py index b9fcd202d..f54801006 100644 --- a/packages/overture-schema-core/tests/test_json_schema.py +++ b/packages/overture-schema-system/tests/test_json_schema.py @@ -1,11 +1,10 @@ -from overture.schema.core.json_schema import EnhancedJsonSchemaGenerator from pydantic import BaseModel -from pydantic.json_schema import GenerateJsonSchema +from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue +from overture.schema.system.json_schema import GenerateOmitNullableOptionalJsonSchema -class TestEnhancedJsonSchemaGenerator: - """Test the EnhancedJsonSchemaGenerator class.""" +class TestenerateOmitOptionalJsonSchema: def test_nullable_with_none_default_becomes_optional(self) -> None: """Test that X | None = None becomes optional without default.""" @@ -14,7 +13,7 @@ class TestModel(BaseModel): required_field: str schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) # The nullable_field should not appear in required fields @@ -38,7 +37,7 @@ class TestModel(BaseModel): nullable_field: str | None = "default_value" schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) properties = schema["properties"] @@ -58,7 +57,7 @@ class TestModel(BaseModel): bool_field: bool = True schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) properties = schema["properties"] @@ -72,19 +71,29 @@ def test_required_fields_unchanged(self) -> None: class TestModel(BaseModel): required_str: str - required_int: int + required_int: int | None optional_with_none: str | None = None schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) assert schema["required"] == ["required_str", "required_int"] + def strip(json_schema: JsonSchemaValue, *keys: str) -> JsonSchemaValue: + for k in keys: + del json_schema[k] + return json_schema + properties = schema["properties"] - assert "default" not in properties["required_str"] - assert "default" not in properties["required_int"] - assert "default" not in properties["optional_with_none"] + assert {"type": "string"} == strip(properties["required_str"], "title") + assert { + "anyOf": [ + {"type": "integer"}, + {"type": "null"}, + ] + } == strip(properties["required_int"], "title") + assert {"type": "string"} == strip(properties["optional_with_none"], "title") def test_comparison_with_standard_generator(self) -> None: """Test behavior differs from standard GenerateJsonSchema.""" @@ -99,7 +108,7 @@ class TestModel(BaseModel): # Our custom generator schema custom_schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) # Standard should have default: null @@ -123,7 +132,7 @@ class TestModel(BaseModel): required_field: str schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) assert schema["required"] == ["required_field"] @@ -152,7 +161,7 @@ class TestModel(BaseModel): opt_nested: NestedModel | None = None schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) properties = schema["properties"] @@ -177,7 +186,7 @@ class TestModel(BaseModel): union_field: str | int | None = None schema = TestModel.model_json_schema( - schema_generator=EnhancedJsonSchemaGenerator + schema_generator=GenerateOmitNullableOptionalJsonSchema ) properties = schema["properties"] diff --git a/packages/overture-schema-transportation-theme/src/overture/schema/transportation/enums.py b/packages/overture-schema-transportation-theme/src/overture/schema/transportation/enums.py index 0a4fb0af1..c5cf91720 100644 --- a/packages/overture-schema-transportation-theme/src/overture/schema/transportation/enums.py +++ b/packages/overture-schema-transportation-theme/src/overture/schema/transportation/enums.py @@ -12,8 +12,10 @@ class Subtype(str, Enum): class DestinationLabelType(str, Enum): - """Indicates what special symbol/icon is present on a signpost, visible as road - marking or similar.""" + """ + Indicates what special symbol/icon is present on a signpost, visible as road marking or + similar. + """ STREET = "street" COUNTRY = "country" @@ -63,8 +65,10 @@ class RailClass(str, Enum): class DestinationSignSymbol(str, Enum): - """Indicates what special symbol/icon is present on a signpost, visible as road - marking or similar.""" + """ + Indicates what special symbol/icon is present on a signpost, visible as road marking or + similar. + """ MOTORWAY = "motorway" AIRPORT = "airport" diff --git a/packages/overture-schema-transportation-theme/src/overture/schema/transportation/models.py b/packages/overture-schema-transportation-theme/src/overture/schema/transportation/models.py index be291ea93..246d2efea 100644 --- a/packages/overture-schema-transportation-theme/src/overture/schema/transportation/models.py +++ b/packages/overture-schema-transportation-theme/src/overture/schema/transportation/models.py @@ -45,8 +45,10 @@ def _connector_type() -> type[OvertureFeature]: @no_extra_fields @scoped(Scope.GEOMETRIC_POSITION) class ConnectorReference(BaseModel): - """Contains the GERS ID and relative position between 0 and 1 of a connector feature - along the segment.""" + """ + Contains the GERS ID and relative position between 0 and 1 of a connector feature along the + segment. + """ model_config = ConfigDict(frozen=True) @@ -150,8 +152,7 @@ class RouteReference(BaseModel): @no_extra_fields class Speed(BaseModel): - """A speed value, i.e. a certain number of distance units travelled per unit - time.""" + """A speed value, i.e. a certain number of distance units travelled per unit time.""" model_config = ConfigDict(frozen=True) @@ -204,7 +205,6 @@ class SpeedLimitRule(BaseModel): is_max_speed_variable: Annotated[ bool | None, Field( - default=False, description="Indicates a variable speed corridor", strict=True, ), @@ -281,8 +281,10 @@ class RailFlagRule(BaseModel): @no_extra_fields @scoped(Scope.GEOMETRIC_RANGE) class LevelRule(BaseModel): - """A single level rule defining the Z-order, i.e. stacking order, applicable within - a given scope on the road segment.""" + """ + A single level rule defining the Z-order, i.e. stacking order, applicable within a given scope + on the road segment. + """ # Required diff --git a/packages/overture-schema-transportation-theme/src/overture/schema/transportation/segment/models.py b/packages/overture-schema-transportation-theme/src/overture/schema/transportation/segment/models.py index d890b0d0a..4d3d0f972 100644 --- a/packages/overture-schema-transportation-theme/src/overture/schema/transportation/segment/models.py +++ b/packages/overture-schema-transportation-theme/src/overture/schema/transportation/segment/models.py @@ -2,7 +2,7 @@ from typing import Annotated, Literal -from pydantic import ConfigDict, Field +from pydantic import ConfigDict, Field, Tag from overture.schema.core import ( OvertureFeature, @@ -10,6 +10,7 @@ from overture.schema.core.names import ( Named, ) +from overture.schema.system.feature import Feature from overture.schema.system.field_constraint import UniqueItemsConstraint from overture.schema.system.primitive import ( Geometry, @@ -63,7 +64,6 @@ class TransportationSegment( connectors: Annotated[ list[ConnectorReference] | None, Field( - default=[], min_length=2, description="List of connectors which this segment is physically connected to and their relative location. Each connector is a possible routing decision point, meaning it defines a place along the segment in which there is possibility to transition to other segments which share the same connector.", ), @@ -127,7 +127,14 @@ class WaterSegment(TransportationSegment): Segment = Annotated[ - RoadSegment | RailSegment | WaterSegment, Field(discriminator="subtype") + Annotated[RoadSegment, Tag(Subtype.ROAD)] + | Annotated[RailSegment, Tag(Subtype.RAIL)] + | Annotated[WaterSegment, Tag(Subtype.WATER)], + Field( + discriminator=Feature.field_discriminator( + "subtype", RoadSegment, RailSegment, WaterSegment + ) + ), ] # Explicitly assign docstring to the Segment type alias diff --git a/packages/overture-schema-transportation-theme/tests/connector_baseline_schema.json b/packages/overture-schema-transportation-theme/tests/connector_baseline_schema.json index 7a90fb0a8..b633d1219 100644 --- a/packages/overture-schema-transportation-theme/tests/connector_baseline_schema.json +++ b/packages/overture-schema-transportation-theme/tests/connector_baseline_schema.json @@ -1,11 +1,11 @@ { "$defs": { - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -17,34 +17,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -54,7 +55,7 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" } }, @@ -124,9 +125,9 @@ }, "properties": { "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", diff --git a/packages/overture-schema-transportation-theme/tests/segment_baseline_schema.json b/packages/overture-schema-transportation-theme/tests/segment_baseline_schema.json index 96d58fdb4..96d10a548 100644 --- a/packages/overture-schema-transportation-theme/tests/segment_baseline_schema.json +++ b/packages/overture-schema-transportation-theme/tests/segment_baseline_schema.json @@ -39,7 +39,7 @@ }, "ConnectorReference": { "additionalProperties": false, - "description": "Contains the GERS ID and relative position between 0 and 1 of a connector feature\nalong the segment.", + "description": "Contains the GERS ID and relative position between 0 and 1 of a connector feature along the\nsegment.", "properties": { "at": { "description": "The linearly-referenced position on the geometry, specified as a percentage displacement from the start of the geometry, that the containing ConnectorReference applies to.", @@ -63,7 +63,7 @@ "type": "object" }, "DestinationLabelType": { - "description": "Indicates what special symbol/icon is present on a signpost, visible as road\nmarking or similar.", + "description": "Indicates what special symbol/icon is present on a signpost, visible as road marking or\nsimilar.", "enum": [ "street", "country", @@ -169,7 +169,7 @@ "type": "object" }, "DestinationSignSymbol": { - "description": "Indicates what special symbol/icon is present on a signpost, visible as road\nmarking or similar.", + "description": "Indicates what special symbol/icon is present on a signpost, visible as road marking or\nsimilar.", "enum": [ "motorway", "airport", @@ -220,7 +220,7 @@ }, "LevelRule": { "additionalProperties": false, - "description": "A single level rule defining the Z-order, i.e. stacking order, applicable within\na given scope on the road segment.", + "description": "A single level rule defining the Z-order, i.e. stacking order, applicable within a given scope\non the road segment.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing LevelRule applies to.", @@ -235,20 +235,22 @@ "type": "array" }, "value": { - "default": 0, "description": "Z-order of the feature where 0 is visual level", - "maximum": 2147483647, - "minimum": -2147483648, + "maximum": 32767, + "minimum": -32768, "title": "Value", "type": "integer" } }, + "required": [ + "value" + ], "title": "LevelRule", "type": "object" }, "NameRule": { "additionalProperties": false, - "description": "Name rule with variant and language specification.", + "description": "A rule that can be evaluated to determine the name in advanced scenarios.\n\nName rules are used for cases where the primary name is not sufficient; the common name is not\nthe right fit for the use case and another variant is needed; or where the name only applies in\ncertain specific circumstances.\n\nExamples might include:\n- An official, alternate, or short name.\n- A name that only applies to part of a linear path like a road segment (geometric range\n scoping).\n- A name that only applies to the left or right side of a linear path like a road segment (side\n scoping).\n- A name that is only accepted by some political perspectives.", "properties": { "between": { "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing NameRule applies to.", @@ -263,7 +265,7 @@ "type": "array" }, "language": { - "description": "IETF BCP-47 language tag", + "description": "The language in which the name `value` is specified, if known, as an IETF BCP 47\nlanguage tag.", "pattern": "^(?:(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}?)|(?:[A-Za-z]{4,8}))(?:-[A-Za-z]{4})?(?:-[A-Za-z]{2}|[0-9]{3})?(?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(?:-[A-WY-Za-wy-z0-9](?:-[A-Za-z0-9]{2,8})+)*$", "title": "Language", "type": "string" @@ -277,14 +279,15 @@ "description": "The side, either left or right, that the containing NameRule applies to." }, "value": { - "description": "String with no leading/trailing whitespace", + "description": "The actual name value.", "minLength": 1, "pattern": "^(\\S.*)?\\S$", "title": "Value", "type": "string" }, "variant": { - "$ref": "#/$defs/NameVariant" + "$ref": "#/$defs/NameVariant", + "description": "The name variant for this name rule." } }, "required": [ @@ -295,6 +298,7 @@ "type": "object" }, "NameVariant": { + "description": "Name variant used in a `NameRule`.", "enum": [ "common", "official", @@ -425,7 +429,7 @@ "type": "object" }, "PurposeOfUse": { - "description": "Reason why a person or entity travelling on the transportation network is using a\nparticular location.", + "description": "Reason why a person or entity travelling on the transportation network is using a particular\nlocation.", "enum": [ "as_customer", "at_destination", @@ -622,9 +626,9 @@ "type": "array" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", @@ -686,7 +690,7 @@ "type": "object" }, "RecognizedStatus": { - "description": "Status of the person or entity travelling as recognized by authorities\ncontrolling the particular location.", + "description": "Status of the person or entity travelling as recognized by authorities controlling the particular\nlocation.", "enum": [ "as_permitted", "as_private", @@ -917,9 +921,9 @@ "type": "array" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", @@ -1106,12 +1110,12 @@ "title": "Side", "type": "string" }, - "SourcePropertyItem": { + "SourceItem": { "additionalProperties": false, - "description": "An object storing the source for a specified property.\n\nThe property is a reference to the property element within this Feature, and will be\nreferenced using JSON Pointer Notation RFC 6901 (\nhttps://datatracker.ietf.org/doc/rfc6901/).\nThe source dataset for that referenced property will be specified in the overture list of approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Specifies the source of the data used for a feature or one of its properties.", "properties": { "between": { - "description": "Linear reference range [start, end] where 0.0 <= start < end <= 1.0", + "description": "The linearly-referenced sub-segment of the geometry, specified as a range (pair) of percentage displacements from the start of the geometry, that the containing SourceItem applies to.", "items": { "maximum": 1.0, "minimum": 0.0, @@ -1123,34 +1127,35 @@ "type": "array" }, "confidence": { - "description": "Confidence score between 0.0 and 1.0", + "description": "Confidence value from the source dataset.\n\nThis is a value between 0.0 and 1.0 and is particularly relevant for ML-derived data.", "maximum": 1.0, "minimum": 0.0, "title": "Confidence", "type": "number" }, "dataset": { + "description": "Name of the dataset where the source data can be found.", "title": "Dataset", "type": "string" }, "license": { - "description": "License name. This should be a valid SPDX license identifier when available. If the license is NULL, contact the data provider for more license information.", + "description": "Source data license name.\n\nThis should be a valid SPDX license identifier when available.\n\nIf omitted, contact the data provider for more license information.", "pattern": "^(\\S.*)?\\S$", "title": "License", "type": "string" }, "property": { - "description": "JSON Pointer (RFC 6901)", + "description": "A JSON Pointer identifying the property (field) that this source information applies to.\n\nThe root document value `\"\"` indicates that this source information applies to the\nentire feature, excepting properties (fields) for which a dedicated source information\nrecord exists.\n\nAny other JSON Pointer apart from `\"\"` indicates that this source record provides\ndedicated source information for the property at the path in the JSON Pointer. As an\nexample, the value `\"/names/common/en\"` indicates that the source information applies to\nthe English common name of a named feature, while the value `\"/geometry\"` indicates that\nit applies to the feature geometry.", "title": "Property", "type": "string" }, "record_id": { - "description": "Refers to the specific record within the dataset that was used.", + "description": "Identifies the specific record within the source dataset where the source data can\nbe found.\n\nThe format of record identifiers is dataset-specific.", "title": "Record Id", "type": "string" }, "update_time": { - "description": "Timestamp when the feature was last updated", + "description": "Last update time of the source data record.", "format": "date-time", "title": "Update Time", "type": "string" @@ -1160,12 +1165,12 @@ "property", "dataset" ], - "title": "SourcePropertyItem", + "title": "SourceItem", "type": "object" }, "Speed": { "additionalProperties": false, - "description": "A speed value, i.e. a certain number of distance units travelled per unit\ntime.", + "description": "A speed value, i.e. a certain number of distance units travelled per unit time.", "properties": { "unit": { "$ref": "#/$defs/SpeedUnit" @@ -1369,7 +1374,7 @@ "description": "Height unit in which `value` is expressed" }, "value": { - "decription": "Vehicle height selection threshold in the given `unit`", + "description": "Vehicle height selection threshold in the given `unit`", "minimum": 0, "title": "Value", "type": "number" @@ -1603,9 +1608,9 @@ "type": "array" }, "sources": { - "description": "The array of source information for the properties of a given feature, with each entry being a source object which lists the property in JSON Pointer notation and the dataset approved sources from the Overture Data Working Group that contains the relevant metadata for that dataset including license source organization.", + "description": "Information about the source data used to assemble the feature.", "items": { - "$ref": "#/$defs/SourcePropertyItem" + "$ref": "#/$defs/SourceItem" }, "minItems": 1, "title": "Sources", @@ -2040,14 +2045,6 @@ "type": "object" } }, - "discriminator": { - "mapping": { - "rail": "#/$defs/RailSegment", - "road": "#/$defs/RoadSegment", - "water": "#/$defs/WaterSegment" - }, - "propertyName": "subtype" - }, "oneOf": [ { "$ref": "#/$defs/RoadSegment" diff --git a/packages/overture-schema-transportation-theme/tests/test_connector_json_schema_baseline.py b/packages/overture-schema-transportation-theme/tests/test_connector_json_schema_baseline.py index 7f518fd20..132ac9977 100644 --- a/packages/overture-schema-transportation-theme/tests/test_connector_json_schema_baseline.py +++ b/packages/overture-schema-transportation-theme/tests/test_connector_json_schema_baseline.py @@ -3,7 +3,7 @@ import json import os -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema from overture.schema.transportation import Connector diff --git a/packages/overture-schema-transportation-theme/tests/test_segment_json_schema_baseline.py b/packages/overture-schema-transportation-theme/tests/test_segment_json_schema_baseline.py index 336907467..529ced367 100644 --- a/packages/overture-schema-transportation-theme/tests/test_segment_json_schema_baseline.py +++ b/packages/overture-schema-transportation-theme/tests/test_segment_json_schema_baseline.py @@ -3,7 +3,7 @@ import json import os -from overture.schema.core import json_schema +from overture.schema.system.json_schema import json_schema from overture.schema.transportation import Segment diff --git a/packages/overture-schema/README.md b/packages/overture-schema/README.md index 3f88a46a0..16eb1b4b7 100644 --- a/packages/overture-schema/README.md +++ b/packages/overture-schema/README.md @@ -68,15 +68,12 @@ from overture.schema import ( The package also exports several utility functions: ```python -from overture.schema import parse, parse_feature, discover_models, json_schema +from overture.schema import parse, discover_models, json_schema from overture.schema import Building # Parse any Overture feature (auto-discovers all registered models) -validated_feature = parse(feature_data, mode="json") # Returns GeoJSON format -validated_feature = parse(feature_data, mode="python") # Returns flat format - -# Parse with a specific model type -parsed_building = parse_feature(building_data, Building) +validated_feature = parse(feature_data, mode="json") # Parses GeoJSON format +validated_feature = parse(feature_data, mode="python") # Parses flat format # Discover all registered models programmatically all_models = discover_models() diff --git a/packages/overture-schema/src/overture/schema/__init__.py b/packages/overture-schema/src/overture/schema/__init__.py index aa91d3f7a..1f75ea511 100644 --- a/packages/overture-schema/src/overture/schema/__init__.py +++ b/packages/overture-schema/src/overture/schema/__init__.py @@ -1,76 +1,171 @@ __path__ = __import__("pkgutil").extend_path(__path__, __name__) +from collections.abc import Generator from functools import reduce from operator import or_ -from typing import TYPE_CHECKING, Annotated, Any +from types import UnionType +from typing import Annotated, Any, Literal, cast, get_args, get_origin -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, Tag, TypeAdapter -from overture.schema.core import parse_feature +from overture.schema.core import OvertureFeature from overture.schema.core.discovery import discover_models -from overture.schema.core.json_schema import json_schema +from overture.schema.system.feature import Feature -def parse(feature: dict[str, Any], mode: str = "json") -> dict[str, Any] | None: - """Parse and validate a feature using the union of all available models. +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 + ---------- + 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)) + - Args: - feature: Feature data (GeoJSON or flattened format) - mode: Output mode - "json" for GeoJSON format, "python" for flattened format +__all__ = [ + "validate", + "validate_json", +] - Returns: - Parsed feature in the specified format - Uses the discovery mechanism to find all registered models and validates - the feature against the union of all available models. +def _union_type_adapter() -> TypeAdapter: + """ + Return a Pydantic type adapter that can validate the union of all models discovered using entry + points. """ - # Discover all registered models via entry points models = discover_models() if not models: - raise ValueError("No registered models found via entry points") + 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) + - if TYPE_CHECKING: - # For type checking, use Any to avoid mypy errors with dynamic types - model_union = Any +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: - # Filter out BaseModel types without a 'type' field; they can't be discriminated - # This is an Overture-specific optimization, as our core models all have 'type' - discriminated_models = [] - non_discriminated_models = [] - - for model in models.values(): - if ( - isinstance(model, type) - and issubclass(model, BaseModel) - and "type" not in model.model_fields - ): - non_discriminated_models.append(model) - else: - # Include union types and models with 'type' field - discriminated_models.append(model) - - assert discriminated_models or non_discriminated_models - - discriminated_union = None - if discriminated_models: - discriminated_union = Annotated[ - reduce(or_, discriminated_models), Field(discriminator="type") - ] - - non_discriminated_union = 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 - else: - model_union = non_discriminated_union - - return parse_feature(feature, model_union, mode) + 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 + ) -__all__ = [ - "parse", - "parse_feature", - "json_schema", -] +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(Annotated)[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 diff --git a/packages/overture-schema/tests/test_schema_validation.py b/packages/overture-schema/tests/test_schema_validation.py index 82f892edf..b3ec4399f 100644 --- a/packages/overture-schema/tests/test_schema_validation.py +++ b/packages/overture-schema/tests/test_schema_validation.py @@ -1,11 +1,12 @@ +import json from collections.abc import Generator from pathlib import Path from typing import Any import pytest import yaml -from deepdiff import DeepDiff -from overture.schema import parse +from overture.schema import validate, validate_json +from pydantic import ValidationError from yamlcore import CoreLoader # type: ignore # Top-level constants for paths @@ -14,12 +15,11 @@ COUNTEREXAMPLES_DIR = PROJECT_ROOT / "reference" / "counterexamples" -def load_feature(file_path: str) -> dict[str, Any]: +def load_example_file(file_path: str) -> Any: """Load a feature from JSON or YAML file and return flattened/tabular format.""" with open(file_path, encoding="utf-8") as f: # use a YAML-1.2-compliant (which dropped support for yes/no boolean values) Loader - feature = yaml.load(f, Loader=CoreLoader) - return create_flat_variant(feature) + return yaml.load(f, Loader=CoreLoader) def create_flat_variant(feature: dict[str, Any]) -> dict[str, Any]: @@ -38,58 +38,6 @@ def create_flat_variant(feature: dict[str, Any]) -> dict[str, Any]: return flat_feature -def convert_to_geojson_format(flattened_feature: dict[str, Any]) -> dict[str, Any]: - """Convert flattened feature to GeoJSON format for comparison.""" - return { - "type": "Feature", - "id": flattened_feature.get("id", None), - "geometry": flattened_feature.get("geometry", None), - "properties": { - k: v for k, v in flattened_feature.items() if k not in ["id", "geometry"] - }, - } - - -def deep_compare_dicts( - original: dict[str, Any], parsed: dict[str, Any] -) -> tuple[bool, str]: - """Perform deep comparison between original and parsed dictionaries. - - Returns (is_equal, differences_report). - """ - diff = DeepDiff(original, parsed, ignore_order=True, significant_digits=15) - - if not diff: - return True, "" - - # Format differences for readable output - differences = [] - - if "values_changed" in diff: - differences.append("Value changes:") - for key, change in diff["values_changed"].items(): - differences.append( - f" {key}: {change['old_value']} -> {change['new_value']}" - ) - - if "dictionary_item_added" in diff: - differences.append("Added items:") - for item in diff["dictionary_item_added"]: - differences.append(f" {item}") - - if "dictionary_item_removed" in diff: - differences.append("Removed items:") - for item in diff["dictionary_item_removed"]: - differences.append(f" {item}") - - if "type_changes" in diff: - differences.append("Type changes:") - for key, change in diff["type_changes"].items(): - differences.append(f" {key}: {change['old_type']} -> {change['new_type']}") - - return False, "\n".join(differences) - - def walk_directory(directory: Path) -> Generator[Path, None, None]: """Walk directory and yield all relevant files, including those in .disabled directories.""" @@ -138,7 +86,7 @@ def group_files_by_directory(files: list[Path], base_dir: Path) -> dict[str, Any def create_test_cases( - group: dict[str, Any], base_dir: Path, is_counterexample: bool = False + group: dict[str, Any], base_dir: Path ) -> list[tuple[str, str, bool]]: """Create test cases from grouped files.""" test_cases = [] @@ -173,9 +121,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: if EXAMPLES_DIR.exists(): example_files = list(walk_directory(EXAMPLES_DIR)) grouped_examples = group_files_by_directory(example_files, EXAMPLES_DIR) - test_cases = create_test_cases( - grouped_examples, EXAMPLES_DIR, is_counterexample=False - ) + test_cases = create_test_cases(grouped_examples, EXAMPLES_DIR) # Create parameter values with marks for enabled/disabled tests param_values = [] @@ -201,9 +147,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: grouped_counterexamples = group_files_by_directory( counterexample_files, COUNTEREXAMPLES_DIR ) - test_cases = create_test_cases( - grouped_counterexamples, COUNTEREXAMPLES_DIR, is_counterexample=True - ) + test_cases = create_test_cases(grouped_counterexamples, COUNTEREXAMPLES_DIR) # Create parameter values with marks for enabled/disabled tests param_values = [] @@ -223,84 +167,82 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: metafunc.parametrize("counterexample_file", param_values) -def test_example_validation_geojson(example_file: str) -> None: - """Test that examples pass validation with GeoJSON input format.""" - feature = load_feature(example_file) - - if "geometry" not in feature: - pytest.skip("Example does not have a geometry field") - - test_feature = convert_to_geojson_format(feature) +def test_example_validation_json(example_file: str) -> None: + """ + Test that examples pass validation with JSON input format. This will test GeoJSON parsing for + examples based on GeoJSON features. + """ + json_input = load_example_file(example_file) try: - parsed_feature = parse(test_feature) + model = validate_json(json.dumps(json_input)) except Exception as e: raise pytest.fail.Exception( - f"Example failed validation (GeoJSON): {example_file}" + f"Example failed validation (JSON): {example_file}" ) from e - # If validation passed and we have a parsed feature, compare with GeoJSON format - if parsed_feature is not None: - # Parsed feature should be in GeoJSON format, so compare directly - is_equal, diff_report = deep_compare_dicts(test_feature, parsed_feature) - assert is_equal, ( - f"Parsed feature differs from original (geojson): {example_file}\n" - f"Differences:\n{diff_report}" - ) + # If validation passed and we have a parsed feature, serialize to JSON and compare with the + # original JSON. + json_dump = model.model_dump(exclude_unset=True, by_alias=True, mode="json") + assert json_dump == json_input, ( + f"Dumped model JSON differs from original: {example_file}" + ) def test_example_validation_flat(example_file: str) -> None: - """Test that examples pass validation with flat/Parquet-style input.""" - flat_feature = load_feature(example_file) # Load as flat (authoritative) - test_feature = flat_feature # Use flat format directly + """ + Test that examples pass validation with Python input format, which should have the same + structure as Parquet. + """ + json_input = load_example_file(example_file) + flat_input = create_flat_variant(json_input) try: - parsed_feature = parse(test_feature) + model = validate(flat_input) except Exception as e: raise pytest.fail.Exception( - f"Example failed validation (flat): {example_file}" + f"example failed validation (Python): {example_file}" ) from e - # If validation passed and we have a parsed feature, compare with GeoJSON format - if parsed_feature is not None and "geometry" in flat_feature: - # Parsed feature should be in GeoJSON format, so compare with GeoJSON variant - expected_geojson = convert_to_geojson_format(flat_feature) - is_equal, diff_report = deep_compare_dicts(expected_geojson, parsed_feature) - assert is_equal, ( - f"Parsed feature differs from expected (flat): {example_file}\n" - f"Differences:\n{diff_report}" - ) + # If validation passed and we have a parsed feature, serialize back to both the flattened mode + # and JSON and verify no differences. + json_dump = model.model_dump(exclude_unset=True, by_alias=True, mode="json") + assert json_dump == json_input, ( + f"Dumped model JSON differs from original: {example_file}" + ) -def test_counterexample_validation_geojson(counterexample_file: str) -> None: - """Test that counterexamples fail validation with GeoJSON input format.""" - flat_feature = load_feature(counterexample_file) # Load as flat (authoritative) - test_feature = convert_to_geojson_format(flat_feature) # Convert to GeoJSON format +def test_counterexample_validation_json(counterexample_file: str) -> None: + """ + Test that counterexamples fail validation with JSON input format. This will test GeoJSON + validation for counterexamples based on GeoJSON features. + """ + json_input = load_example_file(counterexample_file) is_valid = False try: - parse(test_feature) + validate_json(json.dumps(json_input)) is_valid = True - except Exception: + except ValidationError: pass assert not is_valid, ( - f"Counterexample should have failed validation (geojson): {counterexample_file}" + f"Counterexample should have failed validation (JSON): {counterexample_file}" ) def test_counterexample_validation_flat(counterexample_file: str) -> None: """Test that counterexamples fail validation with flat input format.""" - flat_feature = load_feature(counterexample_file) # Load as flat (authoritative) - test_feature = flat_feature # Use flat format directly + json_input = load_example_file(counterexample_file) + flat_input = create_flat_variant(json_input) is_valid = False try: - parse(test_feature) + validate(flat_input) is_valid = True - except Exception: + except ValidationError: pass assert not is_valid, ( - f"Counterexample should have failed validation (flat): {counterexample_file}" + f"Counterexample should have failed validation (Python): {counterexample_file}" ) diff --git a/pyproject.toml b/pyproject.toml index df5a702d3..92201b713 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ warn_unused_configs = true [dependency-groups] dev = [ - "docformatter>=1.7.7", "mypy>=1.17.0", "pdoc>=15.0.4", "pydocstyle>=6.3.0", @@ -69,6 +68,3 @@ pythonpath = [ "packages/overture-schema-transportation-theme/tests", "packages/overture-schema/tests", ] - -[tool.docformatter] -black = true diff --git a/reference/examples/annex/sources/basic-sources.yaml b/reference/examples/annex/sources/basic-sources.yaml index 01c7b6520..2c6d10290 100644 --- a/reference/examples/annex/sources/basic-sources.yaml +++ b/reference/examples/annex/sources/basic-sources.yaml @@ -15,7 +15,7 @@ datasets: - -122.45 - 45.65 inception_date: '2022-08-01' - url: https://example.gov + url: https://example.gov/ url_archived: https://web.archive.org/web/2025/https://example.gov data_download_url: - https://data.example.gov/downloads/address_points.geojson @@ -52,7 +52,7 @@ datasets: - 45.38 - -122.37 - 45.72 - url: https://parcels.example.gov + url: https://parcels.example.gov/ data_download_url: - https://parcels.example.gov/downloads/parcels.gpkg countries: diff --git a/uv.lock b/uv.lock index eb396415a..53e6c2d46 100644 --- a/uv.lock +++ b/uv.lock @@ -39,95 +39,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -262,19 +173,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] -[[package]] -name = "docformatter" -version = "1.7.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "charset-normalizer" }, - { name = "untokenize" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/7b/ee08cb5fe2627ed0b6f0cc4a1c6be6c9c71de5a3e9785de8174273fc3128/docformatter-1.7.7.tar.gz", hash = "sha256:ea0e1e8867e5af468dfc3f9e947b92230a55be9ec17cd1609556387bffac7978", size = 26587, upload-time = "2025-05-11T04:54:04.356Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl", hash = "sha256:7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834", size = 33525, upload-time = "2025-05-11T04:54:03.353Z" }, -] - [[package]] name = "email-validator" version = "2.3.0" @@ -849,7 +747,6 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ - { name = "docformatter" }, { name = "mypy" }, { name = "pdoc" }, { name = "pydocstyle" }, @@ -862,7 +759,6 @@ dev = [ [package.metadata.requires-dev] dev = [ - { name = "docformatter", specifier = ">=1.7.7" }, { name = "mypy", specifier = ">=1.17.0" }, { name = "pdoc", specifier = ">=15.0.4" }, { name = "pydocstyle", specifier = ">=6.3.0" }, @@ -1380,12 +1276,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "untokenize" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2", size = 3099, upload-time = "2014-02-08T16:30:40.631Z" } - [[package]] name = "yamlcore" version = "0.0.4"