diff --git a/Dockerfile b/Dockerfile index 71505ba98..04f1a20db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ # # ================================================================= -FROM ubuntu:jammy-20240627.1 +FROM ubuntu:jammy-20240911.1 LABEL maintainer="Just van den Broecke " diff --git a/docs/source/_templates/indexsidebar.html b/docs/source/_templates/indexsidebar.html index 8000f32dd..a782c3ba6 100644 --- a/docs/source/_templates/indexsidebar.html +++ b/docs/source/_templates/indexsidebar.html @@ -16,11 +16,14 @@ OGC Reference Implementation + + OGC Reference Implementation + OSGeo Project - - FOSS4G Conference + + FOSS4G Conference

diff --git a/docs/source/conf.py b/docs/source/conf.py index dff89f13c..6a9e11d61 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -112,7 +112,7 @@ def __getattr__(cls, name): # built documents. # # The short X.Y version. -version = '0.18.dev0' +version = '0.19.dev0' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/source/data-publishing/ogcapi-features.rst b/docs/source/data-publishing/ogcapi-features.rst index 00d48a020..12478c1d2 100644 --- a/docs/source/data-publishing/ogcapi-features.rst +++ b/docs/source/data-publishing/ogcapi-features.rst @@ -145,7 +145,11 @@ To publish an ESRI `Feature Service`_ or `Map Service`_ specify the URL for the * ``id_field`` will often be ``OBJECTID``, ``objectid``, or ``FID``. * If the map or feature service is not shared publicly, the ``username`` and ``password`` fields can be set in the - configuration to authenticate into the service. + configuration to authenticate to the service. +* If the map or feature service is self-hosted and not shared publicly, the ``token_service`` and optional ``referer`` fields + can be set in the configuration to authenticate to the service. + +To publish from an ArcGIS online hosted service: .. code-block:: yaml @@ -158,6 +162,24 @@ To publish an ESRI `Feature Service`_ or `Map Service`_ specify the URL for the crs: 4326 # Optional crs (default is EPSG:4326) username: username # Optional ArcGIS username password: password # Optional ArcGIS password + token_service: https://your.server.com/arcgis/sharing/rest/generateToken # optional URL to your generateToken service + referer: https://your.server.com # optional referer, defaults to https://www.arcgis.com if not set + +To publish from a self-hosted service that is not publicly accessible, the ``token_service`` field is required: + +.. code-block:: yaml + + providers: + - type: feature + name: ESRI + data: https://your.server.com/arcgis/rest/services/your-layer/MapServer/0 + id_field: objectid + time_field: date_in_your_device_time_zone # Optional time field + crs: 4326 # Optional crs (default is EPSG:4326) + username: username # Optional ArcGIS username + password: password # Optional ArcGIS password + token_service: https://your.server.com/arcgis/sharing/rest/generateToken # Optional url to your generateToken service + referer: https://your.server.com # Optional referer, defaults to https://www.arcgis.com if not set GeoJSON ^^^^^^^ diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index a98b218c2..d41ff33f4 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -14,12 +14,12 @@ Features * OGC API - Features * OGC API - Environmental Data Retrieval * OGC API - Tiles + * OGC API - Processes * additionally implements * OGC API - Coverages * OGC API - Maps - * OGC API - Processes * OGC API - Records * SpatioTemporal Asset Library @@ -52,7 +52,7 @@ Standards are at the core of pygeoapi. Below is the project's standards support `OGC API - Coverages`_,Implementing `OGC API - Maps`_,Implementing `OGC API - Tiles`_,Reference Implementation - `OGC API - Processes`_,Implementing + `OGC API - Processes`_,Compliant `OGC API - Records`_,Implementing `OGC API - Environmental Data Retrieval`_,Reference Implementation `SpatioTemporal Asset Catalog`_,Implementing diff --git a/pygeoapi/__init__.py b/pygeoapi/__init__.py index e66240e72..9c5d35688 100644 --- a/pygeoapi/__init__.py +++ b/pygeoapi/__init__.py @@ -30,7 +30,7 @@ # # ================================================================= -__version__ = '0.18.dev0' +__version__ = '0.19.dev0' import click try: diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index 661f5cd80..008b28cb7 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -121,29 +121,22 @@ def get_collection_queryables(api: API, request: Union[APIRequest, Any], HTTPStatus.NOT_FOUND, headers, request.format, 'NotFound', msg) LOGGER.debug('Creating collection queryables') - try: - LOGGER.debug('Loading feature provider') - p = load_plugin('provider', get_provider_by_type( - api.config['resources'][dataset]['providers'], 'feature')) - except ProviderTypeError: + + p = None + for pt in ['feature', 'coverage', 'record']: try: - LOGGER.debug('Loading coverage provider') + LOGGER.debug(f'Loading {pt} provider') p = load_plugin('provider', get_provider_by_type( - api.config['resources'][dataset]['providers'], 'coverage')) # noqa + api.config['resources'][dataset]['providers'], pt)) + break except ProviderTypeError: - LOGGER.debug('Loading record provider') - p = load_plugin('provider', get_provider_by_type( - api.config['resources'][dataset]['providers'], 'record')) - finally: - msg = 'queryables not available for this collection' - return api.get_exception( - HTTPStatus.BAD_REQUEST, headers, request.format, - 'NoApplicableError', msg) + LOGGER.debug(f'Providing type {pt} not found') - except ProviderGenericError as err: + if p is None: + msg = 'queryables not available for this collection' return api.get_exception( - err.http_status_code, headers, request.format, - err.ogc_exception_code, err.message) + HTTPStatus.BAD_REQUEST, headers, request.format, + 'NoApplicableError', msg) queryables = { 'type': 'object', diff --git a/pygeoapi/provider/esri.py b/pygeoapi/provider/esri.py index 8179a705b..47d74e2b9 100644 --- a/pygeoapi/provider/esri.py +++ b/pygeoapi/provider/esri.py @@ -62,8 +62,9 @@ def __init__(self, provider_def): self.crs = provider_def.get('crs', '4326') self.username = provider_def.get('username') self.password = provider_def.get('password') + self.token_url = provider_def.get('token_service', ARCGIS_URL) + self.token_referer = provider_def.get('referer', GENERATE_TOKEN_URL) self.token = None - self.session = Session() self.login() @@ -194,16 +195,15 @@ def login(self): msg = 'Missing ESRI login information, not setting token' LOGGER.debug(msg) return - params = { 'f': 'pjson', 'username': self.username, 'password': self.password, - 'referer': ARCGIS_URL + 'referer': self.token_referer } LOGGER.debug('Logging in') - with self.session.post(GENERATE_TOKEN_URL, data=params) as r: + with self.session.post(self.token_url, data=params) as r: self.token = r.json().get('token') # https://enterprise.arcgis.com/en/server/latest/administer/windows/about-arcgis-tokens.htm self.session.headers.update({ diff --git a/pygeoapi/provider/rasterio_.py b/pygeoapi/provider/rasterio_.py index 69d2dbca8..3b0fbc2c7 100644 --- a/pygeoapi/provider/rasterio_.py +++ b/pygeoapi/provider/rasterio_.py @@ -80,6 +80,8 @@ def get_fields(self): dtype2 = dtype if dtype.startswith('float'): dtype2 = 'number' + elif dtype.startswith('int'): + dtype2 = 'integer' self._fields[i2] = { 'title': name, @@ -240,16 +242,15 @@ def query(self, properties=[], subsets={}, bbox=None, bbox_crs=4326, out_meta['units'] = _data.units LOGGER.debug('Serializing data in memory') - with MemoryFile() as memfile: - with memfile.open(**out_meta) as dest: - dest.write(out_image) - - if format_ == 'json': - LOGGER.debug('Creating output in CoverageJSON') - out_meta['bands'] = args['indexes'] - return self.gen_covjson(out_meta, out_image) - - else: # return data in native format + if format_ == 'json': + LOGGER.debug('Creating output in CoverageJSON') + out_meta['bands'] = args['indexes'] + return self.gen_covjson(out_meta, out_image) + + else: # return data in native format + with MemoryFile() as memfile: + with memfile.open(**out_meta) as dest: + dest.write(out_image) LOGGER.debug('Returning data in native format') return memfile.read() diff --git a/pygeoapi/provider/xarray_.py b/pygeoapi/provider/xarray_.py index 585879282..814444804 100644 --- a/pygeoapi/provider/xarray_.py +++ b/pygeoapi/provider/xarray_.py @@ -85,13 +85,23 @@ def __init__(self, provider_def): else: data_to_open = self.data - self._data = open_func(data_to_open) + try: + self._data = open_func(data_to_open) + except ValueError as err: + # Manage non-cf-compliant time dimensions + if 'time' in str(err): + self._data = open_func(self.data, decode_times=False) + else: + raise err + self.storage_crs = self._parse_storage_crs(provider_def) self._coverage_properties = self._get_coverage_properties() self.axes = [self._coverage_properties['x_axis_label'], self._coverage_properties['y_axis_label'], self._coverage_properties['time_axis_label']] + self.time_axis_covjson = provider_def.get('time_axis_covjson') \ + or self.time_field self.get_fields() except Exception as err: @@ -101,15 +111,17 @@ def __init__(self, provider_def): def get_fields(self): if not self._fields: for key, value in self._data.variables.items(): - if len(value.shape) >= 3: + if key not in self._data.coords: LOGGER.debug('Adding variable') dtype = value.dtype if dtype.name.startswith('float'): - dtype = 'number' + dtype = 'float' + elif dtype.name.startswith('int'): + dtype = 'integer' self._fields[key] = { 'type': dtype, - 'title': value.attrs['long_name'], + 'title': value.attrs.get('long_name'), 'x-ogc-unit': value.attrs.get('units') } @@ -142,9 +154,9 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, data = self._data[[*properties]] - if any([self._coverage_properties['x_axis_label'] in subsets, - self._coverage_properties['y_axis_label'] in subsets, - self._coverage_properties['time_axis_label'] in subsets, + if any([self._coverage_properties.get('x_axis_label') in subsets, + self._coverage_properties.get('y_axis_label') in subsets, + self._coverage_properties.get('time_axis_label') in subsets, datetime_ is not None]): LOGGER.debug('Creating spatio-temporal subset') @@ -163,18 +175,36 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, self._coverage_properties['y_axis_label'] in subsets, len(bbox) > 0]): msg = 'bbox and subsetting by coordinates are exclusive' - LOGGER.warning(msg) + LOGGER.error(msg) raise ProviderQueryError(msg) else: - query_params[self._coverage_properties['x_axis_label']] = \ - slice(bbox[0], bbox[2]) - query_params[self._coverage_properties['y_axis_label']] = \ - slice(bbox[1], bbox[3]) + x_axis_label = self._coverage_properties['x_axis_label'] + x_coords = data.coords[x_axis_label] + if x_coords.values[0] > x_coords.values[-1]: + LOGGER.debug( + 'Reversing slicing of x axis from high to low' + ) + query_params[x_axis_label] = slice(bbox[2], bbox[0]) + else: + query_params[x_axis_label] = slice(bbox[0], bbox[2]) + y_axis_label = self._coverage_properties['y_axis_label'] + y_coords = data.coords[y_axis_label] + if y_coords.values[0] > y_coords.values[-1]: + LOGGER.debug( + 'Reversing slicing of y axis from high to low' + ) + query_params[y_axis_label] = slice(bbox[3], bbox[1]) + else: + query_params[y_axis_label] = slice(bbox[1], bbox[3]) LOGGER.debug('bbox_crs is not currently handled') if datetime_ is not None: - if self._coverage_properties['time_axis_label'] in subsets: + if self._coverage_properties['time_axis_label'] is None: + msg = 'Dataset does not contain a time axis' + LOGGER.error(msg) + raise ProviderQueryError(msg) + elif self._coverage_properties['time_axis_label'] in subsets: msg = 'datetime and temporal subsetting are exclusive' LOGGER.error(msg) raise ProviderQueryError(msg) @@ -196,13 +226,15 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, LOGGER.warning(err) raise ProviderQueryError(err) - if (any([data.coords[self.x_field].size == 0, - data.coords[self.y_field].size == 0, - data.coords[self.time_field].size == 0])): + if any(size == 0 for size in data.sizes.values()): msg = 'No data found' LOGGER.warning(msg) raise ProviderNoDataError(msg) + if format_ == 'json': + # json does not support float32 + data = _convert_float32_to_float64(data) + out_meta = { 'bbox': [ data.coords[self.x_field].values[0], @@ -210,18 +242,20 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, data.coords[self.x_field].values[-1], data.coords[self.y_field].values[-1] ], - "time": [ - _to_datetime_string(data.coords[self.time_field].values[0]), - _to_datetime_string(data.coords[self.time_field].values[-1]) - ], "driver": "xarray", "height": data.sizes[self.y_field], "width": data.sizes[self.x_field], - "time_steps": data.sizes[self.time_field], "variables": {var_name: var.attrs for var_name, var in data.variables.items()} } + if self.time_field is not None: + out_meta['time'] = [ + _to_datetime_string(data.coords[self.time_field].values[0]), + _to_datetime_string(data.coords[self.time_field].values[-1]), + ] + out_meta["time_steps"] = data.sizes[self.time_field] + LOGGER.debug('Serializing data in memory') if format_ == 'json': LOGGER.debug('Creating output in CoverageJSON') @@ -230,9 +264,11 @@ def query(self, properties=[], subsets={}, bbox=[], bbox_crs=4326, LOGGER.debug('Returning data in native zarr format') return _get_zarr_data(data) else: # return data in native format - with tempfile.TemporaryFile() as fp: + with tempfile.NamedTemporaryFile() as fp: LOGGER.debug('Returning data in native NetCDF format') - fp.write(data.to_netcdf()) + data.to_netcdf( + fp.name + ) # we need to pass a string to be able to use the "netcdf4" engine # noqa fp.seek(0) return fp.read() @@ -248,7 +284,7 @@ def gen_covjson(self, metadata, data, fields): """ LOGGER.debug('Creating CoverageJSON domain') - minx, miny, maxx, maxy = metadata['bbox'] + startx, starty, stopx, stopy = metadata['bbox'] mint, maxt = metadata['time'] selected_fields = { @@ -256,20 +292,6 @@ def gen_covjson(self, metadata, data, fields): if key in fields } - try: - tmp_min = data.coords[self.y_field].values[0] - except IndexError: - tmp_min = data.coords[self.y_field].values - try: - tmp_max = data.coords[self.y_field].values[-1] - except IndexError: - tmp_max = data.coords[self.y_field].values - - if tmp_min > tmp_max: - LOGGER.debug(f'Reversing direction of {self.y_field}') - miny = tmp_max - maxy = tmp_min - cj = { 'type': 'Coverage', 'domain': { @@ -277,19 +299,14 @@ def gen_covjson(self, metadata, data, fields): 'domainType': 'Grid', 'axes': { 'x': { - 'start': minx, - 'stop': maxx, + 'start': startx, + 'stop': stopx, 'num': metadata['width'] }, 'y': { - 'start': maxy, - 'stop': miny, + 'start': starty, + 'stop': stopy, 'num': metadata['height'] - }, - self.time_field: { - 'start': mint, - 'stop': maxt, - 'num': metadata['time_steps'] } }, 'referencing': [{ @@ -304,10 +321,15 @@ def gen_covjson(self, metadata, data, fields): 'ranges': {} } + if self.time_field is not None: + cj['domain']['axes'][self.time_axis_covjson] = { + 'values': [str(i) for i in data.coords[self.time_field].values] + } + for key, value in selected_fields.items(): parameter = { 'type': 'Parameter', - 'description': value['title'], + 'description': {'en': value['title']}, 'unit': { 'symbol': value['x-ogc-unit'] }, @@ -322,21 +344,26 @@ def gen_covjson(self, metadata, data, fields): cj['parameters'][key] = parameter data = data.fillna(None) - data = _convert_float32_to_float64(data) try: for key, value in selected_fields.items(): cj['ranges'][key] = { 'type': 'NdArray', 'dataType': value['type'], - 'axisNames': [ - 'y', 'x', self._coverage_properties['time_axis_label'] - ], - 'shape': [metadata['height'], - metadata['width'], - metadata['time_steps']] + 'axisNames': [], + 'shape': [] } cj['ranges'][key]['values'] = data[key].values.flatten().tolist() # noqa + + if self.time_field is not None: + cj['ranges'][key]['axisNames'].append( + self.time_axis_covjson + ) + cj['ranges'][key]['shape'].append(metadata['time_steps']) + cj['ranges'][key]['axisNames'].append('y') + cj['ranges'][key]['axisNames'].append('x') + cj['ranges'][key]['shape'].append(metadata['height']) + cj['ranges'][key]['shape'].append(metadata['width']) except IndexError as err: LOGGER.warning(err) raise ProviderQueryError('Invalid query parameter') @@ -382,31 +409,37 @@ def _get_coverage_properties(self): self._data.coords[self.x_field].values[-1], self._data.coords[self.y_field].values[-1], ], - 'time_range': [ - _to_datetime_string( - self._data.coords[self.time_field].values[0] - ), - _to_datetime_string( - self._data.coords[self.time_field].values[-1] - ) - ], 'bbox_crs': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', 'crs_type': 'GeographicCRS', 'x_axis_label': self.x_field, 'y_axis_label': self.y_field, - 'time_axis_label': self.time_field, 'width': self._data.sizes[self.x_field], 'height': self._data.sizes[self.y_field], - 'time': self._data.sizes[self.time_field], - 'time_duration': self.get_time_coverage_duration(), 'bbox_units': 'degrees', - 'resx': np.abs(self._data.coords[self.x_field].values[1] - - self._data.coords[self.x_field].values[0]), - 'resy': np.abs(self._data.coords[self.y_field].values[1] - - self._data.coords[self.y_field].values[0]), - 'restime': self.get_time_resolution() + 'resx': np.abs( + self._data.coords[self.x_field].values[1] + - self._data.coords[self.x_field].values[0] + ), + 'resy': np.abs( + self._data.coords[self.y_field].values[1] + - self._data.coords[self.y_field].values[0] + ), } + if self.time_field is not None: + properties['time_axis_label'] = self.time_field + properties['time_range'] = [ + _to_datetime_string( + self._data.coords[self.time_field].values[0] + ), + _to_datetime_string( + self._data.coords[self.time_field].values[-1] + ), + ] + properties['time'] = self._data.sizes[self.time_field] + properties['time_duration'] = self.get_time_coverage_duration() + properties['restime'] = self.get_time_resolution() + # Update properties based on the xarray's CRS epsg_code = self.storage_crs.to_epsg() LOGGER.debug(f'{epsg_code}') @@ -425,10 +458,12 @@ def _get_coverage_properties(self): properties['axes'] = [ properties['x_axis_label'], - properties['y_axis_label'], - properties['time_axis_label'] + properties['y_axis_label'] ] + if self.time_field is not None: + properties['axes'].append(properties['time_axis_label']) + return properties @staticmethod @@ -455,7 +490,8 @@ def get_time_resolution(self): :returns: time resolution string """ - if self._data[self.time_field].size > 1: + if self.time_field is not None \ + and self._data[self.time_field].size > 1: time_diff = (self._data[self.time_field][1] - self._data[self.time_field][0]) @@ -472,6 +508,9 @@ def get_time_coverage_duration(self): :returns: time coverage duration string """ + if self.time_field is None: + return None + dur = self._data[self.time_field][-1] - self._data[self.time_field][0] ms_difference = dur.values.astype('timedelta64[ms]').astype(np.double) @@ -500,7 +539,7 @@ def _parse_grid_mapping(self): for var_name, var in self._data.variables.items(): if all(dim in var.dims for dim in spatiotemporal_dims): try: - grid_mapping_name = self._data[var_name].attrs['grid_mapping'] # noqa + grid_mapping_name = self._data[var_name].attrs['grid_mapping'] # noqa LOGGER.debug(f'Grid mapping: {grid_mapping_name}') except KeyError as err: LOGGER.debug(err) @@ -634,7 +673,7 @@ def _convert_float32_to_float64(data): for var_name in data.variables: if data[var_name].dtype == 'float32': og_attrs = data[var_name].attrs - data[var_name] = data[var_name].astype('float64') + data[var_name] = data[var_name].astype('float64', copy=False) data[var_name].attrs = og_attrs return data diff --git a/pygeoapi/templates/collections/edr/query.html b/pygeoapi/templates/collections/edr/query.html index ac7f17d2d..d3af4ce3d 100644 --- a/pygeoapi/templates/collections/edr/query.html +++ b/pygeoapi/templates/collections/edr/query.html @@ -139,7 +139,7 @@ if (!firstLayer) { firstLayer = layer; layer.on('afterAdd', () => { - zoomToLayers([layers]) + zoomToLayers([layer]) if (!cov.coverages) { if (isVerticalProfile(cov) || isTimeSeries(cov)) { layer.openPopup(); diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 0477ef81f..faad9ed09 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -575,7 +575,7 @@ def test_conformance(config, api_): assert isinstance(root, dict) assert 'conformsTo' in root - assert len(root['conformsTo']) == 37 + assert len(root['conformsTo']) == 42 assert 'http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs' \ in root['conformsTo'] @@ -604,7 +604,7 @@ def test_describe_collections(config, api_): collections = json.loads(response) assert len(collections) == 2 - assert len(collections['collections']) == 9 + assert len(collections['collections']) == 10 assert len(collections['links']) == 3 rsp_headers, code, response = api_.describe_collections(req, 'foo') diff --git a/tests/api/test_environmental_data_retrieval.py b/tests/api/test_environmental_data_retrieval.py index 35abed7fd..2b43f9a70 100644 --- a/tests/api/test_environmental_data_retrieval.py +++ b/tests/api/test_environmental_data_retrieval.py @@ -123,7 +123,7 @@ def test_get_collection_edr_query(config, api_): # bounded date range req = mock_api_request({ 'coords': 'POINT(11 11)', - 'datetime': '2000-01-17/2000-06-16' + 'datetime': '2000-01-17/2000-08-16' }) rsp_headers, code, response = get_collection_edr_query( api_, req, 'icoads-sst', None, 'position') @@ -132,14 +132,17 @@ def test_get_collection_edr_query(config, api_): data = json.loads(response) time_dict = data['domain']['axes']['TIME'] - assert time_dict['start'] == '2000-02-15T16:29:05.999999999' - assert time_dict['stop'] == '2000-06-16T10:25:30.000000000' - assert time_dict['num'] == 5 + assert time_dict['values'] == ['2000-06-16T10:25:30.000000000', + '2000-07-16T20:54:36.000000000', + '2000-08-16T07:23:42.000000000'] + # assert time_dict['start'] == '2000-06-16T10:25:30.000000000' + # assert time_dict['stop'] == '2000-08-16T07:23:42.000000000' + # assert time_dict['num'] == 3 # unbounded date range - start req = mock_api_request({ 'coords': 'POINT(11 11)', - 'datetime': '../2000-06-16' + 'datetime': '../2000-08-16' }) rsp_headers, code, response = get_collection_edr_query( api_, req, 'icoads-sst', None, 'position') @@ -148,14 +151,17 @@ def test_get_collection_edr_query(config, api_): data = json.loads(response) time_dict = data['domain']['axes']['TIME'] - assert time_dict['start'] == '2000-01-16T06:00:00.000000000' - assert time_dict['stop'] == '2000-06-16T10:25:30.000000000' - assert time_dict['num'] == 6 + assert time_dict['values'] == ['2000-06-16T10:25:30.000000000', + '2000-07-16T20:54:36.000000000', + '2000-08-16T07:23:42.000000000'] + # assert time_dict['start'] == '2000-06-16T10:25:30.000000000' + # assert time_dict['stop'] == '2000-08-16T07:23:42.000000000' + # assert time_dict['num'] == 3 # unbounded date range - end req = mock_api_request({ 'coords': 'POINT(11 11)', - 'datetime': '2000-06-16/..' + 'datetime': '2000-08-16/..' }) rsp_headers, code, response = get_collection_edr_query( api_, req, 'icoads-sst', None, 'position') @@ -163,14 +169,18 @@ def test_get_collection_edr_query(config, api_): data = json.loads(response) time_dict = data['domain']['axes']['TIME'] - - assert time_dict['start'] == '2000-06-16T10:25:30.000000000' - assert time_dict['stop'] == '2000-12-16T01:20:05.999999996' - assert time_dict['num'] == 7 + assert time_dict['values'] == ['2000-08-16T07:23:42.000000000', + '2000-09-15T17:52:48.000000000', + '2000-10-16T04:21:54.000000000', + '2000-11-15T14:51:00.000000000', + '2000-12-16T01:20:05.999999996'] + # assert time_dict['start'] == '2000-08-16T07:23:42.000000000' + # assert time_dict['stop'] == '2000-12-16T01:20:05.999999996' + # assert time_dict['num'] == 7 # some data req = mock_api_request({ - 'coords': 'POINT(11 11)', 'datetime': '2000-01-16' + 'coords': 'POINT(11 11)', 'datetime': '2000-06-16' }) rsp_headers, code, response = get_collection_edr_query( api_, req, 'icoads-sst', None, 'position') diff --git a/tests/api/test_itemtypes.py b/tests/api/test_itemtypes.py index 2cd445898..ae19c28d6 100644 --- a/tests/api/test_itemtypes.py +++ b/tests/api/test_itemtypes.py @@ -79,6 +79,14 @@ def test_get_collection_queryables(config, api_): assert 'properties' in queryables assert len(queryables['properties']) == 5 + req = mock_api_request({'f': 'json'}) + rsp_headers, code, response = get_collection_queryables(api_, req, 'canada-metadata') # noqa + assert rsp_headers['Content-Type'] == 'application/schema+json' + queryables = json.loads(response) + + assert 'properties' in queryables + assert len(queryables['properties']) == 10 + # test with provider filtered properties api_.config['resources']['obs']['providers'][0]['properties'] = ['stn_id'] diff --git a/tests/data/coads_sst.nc b/tests/data/coads_sst.nc index 94434632c..57e68149d 100644 Binary files a/tests/data/coads_sst.nc and b/tests/data/coads_sst.nc differ diff --git a/tests/pygeoapi-test-config.yml b/tests/pygeoapi-test-config.yml index 95a868631..58b62484f 100644 --- a/tests/pygeoapi-test-config.yml +++ b/tests/pygeoapi-test-config.yml @@ -398,6 +398,44 @@ resources: name: png mimetype: image/png + canada-metadata: + type: collection + title: + en: Open Canada sample data + fr: Exemple de donn\u00e9es Canada Ouvert + description: + en: Sample metadata records from open.canada.ca + fr: Exemples d'enregistrements de m\u00e9tadonn\u00e9es sur ouvert.canada.ca + keywords: + en: + - canada + - open data + fr: + - canada + - donn\u00e9es ouvertes + links: + - type: text/html + rel: canonical + title: information + href: https://open.canada.ca/en/open-data + hreflang: en-CA + - type: text/html + rel: alternate + title: informations + href: https://ouvert.canada.ca/fr/donnees-ouvertes + hreflang: fr-CA + extents: + spatial: + bbox: [-180,-90,180,90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + providers: + - type: record + name: TinyDBCatalogue + data: tests/data/open.canada.ca/sample-records.tinydb + id_field: externalId + time_field: created + title_field: title + hello-world: type: process processor: diff --git a/tests/test_util.py b/tests/test_util.py index c71ce80a0..d15aac321 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -172,7 +172,7 @@ def test_path_basename(): def test_filter_dict_by_key_value(config): collections = util.filter_dict_by_key_value(config['resources'], 'type', 'collection') - assert len(collections) == 9 + assert len(collections) == 10 notfound = util.filter_dict_by_key_value(config['resources'], 'type', 'foo') diff --git a/tests/test_xarray_zarr_provider.py b/tests/test_xarray_zarr_provider.py index 5163b32a6..ec014e655 100644 --- a/tests/test_xarray_zarr_provider.py +++ b/tests/test_xarray_zarr_provider.py @@ -30,6 +30,7 @@ from numpy import float64, int64 import pytest +import xarray as xr from pygeoapi.provider.xarray_ import XarrayProvider from pygeoapi.util import json_serial @@ -53,6 +54,20 @@ def config(): } +@pytest.fixture() +def config_no_time(tmp_path): + ds = xr.open_zarr(path) + ds = ds.sel(time=ds.time[0]) + ds = ds.drop_vars('time') + ds.to_zarr(tmp_path / 'no_time.zarr') + return { + 'name': 'zarr', + 'type': 'coverage', + 'data': str(tmp_path / 'no_time.zarr'), + 'format': {'name': 'zarr', 'mimetype': 'application/zip'}, + } + + def test_provider(config): p = XarrayProvider(config) @@ -85,3 +100,14 @@ def test_numpy_json_serial(): d = float64(500.00000005) assert json_serial(d) == 500.00000005 + + +def test_no_time(config_no_time): + p = XarrayProvider(config_no_time) + + assert len(p.fields) == 4 + assert p.axes == ['lon', 'lat'] + + coverage = p.query(format='json') + + assert sorted(coverage['domain']['axes'].keys()) == ['x', 'y']