diff --git a/ogc/edr/edr_config.py b/ogc/edr/edr_config.py index 100b262..5bdd87d 100644 --- a/ogc/edr/edr_config.py +++ b/ogc/edr/edr_config.py @@ -51,12 +51,12 @@ def get_configuration(base_url: str, layers: List[pogc.Layer]) -> Dict[str, Any] # Add the data resources and provider information resources = configuration.get("resources", {}) - configuration["resources"] = resources | EdrConfig._resources_definition(layers) + configuration["resources"] = resources | EdrConfig._resources_definition(base_url, layers) return configuration @staticmethod - def _resources_definition(layers: List[pogc.Layer]) -> Dict[str, Any]: + def _resources_definition(base_url: str, layers: List[pogc.Layer]) -> Dict[str, Any]: """Define resource related data for the configuration. The resources dictionary holds the information needed to generate the collections. @@ -66,6 +66,8 @@ def _resources_definition(layers: List[pogc.Layer]) -> Dict[str, Any]: Parameters ---------- + base_url : str + The base URL used as an identifier for the given layers. layers : List[pogc.Layer] The layers which define the data sources for the EDR server. @@ -98,7 +100,7 @@ def _resources_definition(layers: List[pogc.Layer]) -> Dict[str, Any]: "default": True, "name": "ogc.edr.edr_provider.EdrProvider", "data": group_name, - "layers": group_layers, + "base_url": base_url, "crs": [ "https://www.opengis.net/def/crs/OGC/1.3/CRS84", "https://www.opengis.net/def/crs/EPSG/0/4326", diff --git a/ogc/edr/edr_provider.py b/ogc/edr/edr_provider.py index 897d12a..dad1b4e 100644 --- a/ogc/edr/edr_provider.py +++ b/ogc/edr/edr_provider.py @@ -4,6 +4,7 @@ import zipfile import traitlets as tl from datetime import datetime +from collections import defaultdict from typing import List, Dict, Tuple, Any from shapely.geometry.base import BaseGeometry from pygeoapi.provider.base import ProviderConnectionError, ProviderInvalidQueryError @@ -17,6 +18,37 @@ class EdrProvider(BaseEDRProvider): """Custom provider to be used with layer data sources.""" + _layers_dict = defaultdict(list) + + @classmethod + def set_layers(cls, base_url: str, layers: List[pogc.Layer]): + """Set the layer resources which will be available to the provider. + + Parameters + ---------- + base_url : str + The base URL that the layers are available on. + layers : List[pogc.Layer] + The layers which the provider will have access to. + """ + cls._layers_dict[base_url] = layers + + @classmethod + def get_layers(cls, base_url: str) -> List[pogc.Layer]: + """Get the layer resources for a specific base URL. + + Parameters + ---------- + base_url : str + The base URL for the layers. + + Returns + ------- + List[pogc.Layer] + The layers associated with the base URL. + """ + return cls._layers_dict.get(base_url, []) + def __init__(self, provider_def: Dict[str, Any]): """Construct the provider using the provider definition. @@ -30,7 +62,7 @@ def __init__(self, provider_def: Dict[str, Any]): ProviderConnectionError Raised if the specified collection is not found within any layers. ProviderConnectionError - Raised if the provider does not specify any data sources. + Raised if the provider does not specify any base URL. """ super().__init__(provider_def) collection_id = provider_def.get("data", None) @@ -39,9 +71,9 @@ def __init__(self, provider_def: Dict[str, Any]): self.collection_id = str(collection_id) - self.layers = provider_def.get("layers", []) - if len(self.layers) == 0: - raise ProviderConnectionError("Valid data sources not found.") + self.base_url = provider_def.get("base_url", None) + if self.base_url is None: + raise ProviderConnectionError("Valid URL identifier not found for the data.") @property def parameters(self) -> Dict[str, pogc.Layer]: @@ -54,7 +86,9 @@ def parameters(self) -> Dict[str, pogc.Layer]: Dict[str, pogc.Layer] The parameters as a dictionary of layer identifiers and layer objects. """ - return {layer.identifier: layer for layer in self.layers if layer.group == self.collection_id} + return { + layer.identifier: layer for layer in self.get_layers(self.base_url) if layer.group == self.collection_id + } def handle_query(self, requested_coordinates: podpac.Coordinates, **kwargs): """Handle the requests to the EDR server at the specified requested coordinates. @@ -136,13 +170,21 @@ def handle_query(self, requested_coordinates: podpac.Coordinates, **kwargs): ) self.check_query_condition( - requested_native_coordinates.size > settings.MAX_GRID_COORDS_REQUEST_SIZE, + bool(requested_native_coordinates.size > settings.MAX_GRID_COORDS_REQUEST_SIZE), "Grid coordinates x_size * y_size must be less than %d" % settings.MAX_GRID_COORDS_REQUEST_SIZE, ) dataset = {} for requested_parameter, layer in parameters_filtered.items(): units_data_array = layer.node.eval(requested_native_coordinates) + # Recombine stacked temporal dimensions if necessary. + # The temporal output should always be stacked, based on stacked input. + if "time_forecastOffsetHr" in units_data_array.dims: + forecast_offsets = units_data_array.forecastOffsetHr.data.copy() + time_data = units_data_array.time.data.copy() + units_data_array = units_data_array.drop_vars({"time", "time_forecastOffsetHr", "forecastOffsetHr"}) + units_data_array = units_data_array.rename(time_forecastOffsetHr="time") + units_data_array = units_data_array.assign_coords(time=time_data + forecast_offsets) dataset[requested_parameter] = units_data_array self.check_query_condition(len(dataset) == 0, "No matching parameters found.") @@ -150,7 +192,8 @@ def handle_query(self, requested_coordinates: podpac.Coordinates, **kwargs): # Return a coverage json if specified, else return Base64 encoded native response if output_format == "json" or output_format == "coveragejson": crs = self.interpret_crs(requested_native_coordinates.crs if requested_native_coordinates else None) - return self.to_coverage_json(self.layers, dataset, crs) + layers = self.get_layers(self.base_url) + return self.to_coverage_json(layers, dataset, crs) else: return self.to_geotiff_response(dataset, self.collection_id) @@ -182,11 +225,13 @@ def position(self, **kwargs): crs = EdrProvider.interpret_crs(crs) if not isinstance(wkt, BaseGeometry): - raise ProviderInvalidQueryError("Invalid wkt provided.") + msg = "Invalid WKT string provided for the position query." + raise ProviderInvalidQueryError(msg, user_msg=msg) elif wkt.geom_type == "Point": lon, lat = EdrProvider.crs_converter([wkt.x], [wkt.y], crs) else: - raise ProviderInvalidQueryError("Unknown WKT Type (Use Point).") + msg = "Unknown WKT string type for the position query (use Point)." + raise ProviderInvalidQueryError(msg, user_msg=msg) requested_coordinates = podpac.Coordinates([lat, lon], dims=["lat", "lon"], crs=crs) @@ -217,7 +262,8 @@ def cube(self, **kwargs): crs = EdrProvider.interpret_crs(crs) if not isinstance(bbox, List) or len(bbox) != 4: - raise ProviderInvalidQueryError("Invalid bounding box provided.") + msg = "Invalid bounding box provided, expected bounding box of (minx, miny, maxx, maxy)." + raise ProviderInvalidQueryError(msg, user_msg=msg) xmin, ymin, xmax, ymax = bbox lon, lat = EdrProvider.crs_converter([xmin, xmax], [ymin, ymax], crs) @@ -254,11 +300,13 @@ def area(self, **kwargs): crs = EdrProvider.interpret_crs(crs) if not isinstance(wkt, BaseGeometry): - raise ProviderInvalidQueryError("Invalid wkt provided.") + msg = "Invalid WKT string provided for the area query." + raise ProviderInvalidQueryError(msg, user_msg=msg) elif wkt.geom_type == "Polygon": lon, lat = EdrProvider.crs_converter(wkt.exterior.xy[0], wkt.exterior.xy[1], crs) else: - raise ProviderInvalidQueryError("Unknown WKT Type (Use Polygon).") + msg = "Unknown WKT string type for the area query (use Polygon)." + raise ProviderInvalidQueryError(msg, user_msg=msg) requested_coordinates = podpac.Coordinates([lat, lon], dims=["lat", "lon"], crs=crs) @@ -288,7 +336,8 @@ def instances(self, **kwargs) -> List[str]: The instances available in the collection. """ instances = set() - for layer in self.layers: + layers = self.get_layers(self.base_url) + for layer in layers: if layer.group == self.collection_id: instances.update(layer.time_instances()) return list(instances) @@ -382,7 +431,8 @@ def interpret_crs(crs: str | None) -> str: return settings.crs_84_pyproj_format # Pyproj acceptable format if crs.lower() not in [key.lower() for key in settings.EDR_CRS.keys()]: - raise ProviderInvalidQueryError("Invalid CRS provided.") + msg = f"Invalid CRS provided, expected one of {', '.join(settings.EDR_CRS.keys())}" + raise ProviderInvalidQueryError(msg, user_msg=msg) return crs @@ -539,6 +589,10 @@ def to_coverage_json( coordinates = next(iter(dataset.values())).coords x_arr, y_arr = EdrProvider.crs_converter(coordinates["lon"].values, coordinates["lat"].values, crs) + # Convert numpy array coordinates to a flattened list. + x_arr = list(x_arr.flatten()) + y_arr = list(y_arr.flatten()) + coverage_json = { "type": "Coverage", "domain": { @@ -546,13 +600,13 @@ def to_coverage_json( "domainType": "Grid", "axes": { "x": { - "start": x_arr[0], - "stop": x_arr[-1], + "start": x_arr[0] if len(x_arr) > 0 else None, + "stop": x_arr[-1] if len(x_arr) > 0 else None, "num": len(x_arr), }, "y": { - "start": y_arr[0], - "stop": y_arr[-1], + "start": y_arr[0] if len(y_arr) > 0 else None, + "stop": y_arr[-1] if len(y_arr) > 0 else None, "num": len(y_arr), }, }, @@ -635,7 +689,7 @@ def check_query_condition(conditional: bool, message: str): Raised if the conditional provided is true. """ if conditional: - raise ProviderInvalidQueryError(message) + raise ProviderInvalidQueryError(message, user_msg=message) @staticmethod def validate_datetime(datetime_string: str) -> bool: @@ -697,8 +751,8 @@ def get_native_coordinates( target_coordinates: podpac.Coordinates, source_time_instance: np.datetime64 | None, ) -> podpac.Coordinates: - """Find the intersecting coordinates between source and target coordinates. - Convert time instances to offsets for node evalutation. + """Find the intersecting latitude and longitude coordinates between the source and target. + Convert time instances to stacked time and forecast offsets for node evalutation. Parameters ---------- @@ -714,6 +768,9 @@ def get_native_coordinates( podpac.Coordinates The converted coordinates source coordinates intersecting with the target coordinates. """ + # Find intersections with target keeping source crs + source_intersection_coordinates = target_coordinates.intersect(source_coordinates, dims=["lat", "lon"]) + source_intersection_coordinates = source_intersection_coordinates.transform(source_coordinates.crs) # Handle conversion from times and instance to time and offsets if ( "forecastOffsetHr" in target_coordinates.udims @@ -728,14 +785,12 @@ def get_native_coordinates( # This modifies the time coordinates to account for the new forecast offset hour new_coordinates = podpac.Coordinates( - [[source_time_instance], time_deltas], - ["time", "forecastOffsetHr"], + [[[source_time_instance] * len(time_deltas), time_deltas]], + [["time", "forecastOffsetHr"]], crs=source_coordinates.crs, ) - source_coordinates = podpac.coordinates.merge_dims([source_coordinates.drop("time"), new_coordinates]) - - # Find intersections with target keeping source crs - source_intersection_coordinates = target_coordinates.intersect(source_coordinates) - source_intersection_coordinates = source_intersection_coordinates.transform(source_coordinates.crs) + source_intersection_coordinates = podpac.coordinates.merge_dims( + [source_intersection_coordinates.udrop(["time", "forecastOffsetHr"]), new_coordinates] + ) return source_intersection_coordinates diff --git a/ogc/edr/edr_routes.py b/ogc/edr/edr_routes.py index 1c752da..30b4625 100644 --- a/ogc/edr/edr_routes.py +++ b/ogc/edr/edr_routes.py @@ -15,6 +15,7 @@ from ogc import podpac as pogc from .edr_config import EdrConfig +from .edr_provider import EdrProvider class EdrRoutes(tl.HasTraits): @@ -51,7 +52,7 @@ def create_api(self) -> pygeoapi.api.API: # This is a bypass which is needed to get by a conditional check in pygeoapi. pygeoapi.plugin.PLUGINS["formatter"]["GeoTiff"] = "" pygeoapi.plugin.PLUGINS["formatter"]["CoverageJSON"] = "" - + EdrProvider.set_layers(self.base_url, self.layers) config = EdrConfig.get_configuration(self.base_url, self.layers) open_api = get_oas(config, fail_on_invalid_collection=False) return pygeoapi.api.API(config=deepcopy(config), openapi=open_api) diff --git a/ogc/edr/test/test_edr_provider.py b/ogc/edr/test/test_edr_provider.py index a7e89b6..6797baa 100644 --- a/ogc/edr/test/test_edr_provider.py +++ b/ogc/edr/test/test_edr_provider.py @@ -10,7 +10,7 @@ from pygeoapi.provider.base import ProviderInvalidQueryError -def get_provider_definition(layers: List[pogc.Layer]) -> Dict[str, Any]: +def get_provider_definition(base_url: str) -> Dict[str, Any]: """Define the provider definition which is typically handled by pygeoapi. Parameters @@ -28,7 +28,7 @@ def get_provider_definition(layers: List[pogc.Layer]) -> Dict[str, Any]: "default": True, "name": "ogc.edr.edr_provider.EdrProvider", "data": "Layers", - "layers": layers, + "base_url": base_url, "crs": ["https://www.opengis.net/def/crs/OGC/1.3/CRS84", "https://www.opengis.net/def/crs/EPSG/0/4326"], "format": {"name": "GeoJSON", "mimetype": "application/json"}, } @@ -42,12 +42,32 @@ def test_edr_provider_resources(layers: List[pogc.Layer]): layers : List[pogc.Layer] Layers provided by a test fixture. """ + base_url = "/" identifiers = [layer.identifier for layer in layers] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) - assert len(provider.layers) == len(layers) - assert all(layer.identifier in identifiers for layer in provider.layers) + assert len(provider.get_layers(base_url)) == len(layers) + assert all(layer.identifier in identifiers for layer in provider.get_layers(base_url)) + + +def test_edr_provider_resources_limited_by_url(layers: List[pogc.Layer]): + """Test the available resources of the EDR Provider class are limited by URL. + + Parameters + ---------- + layers : List[pogc.Layer] + Layers provided by a test fixture. + """ + base_url = "/" + invalid_url = "/invalid" + + provider = EdrProvider(provider_def=get_provider_definition(invalid_url)) + provider.set_layers(base_url, layers) + + assert len(provider.get_layers(invalid_url)) == 0 + assert len(provider.get_layers(base_url)) == len(layers) def test_edr_provider_get_instance_valid_id(layers: List[pogc.Layer]): @@ -58,9 +78,11 @@ def test_edr_provider_get_instance_valid_id(layers: List[pogc.Layer]): layers : List[pogc.Layer] Layers provided by a test fixture. """ + base_url = "/" time_instance = next(iter(layers[0].time_instances())) - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) assert provider.get_instance(time_instance) == time_instance @@ -73,7 +95,9 @@ def test_edr_provider_get_instance_invalid_id(layers: List[pogc.Layer]): layers : List[pogc.Layer] Layers provided by a test fixture. """ - provider = EdrProvider(provider_def=get_provider_definition(layers)) + base_url = "/" + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) assert provider.get_instance("invalid") is None @@ -86,9 +110,11 @@ def test_edr_provider_parameter_keys(layers: List[pogc.Layer]): layers : List[pogc.Layer] Layers provided by a test fixture. """ + base_url = "/" identifiers = [layer.identifier for layer in layers] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) parameters = provider.parameters assert len(list(parameters.keys())) == len(layers) @@ -103,10 +129,12 @@ def test_edr_provider_instances(layers: List[pogc.Layer]): layers : List[pogc.Layer] Layers provided by a test fixture. """ + base_url = "/" instance_sets = [layer.time_instances() for layer in layers] time_instances = set().union(*instance_sets) - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) instances = provider.instances() assert len(instances) == len(time_instances) @@ -121,9 +149,11 @@ def test_edr_provider_get_fields(layers: List[pogc.Layer]): layers : List[pogc.Layer] Layers provided by a test fixture. """ + base_url = "/" identifiers = [layer.identifier for layer in layers] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) fields = provider.get_fields() assert len(fields.keys()) == len(layers) @@ -143,18 +173,18 @@ def test_edr_provider_position_request_valid_wkt( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal del args["bbox"] args["wkt"] = Point(5.2, 52.1) parameter_name = single_layer_cube_args_internal["select_properties"][0] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) response = provider.position(**args) - assert set(response["domain"]["ranges"][parameter_name]["axisNames"]) == set( - layers[0].node.find_coordinates()[0].dims - ) + assert set(response["domain"]["ranges"][parameter_name]["axisNames"]) == {"lat", "lon", "time"} assert np.prod(np.array(response["domain"]["ranges"][parameter_name]["shape"])) == len( response["domain"]["ranges"][parameter_name]["values"] ) @@ -173,11 +203,13 @@ def test_edr_provider_position_request_invalid_wkt( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal del args["bbox"] args["wkt"] = "invalid" - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) with pytest.raises(ProviderInvalidQueryError): provider.position(**args) @@ -196,10 +228,12 @@ def test_edr_provider_position_request_invalid_property( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal args["select_properties"] = "invalid" - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) with pytest.raises(ProviderInvalidQueryError): provider.position(**args) @@ -218,16 +252,16 @@ def test_edr_provider_cube_request_valid_bbox( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal parameter_name = single_layer_cube_args_internal["select_properties"][0] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) response = provider.cube(**args) - assert set(response["domain"]["ranges"][parameter_name]["axisNames"]) == set( - layers[0].node.find_coordinates()[0].dims - ) + assert set(response["domain"]["ranges"][parameter_name]["axisNames"]) == {"lat", "lon", "time"} assert np.prod(np.array(response["domain"]["ranges"][parameter_name]["shape"])) == len( response["domain"]["ranges"][parameter_name]["values"] ) @@ -246,10 +280,12 @@ def test_edr_provider_cube_request_invalid_bbox( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal args["bbox"] = "invalid" - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) with pytest.raises(ProviderInvalidQueryError): provider.cube(**args) @@ -268,10 +304,12 @@ def test_edr_provider_cube_request_invalid_altitude( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal args["z"] = "invalid" - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) with pytest.raises(ProviderInvalidQueryError): provider.position(**args) @@ -288,18 +326,18 @@ def test_edr_provider_area_request_valid_wkt(layers: List[pogc.Layer], single_la single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal del args["bbox"] args["wkt"] = Polygon(((-180.0, -90.0), (-180.0, 90.0), (180.0, -90.0), (180.0, 90.0))) parameter_name = single_layer_cube_args_internal["select_properties"][0] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) response = provider.area(**args) - assert set(response["domain"]["ranges"][parameter_name]["axisNames"]) == set( - layers[0].node.find_coordinates()[0].dims - ) + assert set(response["domain"]["ranges"][parameter_name]["axisNames"]) == {"lat", "lon", "time"} assert np.prod(np.array(response["domain"]["ranges"][parameter_name]["shape"])) == len( response["domain"]["ranges"][parameter_name]["values"] ) @@ -318,11 +356,13 @@ def test_edr_provider_area_request_invalid_wkt( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal del args["bbox"] args["wkt"] = "invalid" - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) with pytest.raises(ProviderInvalidQueryError): provider.area(**args) @@ -341,10 +381,12 @@ def test_edr_provider_cube_request_invalid_datetime( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal args["datetime_"] = "10_24/2025" - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) with pytest.raises(ProviderInvalidQueryError): provider.cube(**args) @@ -363,11 +405,13 @@ def test_edr_provider_cube_request_valid_geotiff_format( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal args["format_"] = "geotiff" parameter_name = single_layer_cube_args_internal["select_properties"][0] - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) response = provider.cube(**args) @@ -388,6 +432,7 @@ def test_edr_provider_cube_request_valid_geotiff_format_multiple_parameters( single_layer_cube_args_internal : Dict[str, Any] Single layer arguments with internal pygeoapi keys provided by a test fixture. """ + base_url = "/" args = single_layer_cube_args_internal args["format_"] = "geotiff" @@ -396,7 +441,8 @@ def test_edr_provider_cube_request_valid_geotiff_format_multiple_parameters( selected_layers = [layer.identifier for layer in layers if layer.group == group] args["select_properties"] = selected_layers - provider = EdrProvider(provider_def=get_provider_definition(layers)) + provider = EdrProvider(provider_def=get_provider_definition(base_url)) + provider.set_layers(base_url, layers) response = provider.cube(**args) buffer = io.BytesIO(base64.b64decode(response["fp"])) diff --git a/ogc/edr/test/test_edr_routes.py b/ogc/edr/test/test_edr_routes.py index caa5cd6..a1cd183 100644 --- a/ogc/edr/test/test_edr_routes.py +++ b/ogc/edr/test/test_edr_routes.py @@ -226,7 +226,7 @@ def test_edr_routes_collection_query(layers: List[pogc.Layer], single_layer_cube assert status == HTTPStatus.OK - assert content["domain"]["ranges"][parameter_name]["axisNames"] == list(layers[0].node.find_coordinates()[0].dims) + assert set(content["domain"]["ranges"][parameter_name]["axisNames"]) == {"lat", "lon", "time"} assert np.prod(np.array(content["domain"]["ranges"][parameter_name]["shape"])) == len( content["domain"]["ranges"][parameter_name]["values"] ) diff --git a/ogc/servers.py b/ogc/servers.py index 8b4932e..b7b859e 100755 --- a/ogc/servers.py +++ b/ogc/servers.py @@ -288,11 +288,15 @@ def wrapper(*args, **kwargs) -> Response: allowed_chars = r'-A-Za-z0-9 +.,_/:*\{\}\(\)\[\]"' match_one_unallowed_char = "[^%s]" % allowed_chars filtered_args = { + # Convert keys to lower-case. # Find every unallowed char in the value and replace it # with nothing (remove it). - k: re.sub(match_one_unallowed_char, "", str(v)) + k.lower(): re.sub(match_one_unallowed_char, "", str(v)) for (k, v) in request.args.items() } + # Replace format with its lowercase version to match pygeoapi expectations + if filtered_args.get("f", None): + filtered_args["f"] = filtered_args["f"].lower() # Replace the arguments with the filtered option request.args = ImmutableMultiDict(filtered_args) pygeoapi_request = APIRequest.from_flask(request, ["en"])