From 0820ce405a41599a4c81ab72a909067f689dbcfe Mon Sep 17 00:00:00 2001 From: Lars Falk-Petersen Date: Fri, 29 Nov 2024 09:12:37 +0100 Subject: [PATCH] Split out requrementA5_2 and add test. --- sedr/__init__.py | 2 +- sedr/edreq12.py | 26 +++- sedr/preflight.py | 10 +- sedr/schemat.py | 31 +--- sedr/test_edreq12.py | 33 +++++ sedr/util.py | 11 ++ testdata/edrisobaric_collection.json | 139 ++++++++++++++++++ testdata/edrisobaric_collection_bad_bbox.json | 137 +++++++++++++++++ 8 files changed, 355 insertions(+), 34 deletions(-) create mode 100644 sedr/test_edreq12.py create mode 100644 testdata/edrisobaric_collection.json create mode 100644 testdata/edrisobaric_collection_bad_bbox.json diff --git a/sedr/__init__.py b/sedr/__init__.py index 8be3c9a..8a4873b 100644 --- a/sedr/__init__.py +++ b/sedr/__init__.py @@ -39,7 +39,7 @@ def main() -> None: util.test_functions["conformance"] += rodeoprofile.tests_conformance util.test_functions["collection"] += rodeoprofile.tests_collection - # TODO: include profile tests based on conformance_url + # TODO: include profile tests based on conformance_url, https://github.com/metno/sedr/issues/32 # if rodeoprofile.conformance_url in conformance_json["conformsTo"]: # util.args.rodeo_profile = True diff --git a/sedr/edreq12.py b/sedr/edreq12.py index ca1d52a..e56578e 100644 --- a/sedr/edreq12.py +++ b/sedr/edreq12.py @@ -73,6 +73,30 @@ def requirementA11_1(jsondata: dict) -> tuple[bool, str]: ) +def requrementA5_2(jsondata: dict) -> tuple[bool, str]: + """ + OGC API - Environmental Data Retrieval Standard + Version: 1.2 + Requirement A5.2 + + Check extent spatial bbox + """ + + spec_url = f"{edr_root_url}#req_core_rc-bbox-definition" + collection_url = util.parse_collection_url(jsondata) + + extent = None + extent = util.parse_spatial_bbox(jsondata) + if extent is None or len(extent) > 1 or not isinstance(extent, list): + return ( + False, + f"Extent→spatial→bbox should be a list of bboxes with exactly " + f"one bbox in, found {len(extent)} in collection " + f"<{jsondata['id']}>. See {spec_url} for more info." + ) + return True, f"Extent→spatial→bbox for collection is {extent}" + + tests_landing: list[Callable[[dict], tuple[bool, str]]] = [] tests_conformance = [requirementA2_2_A5, requirementA11_1] -tests_collection: list[Callable[[dict], tuple[bool, str]]] = [] +tests_collection = [requrementA5_2] diff --git a/sedr/preflight.py b/sedr/preflight.py index e840204..8157b8d 100644 --- a/sedr/preflight.py +++ b/sedr/preflight.py @@ -15,12 +15,12 @@ def fetch_landing(url: str, timeout: int) -> tuple[bool, dict]: landing_json = response.json() except requests.exceptions.ConnectionError as err: util.logger.error( - f"{__name__} fetch_landing Unable to get landing page <%s>.\n%s", url, err + f"Unable to get landing page <%s>.\n%s", url, err ) return False, landing_json - except json.decoder.JSONDecodeError: + except json.decoder.JSONDecodeError as err: util.logger.error( - f"{__name__} fetch_landing Landing page <%s> is not valid JSON.", url + f"fetch_landing Landing page <%s> is not valid JSON.\n%s", url, err ) return False, landing_json return True, landing_json @@ -37,12 +37,12 @@ def fetch_conformance(url: str, timeout: int) -> tuple[bool, dict]: conformance_json = response.json() except requests.exceptions.ConnectionError as err: util.logger.error( - f"{__name__} fetch_landing Unable to get landing page <%s>.\n%s", url, err + f"Unable to get conformance <%s>.\n%s", url, err ) return False, conformance_json except json.decoder.JSONDecodeError as err: util.logger.error( - "Conformance page <%s> is not valid JSON:\n%s", conformance_url, err + "Conformance <%s> is not valid JSON:\n%s", conformance_url, err ) return False, conformance_json return True, conformance_json diff --git a/sedr/schemat.py b/sedr/schemat.py index ceff1e3..7d84ec9 100644 --- a/sedr/schemat.py +++ b/sedr/schemat.py @@ -108,39 +108,16 @@ def test_edr_collections(case): for collection_json in json.loads(response.text)["collections"]: # Use url as key for extents. Remove trailing slash from url. - collection_url = collection_json["links"][0]["href"].rstrip("/") + collection_url = util.parse_collection_url(collection_json) collection_ids[collection_url] = collection_json["id"] util.logger.debug( "test_collections found collection id %s", collection_json["id"] ) - extent = None - try: - extent = collection_json["extent"]["spatial"]["bbox"][ - 0 - ] # TODO: assuming only one extent - - # Make sure bbox contains a list of extents, not just an extent - assert isinstance( - extent, list - ), f"Extent→spatial→bbox should be a list of bboxes with one bbox in, not a single bbox. \ - Example [[1, 2, 3, 4]]. Was <{collection_json['extent']['spatial']['bbox']}>. See {spec_ref} for more info." - extents[collection_url] = tuple(extent) - - util.logger.debug( - "test_collections found extent for %s: %s", collection_url, extent - ) - except AttributeError: - pass - except KeyError as err: - if err.args[0] == "extent": - raise AssertionError( - f"Unable to find extent for collection ID " - f"{collection_json['id']}. Found " - f"[{', '.join(collection_json.keys())}]. " - f"See {spec_ref} for more info." - ) from err + # Validation done in requrementA5_2 + extent = util.parse_spatial_bbox(collection_json) + extents[collection_url] = tuple(extent[0]) # Run edr, ogc, profile tests for f in util.test_functions["collection"]: diff --git a/sedr/test_edreq12.py b/sedr/test_edreq12.py new file mode 100644 index 0000000..b6bad3a --- /dev/null +++ b/sedr/test_edreq12.py @@ -0,0 +1,33 @@ +"""Unit tests for test_edreq12.py.""" + +import unittest +import json +import util +import edreq12 as edreq + + +class TestEDR(unittest.TestCase): + __version__ = "testversion" + util.args = util.parse_args(["--url", "https://example.com/"], __version__) + util.logger = util.set_up_logging( + args=util.args, logfile=util.args.log_file, version=__version__ + ) + + def test_requrementA5_2(self): + # Good tests + jsondata = {} + with open("testdata/edrisobaric_collection.json", "r", encoding="utf-8") as f: + jsondata = json.load(f) + ok, _ = edreq.requrementA5_2(jsondata) + self.assertTrue(ok) + + # Bad tests + jsondata = {} + with open("testdata/edrisobaric_collection_bad_bbox.json", "r", encoding="utf-8") as f: + jsondata = json.load(f) + ok, _ = edreq.requrementA5_2(jsondata) + self.assertFalse(ok) + + +if __name__ == "__main__": + unittest.main() diff --git a/sedr/util.py b/sedr/util.py index 14f9879..1435e44 100644 --- a/sedr/util.py +++ b/sedr/util.py @@ -165,3 +165,14 @@ def locate_openapi_url(url: str, timeout: int) -> str: def build_conformance_url(url: str) -> str: """Build the conformance URL based on main URL.""" return urljoin(url, "/conformance") + + +def parse_collection_url(jsondata: dict) -> str: + return jsondata["links"][0]["href"].rstrip("/") + + +def parse_spatial_bbox(jsondata: dict) -> list: + try: + return jsondata["extent"]["spatial"]["bbox"] + except (AttributeError, KeyError) as err: + return None diff --git a/testdata/edrisobaric_collection.json b/testdata/edrisobaric_collection.json new file mode 100644 index 0000000..fb960b9 --- /dev/null +++ b/testdata/edrisobaric_collection.json @@ -0,0 +1,139 @@ +{ + "id": "weather_forecast", + "title": "IsobaricGRIB - GRIB files", + "description": "These files are used by Avinor ATM systems but possibly also of interest to others. They contain temperature and wind forecasts for a set of isobaric layers (i.e. altitudes having the same pressure). The files are (normally) produced every 6 hours. You can check the time when generated using the Last-Modified header or the `updated` key in `available`. These files are in GRIB2 format (filetype BIN) for the following regions: southern_norway Area 64.25N -1.45W 55.35S 14.51E, resolution .1 degrees? (km?) FIXME It includes every odd-numbered isobaric layer from 1 to 137 (in hundreds of feet?)", + "keywords": [ + "position", + "data", + "api", + "temperature", + "wind", + "forecast", + "isobaric", + "weather_forecast" + ], + "links": [ + { + "href": "https://edrisobaric.k8s.met.no/collections/weather_forecast/", + "rel": "data" + }, + { + "href": "https://data.norge.no/nlod/en/2.0/", + "rel": "license", + "type": "text/html" + }, + { + "href": "https://creativecommons.org/licenses/by/4.0/", + "rel": "license", + "type": "text/html" + } + ], + "extent": { + "spatial": { + "bbox": [ + [ + -1.4499999999999886, + 55.35, + 14.45, + 64.25 + ] + ], + "crs": "GEOGCS[\"Unknown\", DATUM[\"Unknown\", SPHEROID[\"WGS_1984\", 6378137.0, 298.257223563]], PRIMEM[\"Greenwich\",0], UNIT[\"degree\", 0.017453], AXIS[\"Lon\", EAST], AXIS[\"Lat\", NORTH]]" + }, + "temporal": { + "interval": [ + [ + "2024-11-28T18:00:00Z", + "2024-11-29T06:00:00Z" + ] + ], + "values": [ + "2024-11-28T18:00:00+00:00" + ], + "trs": "TIMECRS[\"DateTime\",TDATUM[\"Gregorian Calendar\"],CS[TemporalDateTime,1],AXIS[\"Time (T)\",future]" + }, + "vertical": { + "interval": [ + [ + "850.0" + ], + [ + "100.0" + ] + ], + "values": [ + "850.0", + "750.0", + "700.0", + "600.0", + "500.0", + "450.0", + "400.0", + "350.0", + "300.0", + "275.0", + "250.0", + "225.0", + "200.0", + "150.0", + "100.0" + ], + "vrs": "Vertical Reference System: PressureLevel" + } + }, + "data_queries": { + "position": { + "link": { + "href": "https://edrisobaric.k8s.met.no/collections/weather_forecast/position", + "rel": "data", + "variables": { + "query_type": "position", + "output_formats": [ + "CoverageJSON" + ], + "default_output_format": "CoverageJSON" + } + } + } + }, + "crs": [ + "CRS:84" + ], + "parameter_names": { + "wind_from_direction": { + "type": "Parameter", + "id": "wind_from_direction", + "unit": { + "symbol": { + "value": "˚", + "type": "https://codes.wmo.int/common/unit/_degree_(angle)" + } + }, + "observedProperty": { + "id": "http://vocab.met.no/CFSTDN/en/page/wind_from_direction", + "label": "Wind from direction" + } + }, + "wind_speed": { + "type": "Parameter", + "observedProperty": { + "id": "http://vocab.met.no/CFSTDN/en/page/wind_speed", + "label": "Wind speed" + } + }, + "Air temperature": { + "type": "Parameter", + "id": "Temperature", + "unit": { + "symbol": { + "value": "˚C", + "type": "https://codes.wmo.int/common/unit/_Cel" + } + }, + "observedProperty": { + "id": "http://vocab.met.no/CFSTDN/en/page/air_temperature", + "label": "Air temperature" + } + } + } +} diff --git a/testdata/edrisobaric_collection_bad_bbox.json b/testdata/edrisobaric_collection_bad_bbox.json new file mode 100644 index 0000000..c024e1a --- /dev/null +++ b/testdata/edrisobaric_collection_bad_bbox.json @@ -0,0 +1,137 @@ +{ + "id": "weather_forecast", + "title": "IsobaricGRIB - GRIB files", + "description": "These files are used by Avinor ATM systems but possibly also of interest to others. They contain temperature and wind forecasts for a set of isobaric layers (i.e. altitudes having the same pressure). The files are (normally) produced every 6 hours. You can check the time when generated using the Last-Modified header or the `updated` key in `available`. These files are in GRIB2 format (filetype BIN) for the following regions: southern_norway Area 64.25N -1.45W 55.35S 14.51E, resolution .1 degrees? (km?) FIXME It includes every odd-numbered isobaric layer from 1 to 137 (in hundreds of feet?)", + "keywords": [ + "position", + "data", + "api", + "temperature", + "wind", + "forecast", + "isobaric", + "weather_forecast" + ], + "links": [ + { + "href": "https://edrisobaric.k8s.met.no/collections/weather_forecast/", + "rel": "data" + }, + { + "href": "https://data.norge.no/nlod/en/2.0/", + "rel": "license", + "type": "text/html" + }, + { + "href": "https://creativecommons.org/licenses/by/4.0/", + "rel": "license", + "type": "text/html" + } + ], + "extent": { + "spatial": { + "bbox": [ + -1.4499999999999886, + 55.35, + 14.45, + 64.25 + ], + "crs": "GEOGCS[\"Unknown\", DATUM[\"Unknown\", SPHEROID[\"WGS_1984\", 6378137.0, 298.257223563]], PRIMEM[\"Greenwich\",0], UNIT[\"degree\", 0.017453], AXIS[\"Lon\", EAST], AXIS[\"Lat\", NORTH]]" + }, + "temporal": { + "interval": [ + [ + "2024-11-28T18:00:00Z", + "2024-11-29T06:00:00Z" + ] + ], + "values": [ + "2024-11-28T18:00:00+00:00" + ], + "trs": "TIMECRS[\"DateTime\",TDATUM[\"Gregorian Calendar\"],CS[TemporalDateTime,1],AXIS[\"Time (T)\",future]" + }, + "vertical": { + "interval": [ + [ + "850.0" + ], + [ + "100.0" + ] + ], + "values": [ + "850.0", + "750.0", + "700.0", + "600.0", + "500.0", + "450.0", + "400.0", + "350.0", + "300.0", + "275.0", + "250.0", + "225.0", + "200.0", + "150.0", + "100.0" + ], + "vrs": "Vertical Reference System: PressureLevel" + } + }, + "data_queries": { + "position": { + "link": { + "href": "https://edrisobaric.k8s.met.no/collections/weather_forecast/position", + "rel": "data", + "variables": { + "query_type": "position", + "output_formats": [ + "CoverageJSON" + ], + "default_output_format": "CoverageJSON" + } + } + } + }, + "crs": [ + "CRS:84" + ], + "parameter_names": { + "wind_from_direction": { + "type": "Parameter", + "id": "wind_from_direction", + "unit": { + "symbol": { + "value": "˚", + "type": "https://codes.wmo.int/common/unit/_degree_(angle)" + } + }, + "observedProperty": { + "id": "http://vocab.met.no/CFSTDN/en/page/wind_from_direction", + "label": "Wind from direction" + } + }, + "wind_speed": { + "type": "Parameter", + "observedProperty": { + "id": "http://vocab.met.no/CFSTDN/en/page/wind_speed", + "label": "Wind speed" + } + }, + "Air temperature": { + "type": "Parameter", + "id": "Temperature", + "unit": { + "symbol": { + "value": "˚C", + "type": "https://codes.wmo.int/common/unit/_Cel" + } + }, + "observedProperty": { + "id": "http://vocab.met.no/CFSTDN/en/page/air_temperature", + "label": "Air temperature" + } + } + } +}