From 5757ff9d03349262b34b93562078cb541373adeb Mon Sep 17 00:00:00 2001 From: Michael Barroco Date: Tue, 30 Sep 2025 17:33:15 +0200 Subject: [PATCH 1/2] [ed318] validation --- .gitmodules | 3 ++ geospatial-utils/Dockerfile | 1 + geospatial-utils/main.py | 12 ++++++-- geospatial-utils/validate.py | 57 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 7 +++-- schemas/ED-318 | 1 + uv.lock | 16 ++++++++++ 7 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 .gitmodules create mode 100644 geospatial-utils/validate.py create mode 160000 schemas/ED-318 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..76d8fd7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "interfaces/ED-318"] + path = schemas/ED-318 + url = https://github.com/UASGeoZones/ED-318.git diff --git a/geospatial-utils/Dockerfile b/geospatial-utils/Dockerfile index 2af6fd7..5878dc8 100644 --- a/geospatial-utils/Dockerfile +++ b/geospatial-utils/Dockerfile @@ -50,6 +50,7 @@ WORKDIR /app/geospatial-utils # Add core content from repo ADD ./geospatial-utils /app/geospatial-utils +ADD ./schemas /app/schemas # Discover `geospatial-utils` module in Python ENV PYTHONPATH=/app diff --git a/geospatial-utils/main.py b/geospatial-utils/main.py index fbf33fb..4518c78 100644 --- a/geospatial-utils/main.py +++ b/geospatial-utils/main.py @@ -7,6 +7,7 @@ import config import convert import fileutils +import validate from fileutils import ed269 from loguru import logger @@ -47,9 +48,16 @@ def main(): logger.warning("Additional data not provided in ED269 is hard-coded with Swiss FOCA information. This will be moved to a configurable file in the near future.") ed318_data = convert.from_ed269_to_ed318(ed269_data, config=config.FOCA) output = pathlib.Path(args.output_file) - output.write_text(json.dumps(ed318_data), encoding="utf-8") + json_output = json.dumps(ed318_data) + output.write_text(json_output, encoding="utf-8") + logger.debug(f"Successful conversion. File saved to: {output.absolute()}") - logger.info(f"Successful conversion. ED-318 saved to {output.absolute()}") + errors = validate.ed318(json.loads(json_output)) + if len(errors) > 0: + for e in errors: + logger.error(f"{e.json_path}: {e.message}") + sys.exit(1) + logger.info(f"Successful conversion and validation. ED-318 saved to {output.absolute()}") else: parser.print_help() diff --git a/geospatial-utils/validate.py b/geospatial-utils/validate.py new file mode 100644 index 0000000..2968fbb --- /dev/null +++ b/geospatial-utils/validate.py @@ -0,0 +1,57 @@ +# Validation script extensively inspired from https://github.com/UASGeoZones/ED-318/blob/main/examples/validate_examples.py + +import json +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import jsonschema +from referencing import Registry, Resource + +SCHEMA_PATH = Path(os.path.dirname(__file__)).parent / "schemas/ED-318/schema" +ROOT_SCHEMA = Path(SCHEMA_PATH) / "Schema_GeoZones.json" + + +@dataclass +class ValidationErrorWithPath: + """Error encountered while validating an instance against a schema.""" + + message: str + """Validation error message.""" + + json_path: str + """Location of the data causing the validation error.""" + + +def _collect_errors(e: jsonschema.ValidationError) -> list[ValidationErrorWithPath]: + if e.context: + result: list[ValidationErrorWithPath] = [] + for child in e.context: + result.extend(_collect_errors(child)) + return result + else: + return [ValidationErrorWithPath(message=e.message, json_path=e.json_path)] + + +def _build_registry(schema_dir: Path) -> Registry: + def retrieve(uri: str): + local_ref = (schema_dir / Path(uri)).resolve() + return Resource.from_contents(json.loads(local_ref.read_text())) + + registry: Registry = Registry(retrieve=retrieve) + return registry + + +def ed318(data: dict[str, Any]) -> list[ValidationErrorWithPath]: + """Validate the data object using ED-318 jsonschemas""" + schema_content = json.loads(ROOT_SCHEMA.read_bytes()) + registry = _build_registry(SCHEMA_PATH) + validator = jsonschema.Draft7Validator(schema=schema_content, registry=registry) + validator.check_schema(schema_content) + + errors: list[ValidationErrorWithPath] = [] + for e in validator.iter_errors(data): # type: ignore + errors.extend(_collect_errors(e)) + + return errors diff --git a/pyproject.toml b/pyproject.toml index 4c7a2e5..f4f9338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,11 @@ readme = "README.md" requires-python = "==3.13.5" dependencies = [ "basedpyright>=1.31.1", + "jsonschema>=4.25.1", "loguru>=0.7.3", "requests>=2.32.5", "ruff", + "types-jsonschema>=4.25.1.20250822", "uas-standards>=4.1.0", ] @@ -27,6 +29,7 @@ line-length = 88 typeCheckingMode = "standard" exclude = [ - "**/__pycache__", - "**/.*", + "**/__pycache__", + "**/.*", + "schemas/**", ] diff --git a/schemas/ED-318 b/schemas/ED-318 new file mode 160000 index 0000000..e98b292 --- /dev/null +++ b/schemas/ED-318 @@ -0,0 +1 @@ +Subproject commit e98b292c5665a04989d62e32fd93829f161a89a9 diff --git a/uv.lock b/uv.lock index a2d9280..720f3aa 100644 --- a/uv.lock +++ b/uv.lock @@ -80,18 +80,22 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "basedpyright" }, + { name = "jsonschema" }, { name = "loguru" }, { name = "requests" }, { name = "ruff" }, + { name = "types-jsonschema" }, { name = "uas-standards" }, ] [package.metadata] requires-dist = [ { name = "basedpyright", specifier = ">=1.31.1" }, + { name = "jsonschema", specifier = ">=4.25.1" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "ruff" }, + { name = "types-jsonschema", specifier = ">=4.25.1.20250822" }, { name = "uas-standards", specifier = ">=4.1.0" }, ] @@ -293,6 +297,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "types-jsonschema" +version = "4.25.1.20250822" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/7f/369b54dad6eb6b5adc1fb1c53edbed18e6c32cbc600357135308902fdbdc/types_jsonschema-4.25.1.20250822.tar.gz", hash = "sha256:aac69ed4b23f49aaceb7fcb834141d61b9e4e6a7f6008cb2f0d3b831dfa8464a", size = 15628, upload-time = "2025-08-22T03:04:18.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/3d/bc1d171f032fcf63cedd4ade241f3f4e66d7e3bb53ee1da3c8f2f043eb0b/types_jsonschema-4.25.1.20250822-py3-none-any.whl", hash = "sha256:f82c2d7fa1ce1c0b84ba1de4ed6798469768188884db04e66421913a4e181294", size = 15923, upload-time = "2025-08-22T03:04:17.346Z" }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20250822" From 8d86f3332dd51f2dd476cf1085c100670a82e523 Mon Sep 17 00:00:00 2001 From: Michael Barroco Date: Wed, 1 Oct 2025 18:04:01 +0200 Subject: [PATCH 2/2] format --- geospatial-utils/convert.py | 20 ++++++++++---------- geospatial-utils/main.py | 8 ++++++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/geospatial-utils/convert.py b/geospatial-utils/convert.py index e0e49a1..c411e90 100644 --- a/geospatial-utils/convert.py +++ b/geospatial-utils/convert.py @@ -1,4 +1,14 @@ from config import ED318Additions +from uas_standards.eurocae_ed269 import ( + ApplicableTimePeriod, + ED269Schema, + HorizontalProjectionType, + Reason, + Restriction, + UASZoneAirspaceVolume, + UASZoneAuthority, + UomDimensions, +) from uas_standards.eurocae_ed318 import ( Authority, CodeVerticalReferenceType, @@ -20,16 +30,6 @@ UomDistance, VerticalLayer, ) -from uas_standards.eurocae_ed269 import ( - ApplicableTimePeriod, - ED269Schema, - HorizontalProjectionType, - Reason, - Restriction, - UASZoneAirspaceVolume, - UASZoneAuthority, - UomDimensions, -) COUNTRY_REGION_MAPPING = {"CHE": 0, "LIE": 27} diff --git a/geospatial-utils/main.py b/geospatial-utils/main.py index 4518c78..2dd123f 100644 --- a/geospatial-utils/main.py +++ b/geospatial-utils/main.py @@ -45,7 +45,9 @@ def main(): ed269_data = ed269.loads(source) # TODO: Move hard-coded configuration to a json file. - logger.warning("Additional data not provided in ED269 is hard-coded with Swiss FOCA information. This will be moved to a configurable file in the near future.") + logger.warning( + "Additional data not provided in ED269 is hard-coded with Swiss FOCA information. This will be moved to a configurable file in the near future." + ) ed318_data = convert.from_ed269_to_ed318(ed269_data, config=config.FOCA) output = pathlib.Path(args.output_file) json_output = json.dumps(ed318_data) @@ -57,7 +59,9 @@ def main(): for e in errors: logger.error(f"{e.json_path}: {e.message}") sys.exit(1) - logger.info(f"Successful conversion and validation. ED-318 saved to {output.absolute()}") + logger.info( + f"Successful conversion and validation. ED-318 saved to {output.absolute()}" + ) else: parser.print_help()