From dc8f41d1d6ea7b1c847d6696a2d24be8fe273318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Garn=C3=A6s?= Date: Mon, 20 Feb 2023 19:16:41 +0100 Subject: [PATCH] Add data_queries metadata to /collections endpoint The [Collections requirement class](https://docs.ogc.org/is/19-086r5/19-086r5.html#toc43) has Requirement 48 /req/edr/rc-common-query-type, which states how the data_queries proptery of a collection object should look. The output formats and crs details supported by an EDR query are specified through the application configuration, loaded by the provider plugin which provides this metadata when generating the collections metadata. There are a few query types with additional requirements, which are also implemented here: - /req/edr/rc-radius-variables - /req/edr/rc-cube-variables - /req/edr/rc-corridor-variables --- docs/source/configuration.rst | 11 ++++++++ pygeoapi/api.py | 9 ++++++- pygeoapi/provider/base_edr.py | 47 +++++++++++++++++++++++++++++++++++ pygeoapi/util.py | 43 ++++++++++++++++++++++++++++++++ tests/test_api.py | 25 +++++++++++++++++-- 5 files changed, 132 insertions(+), 3 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index b9422d828..16c286451 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -238,6 +238,17 @@ default. mimetype: application/json # required: format mimetype options: # optional options to pass to provider (i.e. GDAL creation) option_name: option_value + data_queries: # optional and for edr providers only, to enrich /collections metadata + cube: # Specify query type (ie. position, radius, cube, area, corridor, locations, trajectory) + output_formats: # optional + - CoverageJSON + crs_details: # optional + - crs: CRS84 + wkt: "GEOGCS[\"WGS 84 ..." + # Optional, specify query specific units, defaults to [""] + # cube has `height_units`, radius has `within_units`, corridor has `height_units` and `width_units` + height_units: + - m hello-world: # name of process type: collection # REQUIRED (collection, process, or stac-collection) diff --git a/pygeoapi/api.py b/pygeoapi/api.py index 7a1203231..34e166492 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -83,7 +83,7 @@ json_serial, render_j2_template, str2bool, TEMPLATES, to_json, get_api_rules, get_base_url, get_crs_from_uri, get_supported_crs_list, - CrsTransformSpec, transform_bbox) + CrsTransformSpec, transform_bbox, edr_data_query_object) from pygeoapi.models.provider.base import TilesMetadataFormat @@ -1189,6 +1189,7 @@ def describe_collections(self, request: Union[APIRequest, Any], if edr: # TODO: translate LOGGER.debug('Adding EDR links') + collection['data_queries'] = {} parameters = p.get_fields() if parameters: collection['parameter_names'] = {} @@ -1196,6 +1197,12 @@ def describe_collections(self, request: Union[APIRequest, Any], collection['parameter_names'][f['id']] = f for qt in p.get_query_types(): + collection['data_queries'][qt] = \ + edr_data_query_object( + qt, + f'{self.get_collections_url()}/{k}', + p + ) collection['links'].append({ 'type': 'application/json', 'rel': 'data', diff --git a/pygeoapi/provider/base_edr.py b/pygeoapi/provider/base_edr.py index 3e7a259cb..bf45a0cac 100644 --- a/pygeoapi/provider/base_edr.py +++ b/pygeoapi/provider/base_edr.py @@ -28,6 +28,7 @@ # ================================================================= import logging +from typing import List, Dict from pygeoapi.provider.base import BaseProvider @@ -38,6 +39,13 @@ class BaseEDRProvider(BaseProvider): """Base EDR Provider""" query_types = [] + radius_within_units: List[str] + cube_height_units: List[str] + corridor_height_units: List[str] + corridor_width_units: List[str] + + output_formats: Dict[str, List[str]] = {} + crs_details: Dict[str, Dict] = {} def __init__(self, provider_def): """ @@ -50,6 +58,27 @@ def __init__(self, provider_def): super().__init__(provider_def) + for query_type in self.get_query_types(): + self.output_formats[query_type] = \ + provider_def.get('data_queries', {}).get(query_type, {}) \ + .get('output_formats') + self.crs_details[query_type] = \ + provider_def.get('data_queries', {}).get(query_type, {}) \ + .get('crs_details') + + self.cube_height_units = \ + provider_def.get('data_queries', {}).get('cube', {}) \ + .get('height_units', [""]) + self.radius_within_units = \ + provider_def.get('data_queries', {}).get('radius', {}) \ + .get('within_units', [""]) + self.corridor_height_units = \ + provider_def.get('data_queries', {}).get('corridor', {}) \ + .get('height_units', [""]) + self.corridor_width_units = \ + provider_def.get('data_queries', {}).get('corridor', {}) \ + .get('width_units', [""]) + self.instances = [] @classmethod @@ -77,6 +106,24 @@ def get_query_types(self): return self.query_types + def get_output_formats(self, query_type: str): + return self.output_formats.get(query_type) + + def get_crs_details(self, query_type: str): + return self.crs_details.get(query_type) + + def get_cube_height_units(self): + return self.cube_height_units + + def get_radius_within_units(self): + return self.radius_within_units + + def get_corridor_width_units(self): + return self.corridor_width_units + + def get_corridor_height_units(self): + return self.corridor_height_units + def query(self, **kwargs): """ Extract data from collection collection diff --git a/pygeoapi/util.py b/pygeoapi/util.py index d4fbabdff..051239144 100644 --- a/pygeoapi/util.py +++ b/pygeoapi/util.py @@ -75,6 +75,7 @@ from pygeoapi import l10n from pygeoapi.models import config as config_models from pygeoapi.provider.base import ProviderTypeError +from pygeoapi.provider.base_edr import BaseEDRProvider LOGGER = logging.getLogger(__name__) @@ -886,3 +887,45 @@ def bbox2geojsongeometry(bbox: list) -> dict: b = box(*bbox, ccw=False) return geom_to_geojson(b) + +def edr_data_query_object(query_type: str, collection_url: str, + provider_plugin: BaseEDRProvider) -> dict: + """ + Constructs an EDR compliant data_queries metadata object for a given + query type, see Requirement A.48: + https://docs.ogc.org/is/19-086r5/19-086r5.html#toc43 + :param query_type: EDR query type (position, radius etc.) + :param collection_url: The base collection URL including + :param provider_plugin: Instance of EDR provider serving the data, based + on pygeoapi.provider.BaseEDRProvider + :return: `dictionary` with a data_queries metadata object + """ + data_query_obj = { + 'link': { + 'href': f'{collection_url}/{query_type}', + 'rel': 'data', + 'variables': { + 'title': f'{query_type} query', + 'description': f'{query_type} query', + 'query_type': query_type, + } + } + } + output_formats = provider_plugin.get_output_formats(query_type) + if output_formats: + data_query_obj['link']['variables']['output_formats'] = output_formats + crs_details = provider_plugin.get_crs_details(query_type) + if crs_details: + data_query_obj['link']['variables']['crs_details'] = crs_details + if query_type == 'radius': + data_query_obj['link']['within_units'] = \ + provider_plugin.get_radius_within_units() + if query_type == 'cube': + data_query_obj['link']['height_units'] = \ + provider_plugin.get_cube_height_units() + if query_type == 'corridor': + data_query_obj['link']['height_units'] = \ + provider_plugin.get_corridor_height_units() + data_query_obj['link']['width_units'] = \ + provider_plugin.get_corridor_width_units() + return data_query_obj diff --git a/tests/test_api.py b/tests/test_api.py index ad576b1ac..d859e3815 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1878,8 +1878,22 @@ def test_delete_job(api_): assert code == HTTPStatus.NOT_FOUND -def test_get_collection_edr_query(config, api_): - # edr resource +def test_describe_collection_edr(config, api_): + # Collections metadata + req = mock_request() + rsp_headers, code, response = api_.describe_collections(req) + print(response) + collections = json.loads(response)['collections'] + collection = next(c for c in collections if c['id'] == 'icoads-sst') + parameter_names = list(collection['parameter-names'].keys()) + parameter_names.sort() + assert len(parameter_names) == 4 + assert parameter_names == ['AIRT', 'SST', 'UWND', 'VWND'] + data_query_names = list(collection['data_queries'].keys()) + data_query_names.sort() + assert len(data_query_names) == 2 + assert data_query_names == ['cube', 'position'] + # Specific collection metadata req = mock_request() rsp_headers, code, response = api_.describe_collections(req, 'icoads-sst') collection = json.loads(response) @@ -1887,7 +1901,14 @@ def test_get_collection_edr_query(config, api_): parameter_names.sort() assert len(parameter_names) == 4 assert parameter_names == ['AIRT', 'SST', 'UWND', 'VWND'] + data_query_names = list(collection['data_queries'].keys()) + data_query_names.sort() + assert len(data_query_names) == 2 + assert data_query_names == ['cube', 'position'] + +def test_get_collection_edr_query(config, api_): + req = mock_request() # no coords parameter rsp_headers, code, response = api_.get_collection_edr_query( req, 'icoads-sst', None, 'position')