From 09ad6f8fce75bfacb54c7f90d08bd4cead2a238e Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Sat, 16 Apr 2022 18:26:55 -0400 Subject: [PATCH] Hierarchical collections (#885) * add support for hierarchical collections * fix JSON rendering in docs * fix JSON rendering in docs * fix tests * update docs * update HTML templates with collections path * fix template error * add test --- docs/source/configuration.rst | 42 +++++- docs/source/data-publishing/stac.rst | 2 - pygeoapi/api.py | 139 ++++++++++-------- pygeoapi/flask_app.py | 44 +++--- pygeoapi/starlette_app.py | 50 +++---- .../templates/collections/collection.html | 8 +- .../collections/coverage/domainset.html | 2 +- .../collections/coverage/rangetype.html | 2 +- pygeoapi/templates/collections/index.html | 4 +- .../templates/collections/items/item.html | 2 +- .../templates/collections/queryables.html | 2 +- .../templates/collections/tiles/index.html | 2 +- .../templates/collections/tiles/metadata.html | 2 +- tests/pygeoapi-test-config.yml | 2 +- tests/test_api.py | 20 ++- 15 files changed, 192 insertions(+), 131 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 73d76d3..62a8cbc 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -119,7 +119,8 @@ The ``metadata`` section provides settings for overall service metadata and desc ``resources`` ^^^^^^^^^^^^^ -The ``resources`` section lists 1 or more dataset collections to be published by the server. +The ``resources`` section lists 1 or more dataset collections to be published by the server. The +key of the resource name is the advertised collection identifier. The ``resource.type`` property is required. Allowed types are: @@ -228,6 +229,45 @@ Below is an example of how to integrate system environment variables in pygeoapi port: ${MY_PORT} +Hierarchical collections +------------------------ + +Collections defined in the the ``resources`` section are identified by the resource key. The +key of the resource name is the advertised collection identifier. For example, given the following: + +.. code-block:: yaml + + resources: + lakes: + ... + + +The resulting collection will be made available at http://localhost:5000/collections/lakes + +All collections are published by default to http://localhost:5000/collections. To enable +hierarchical collections, provide the hierarchy in the resource key. Given the following: + +.. code-block:: yaml + + resources: + naturalearth/lakes: + ... + +The resulting collection will then be made available at http://localhost:5000/collections/naturalearth/lakes + +.. note:: + + This functionality may change in the future given how hierarchical collection extension specifications + evolve at OGC. + +.. note:: + + Collection grouping is not available. This means that while URLs such as http://localhost:5000/collections/naturalearth/lakes + function as expected, URLs such as http://localhost:5000/collections/naturalearth will not provide + aggregate collection listing or querying. This functionality is also to be determined based on + the evolution of hierarchical collection extension specifications at OGC. + + Linked Data ----------- diff --git a/docs/source/data-publishing/stac.rst b/docs/source/data-publishing/stac.rst index 686ae4e..1fcf2ef 100644 --- a/docs/source/data-publishing/stac.rst +++ b/docs/source/data-publishing/stac.rst @@ -84,7 +84,6 @@ File examples "href": "./eo4ce/catalog.json", "type": "application/json" }, - ... { "rel": "child", "href": "./dem/catalog.json", @@ -147,7 +146,6 @@ Collections are similar to Catalogs with extra fields. "href": "./arcticdem-frontiere-0/arcticdem-frontiere-0.json", "type": "application/json" }, - ... { "rel": "item", "href": "./arcticdem-frontiere-9/arcticdem-frontiere-9.json", diff --git a/pygeoapi/api.py b/pygeoapi/api.py index d56d6ac..e149db0 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -678,7 +678,7 @@ class API: 'rel': 'data', 'type': FORMAT_TYPES[F_JSON], 'title': 'Collections', - 'href': '{}/collections'.format(self.config['server']['url']) + 'href': self.get_collections_url() }, { 'rel': 'http://www.opengis.net/def/rel/ogc/1.0/processes', 'type': FORMAT_TYPES[F_JSON], @@ -878,22 +878,22 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'rel': request.get_linkrel(F_JSON), 'title': 'This document as JSON', - 'href': '{}/collections/{}?f={}'.format( - self.config['server']['url'], k, F_JSON) + 'href': '{}/{}?f={}'.format( + self.get_collections_url(), k, F_JSON) }) collection['links'].append({ 'type': FORMAT_TYPES[F_JSONLD], 'rel': request.get_linkrel(F_JSONLD), 'title': 'This document as RDF (JSON-LD)', - 'href': '{}/collections/{}?f={}'.format( - self.config['server']['url'], k, F_JSONLD) + 'href': '{}/{}?f={}'.format( + self.get_collections_url(), k, F_JSONLD) }) collection['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': request.get_linkrel(F_HTML), 'title': 'This document as HTML', - 'href': '{}/collections/{}?f={}'.format( - self.config['server']['url'], k, F_HTML) + 'href': '{}/{}?f={}'.format( + self.get_collections_url(), k, F_HTML) }) if collection_data_type in ['feature', 'record', 'tile']: @@ -904,36 +904,36 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'rel': 'queryables', 'title': 'Queryables for this collection as JSON', - 'href': '{}/collections/{}/queryables?f={}'.format( - self.config['server']['url'], k, F_JSON) + 'href': '{}/{}/queryables?f={}'.format( + self.get_collections_url(), k, F_JSON) }) collection['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': 'queryables', 'title': 'Queryables for this collection as HTML', - 'href': '{}/collections/{}/queryables?f={}'.format( - self.config['server']['url'], k, F_HTML) + 'href': '{}/{}queryables?f={}'.format( + self.get_collections_url(), k, F_HTML) }) collection['links'].append({ 'type': 'application/geo+json', 'rel': 'items', 'title': 'items as GeoJSON', - 'href': '{}/collections/{}/items?f={}'.format( - self.config['server']['url'], k, F_JSON) + 'href': '{}/{}/items?f={}'.format( + self.get_collections_url(), k, F_JSON) }) collection['links'].append({ 'type': FORMAT_TYPES[F_JSONLD], 'rel': 'items', 'title': 'items as RDF (GeoJSON-LD)', - 'href': '{}/collections/{}/items?f={}'.format( - self.config['server']['url'], k, F_JSONLD) + 'href': '{}/{}/items?f={}'.format( + self.get_collections_url(), k, F_JSONLD) }) collection['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': 'items', 'title': 'Items as HTML', - 'href': '{}/collections/{}/items?f={}'.format( - self.config['server']['url'], k, F_HTML) + 'href': '{}/{}/items?f={}'.format( + self.get_collections_url(), k, F_HTML) }) elif collection_data_type == 'coverage': @@ -943,18 +943,18 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'rel': 'collection', 'title': 'Detailed Coverage metadata in JSON', - 'href': '{}/collections/{}?f={}'.format( - self.config['server']['url'], k, F_JSON) + 'href': '{}/{}?f={}'.format( + self.get_collections_url(), k, F_JSON) }) collection['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': 'collection', 'title': 'Detailed Coverage metadata in HTML', - 'href': '{}/collections/{}?f={}'.format( - self.config['server']['url'], k, F_HTML) + 'href': '{}/{}?f={}'.format( + self.get_collections_url(), k, F_HTML) }) - coverage_url = '{}/collections/{}/coverage'.format( - self.config['server']['url'], k) + coverage_url = '{}/{}/coverage'.format( + self.get_collections_url(), k) collection['links'].append({ 'type': FORMAT_TYPES[F_JSON], @@ -984,8 +984,8 @@ class API: 'type': 'application/prs.coverage+json', 'rel': '{}/coverage'.format(OGC_RELTYPES_BASE), 'title': 'Coverage data', - 'href': '{}/collections/{}/coverage?f={}'.format( - self.config['server']['url'], k, F_JSON) + 'href': '{}/{}/coverage?f={}'.format( + self.get_collections_url(), k, F_JSON) }) if collection_data_format is not None: collection['links'].append({ @@ -993,8 +993,8 @@ class API: 'rel': '{}/coverage'.format(OGC_RELTYPES_BASE), 'title': 'Coverage data as {}'.format( collection_data_format['name']), - 'href': '{}/collections/{}/coverage?f={}'.format( - self.config['server']['url'], k, + 'href': '{}/{}/coverage?f={}'.format( + self.get_collections_url(), k, collection_data_format['name']) }) if dataset is not None: @@ -1027,15 +1027,15 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'rel': 'tiles', 'title': 'Tiles as JSON', - 'href': '{}/collections/{}/tiles?f={}'.format( - self.config['server']['url'], k, F_JSON) + 'href': '{}/{}/tiles?f={}'.format( + self.get_collections_url(), k, F_JSON) }) collection['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': 'tiles', 'title': 'Tiles as HTML', - 'href': '{}/collections/{}/tiles?f={}'.format( - self.config['server']['url'], k, F_HTML) + 'href': '{}/{}/tiles?f={}'.format( + self.get_collections_url(), k, F_HTML) }) try: @@ -1060,15 +1060,15 @@ class API: 'type': 'text/json', 'rel': 'data', 'title': '{} query for this collection as JSON'.format(qt), # noqa - 'href': '{}/collections/{}/{}?f={}'.format( - self.config['server']['url'], k, qt, F_JSON) + 'href': '{}/{}/{}?f={}'.format( + self.get_collections_url(), k, qt, F_JSON) }) collection['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': 'data', 'title': '{} query for this collection as HTML'.format(qt), # noqa - 'href': '{}/collections/{}/{}?f={}'.format( - self.config['server']['url'], k, qt, F_HTML) + 'href': '{}/{}/{}?f={}'.format( + self.get_collections_url(), k, qt, F_HTML) }) except ProviderConnectionError: msg = 'connection error (check logs)' @@ -1089,25 +1089,23 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'rel': request.get_linkrel(F_JSON), 'title': 'This document as JSON', - 'href': '{}/collections?f={}'.format( - self.config['server']['url'], F_JSON) + 'href': '{}?f={}'.format(self.get_collections_url(), F_JSON) }) fcm['links'].append({ 'type': FORMAT_TYPES[F_JSONLD], 'rel': request.get_linkrel(F_JSONLD), 'title': 'This document as RDF (JSON-LD)', - 'href': '{}/collections?f={}'.format( - self.config['server']['url'], F_JSONLD) + 'href': '{}?f={}'.format(self.get_collections_url(), F_JSONLD) }) fcm['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': request.get_linkrel(F_HTML), 'title': 'This document as HTML', - 'href': '{}/collections?f={}'.format( - self.config['server']['url'], F_HTML) + 'href': '{}?f={}'.format(self.get_collections_url(), F_HTML) }) if request.format == F_HTML: # render + fcm['collections_path'] = self.get_collections_url() if dataset is not None: content = render_j2_template(self.config, 'collections/collection.html', @@ -1182,8 +1180,8 @@ class API: self.config['resources'][dataset]['title'], request.locale), 'properties': {}, '$schema': 'http://json-schema.org/draft/2019-09/schema', - '$id': '{}/collections/{}/queryables'.format( - self.config['server']['url'], dataset) + '$id': '{}/{}/queryables'.format( + self.get_collections_url(), dataset) } if p.fields: @@ -1210,6 +1208,9 @@ class API: if request.format == F_HTML: # render queryables['title'] = l10n.translate( self.config['resources'][dataset]['title'], request.locale) + + queryables['collections_path'] = self.get_collections_url() + content = render_j2_template(self.config, 'collections/queryables.html', queryables, request.locale) @@ -1443,8 +1444,7 @@ class API: serialized_query_params += urllib.parse.quote(str(v), safe=',') # TODO: translate titles - uri = '{}/collections/{}/items'.format( - self.config['server']['url'], dataset) + uri = '{}/{}/items'.format(self.get_collections_url(), dataset) content['links'] = [{ 'type': 'application/geo+json', 'rel': request.get_linkrel(F_JSON), @@ -1511,7 +1511,8 @@ class API: content['items_path'] = uri content['dataset_path'] = '/'.join(uri.split('/')[:-1]) - content['collections_path'] = '/'.join(uri.split('/')[:-2]) + content['collections_path'] = self.get_collections_url() + content['offset'] = offset content['id_field'] = p.id_field @@ -1883,8 +1884,8 @@ class API: 'NotFound', msg) uri = content['properties'].get(p.uri_field) if p.uri_field else \ - '{}/collections/{}/items/{}'.format( - self.config['server']['url'], dataset, identifier) + '{}/{}/items/{}'.format( + self.get_collections_url(), dataset, identifier) content['links'] = [{ 'rel': request.get_linkrel(F_JSON), @@ -1906,24 +1907,24 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'title': l10n.translate(collections[dataset]['title'], request.locale), - 'href': '{}/collections/{}'.format( - self.config['server']['url'], dataset) + 'href': '{}/{}'.format( + self.get_collections_url(), dataset) }] if 'prev' in content: content['links'].append({ 'rel': 'prev', 'type': FORMAT_TYPES[request.format], - 'href': '{}/collections/{}/items/{}?f={}'.format( - self.config['server']['url'], dataset, + 'href': '{}/{}/items/{}?f={}'.format( + self.get_collections_url(), dataset, content['prev'], request.format) }) if 'next' in content: content['links'].append({ 'rel': 'next', 'type': FORMAT_TYPES[request.format], - 'href': '{}/collections/{}/items/{}?f={}'.format( - self.config['server']['url'], dataset, + 'href': '{}/{}/items/{}?f={}'.format( + self.get_collections_url(), dataset, content['next'], request.format) }) @@ -1940,6 +1941,7 @@ class API: content['uri_field'] = p.uri_field if p.title_field is not None: content['title_field'] = p.title_field + content['collections_path'] = self.get_collections_url() content = render_j2_template(self.config, 'collections/items/item.html', @@ -2135,6 +2137,7 @@ class API: data['title'] = l10n.translate( self.config['resources'][dataset]['title'], self.default_locale) + data['collections_path'] = self.get_collections_url() content = render_j2_template(self.config, 'collections/coverage/domainset.html', data, self.default_locale) @@ -2188,6 +2191,7 @@ class API: data['title'] = l10n.translate( self.config['resources'][dataset]['title'], self.default_locale) + data['collections_path'] = self.get_collections_url() content = render_j2_template(self.config, 'collections/coverage/rangetype.html', data, self.default_locale) @@ -2252,29 +2256,31 @@ class API: 'type': FORMAT_TYPES[F_JSON], 'rel': request.get_linkrel(F_JSON), 'title': 'This document as JSON', - 'href': '{}/collections/{}/tiles?f={}'.format( - self.config['server']['url'], dataset, F_JSON) + 'href': '{}/{}/tiles?f={}'.format( + self.get_collections_url(), dataset, F_JSON) }) tiles['links'].append({ 'type': FORMAT_TYPES[F_JSONLD], 'rel': request.get_linkrel(F_JSONLD), 'title': 'This document as RDF (JSON-LD)', - 'href': '{}/collections/{}/tiles?f={}'.format( - self.config['server']['url'], dataset, F_JSONLD) + 'href': '{}/{}/tiles?f={}'.format( + self.get_collections_url(), dataset, F_JSONLD) }) tiles['links'].append({ 'type': FORMAT_TYPES[F_HTML], 'rel': request.get_linkrel(F_HTML), 'title': 'This document as HTML', - 'href': '{}/collections/{}/tiles?f={}'.format( - self.config['server']['url'], dataset, F_HTML) + 'href': '{}/{}/tiles?f={}'.format( + self.get_collections_url(), dataset, F_HTML) }) - for service in p.get_tiles_service( + tile_services = p.get_tiles_service( baseurl=self.config['server']['url'], - servicepath='/collections/{}/tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt' # noqa - .format(dataset, 'tileMatrixSetId', - 'tileMatrix', 'tileRow', 'tileCol'))['links']: + servicepath='{}/{}/tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt' + .format(self.get_collections_url(), dataset, 'tileMatrixSetId', + 'tileMatrix', 'tileRow', 'tileCol')) + + for service in tile_services['links']: tiles['links'].append(service) tiles['tileMatrixSetLinks'] = p.get_tiling_schemes() @@ -2291,6 +2297,7 @@ class API: self.config['resources'][dataset]['extents']['spatial']['bbox'] tiles['minzoom'] = p.options['zoom']['min'] tiles['maxzoom'] = p.options['zoom']['max'] + tiles['collections_path'] = self.get_collections_url() content = render_j2_template(self.config, 'collections/tiles/index.html', tiles, @@ -2459,6 +2466,7 @@ class API: self.config['resources'][dataset]['title'], request.locale) metadata['tileset'] = matrix_id metadata['format'] = metadata_format + metadata['collections_path'] = self.get_collections_url() content = render_j2_template(self.config, 'collections/tiles/metadata.html', @@ -3227,6 +3235,9 @@ class API: return self.get_exception( 400, headers, request.format, 'InvalidParameterValue', msg) + def get_collections_url(self): + return '{}/collections'.format((self.config['server']['url'])) + def validate_bbox(value=None) -> list: """ diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index 79f3dc6..359e86f 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -151,7 +151,7 @@ def conformance(): @BLUEPRINT.route('/collections') -@BLUEPRINT.route('/collections/') +@BLUEPRINT.route('/collections/') def collections(collection_id=None): """ OGC API collections endpoint @@ -163,7 +163,7 @@ def collections(collection_id=None): return get_response(api_.describe_collections(request, collection_id)) -@BLUEPRINT.route('/collections//queryables') +@BLUEPRINT.route('/collections//queryables') def collection_queryables(collection_id=None): """ OGC API collections querybles endpoint @@ -175,8 +175,8 @@ def collection_queryables(collection_id=None): return get_response(api_.get_collection_queryables(request, collection_id)) -@BLUEPRINT.route('/collections//items', methods=['GET', 'POST']) -@BLUEPRINT.route('/collections//items/') +@BLUEPRINT.route('/collections//items', methods=['GET', 'POST']) # noqa +@BLUEPRINT.route('/collections//items/') def collection_items(collection_id, item_id=None): """ OGC API collections items endpoint @@ -199,7 +199,7 @@ def collection_items(collection_id, item_id=None): api_.get_collection_item(request, collection_id, item_id)) -@BLUEPRINT.route('/collections//coverage') +@BLUEPRINT.route('/collections//coverage') def collection_coverage(collection_id): """ OGC API - Coverages coverage endpoint @@ -211,7 +211,7 @@ def collection_coverage(collection_id): return get_response(api_.get_collection_coverage(request, collection_id)) -@BLUEPRINT.route('/collections//coverage/domainset') +@BLUEPRINT.route('/collections//coverage/domainset') def collection_coverage_domainset(collection_id): """ OGC API - Coverages coverage domainset endpoint @@ -224,7 +224,7 @@ def collection_coverage_domainset(collection_id): request, collection_id)) -@BLUEPRINT.route('/collections//coverage/rangetype') +@BLUEPRINT.route('/collections//coverage/rangetype') def collection_coverage_rangetype(collection_id): """ OGC API - Coverages coverage rangetype endpoint @@ -237,7 +237,7 @@ def collection_coverage_rangetype(collection_id): request, collection_id)) -@BLUEPRINT.route('/collections//tiles') +@BLUEPRINT.route('/collections//tiles') def get_collection_tiles(collection_id=None): """ OGC open api collections tiles access point @@ -250,7 +250,7 @@ def get_collection_tiles(collection_id=None): request, collection_id)) -@BLUEPRINT.route('/collections//tiles//metadata') # noqa +@BLUEPRINT.route('/collections//tiles//metadata') # noqa def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None): """ OGC open api collection tiles service metadata @@ -264,7 +264,7 @@ def get_collection_tiles_metadata(collection_id=None, tileMatrixSetId=None): request, collection_id, tileMatrixSetId)) -@BLUEPRINT.route('/collections//tiles/\ +@BLUEPRINT.route('/collections//tiles/\ ///') def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None, tileMatrix=None, tileRow=None, tileCol=None): @@ -284,7 +284,7 @@ def get_collection_tiles_data(collection_id=None, tileMatrixSetId=None, @BLUEPRINT.route('/processes') -@BLUEPRINT.route('/processes/') +@BLUEPRINT.route('/processes/') def get_processes(process_id=None): """ OGC API - Processes description endpoint @@ -317,7 +317,7 @@ def get_jobs(job_id=None): return get_response(api_.get_jobs(request, job_id)) -@BLUEPRINT.route('/processes//execution', methods=['POST']) +@BLUEPRINT.route('/processes//execution', methods=['POST']) def execute_process_jobs(process_id): """ OGC API - Processes execution endpoint @@ -358,16 +358,16 @@ def get_job_result_resource(job_id, resource): request, job_id, resource)) -@BLUEPRINT.route('/collections//position') -@BLUEPRINT.route('/collections//area') -@BLUEPRINT.route('/collections//cube') -@BLUEPRINT.route('/collections//trajectory') -@BLUEPRINT.route('/collections//corridor') -@BLUEPRINT.route('/collections//instances//position') # noqa -@BLUEPRINT.route('/collections//instances//area') -@BLUEPRINT.route('/collections//instances//cube') -@BLUEPRINT.route('/collections//instances//trajectory') # noqa -@BLUEPRINT.route('/collections//instances//corridor') # noqa +@BLUEPRINT.route('/collections//position') +@BLUEPRINT.route('/collections//area') +@BLUEPRINT.route('/collections//cube') +@BLUEPRINT.route('/collections//trajectory') +@BLUEPRINT.route('/collections//corridor') +@BLUEPRINT.route('/collections//instances//position') # noqa +@BLUEPRINT.route('/collections//instances//area') # noqa +@BLUEPRINT.route('/collections//instances//cube') # noqa +@BLUEPRINT.route('/collections//instances//trajectory') # noqa +@BLUEPRINT.route('/collections//instances//corridor') # noqa def get_collection_edr_query(collection_id, instance_id=None): """ OGC EDR API endpoints diff --git a/pygeoapi/starlette_app.py b/pygeoapi/starlette_app.py index d2180b9..a815d91 100644 --- a/pygeoapi/starlette_app.py +++ b/pygeoapi/starlette_app.py @@ -139,8 +139,8 @@ async def conformance(request: Request): @app.route('/collections') @app.route('/collections/') -@app.route('/collections/{collection_id}') -@app.route('/collections/{collection_id}/') +@app.route('/collections/{path:collection_id}') +@app.route('/collections/{path:collection_id}/') async def collections(request: Request, collection_id=None): """ OGC API collections endpoint @@ -155,8 +155,8 @@ async def collections(request: Request, collection_id=None): return get_response(api_.describe_collections(request, collection_id)) -@app.route('/collections/{collection_id}/queryables') -@app.route('/collections/{collection_id}/queryables/') +@app.route('/collections/{path:collection_id}/queryables') +@app.route('/collections/{path:collection_id}/queryables/') async def collection_queryables(request: Request, collection_id=None): """ OGC API collections queryables endpoint @@ -220,10 +220,10 @@ async def get_collection_items_tiles(request: Request, name=None, request, name, tileMatrixSetId, tile_matrix, tileRow, tileCol)) -@app.route('/collections/{collection_id}/items', methods=['GET', 'POST']) -@app.route('/collections/{collection_id}/items/', methods=['GET', 'POST']) -@app.route('/collections/{collection_id}/items/{item_id}') -@app.route('/collections/{collection_id}/items/{item_id}/') +@app.route('/collections/{path:collection_id}/items', methods=['GET', 'POST']) +@app.route('/collections/{path:collection_id}/items/', methods=['GET', 'POST']) +@app.route('/collections/{path:collection_id}/items/{item_id}') +@app.route('/collections/{path:collection_id}/items/{item_id}/') async def collection_items(request: Request, collection_id=None, item_id=None): """ OGC API collections items endpoint @@ -252,7 +252,7 @@ async def collection_items(request: Request, collection_id=None, item_id=None): request, collection_id, item_id)) -@app.route('/collections/{collection_id}/coverage') +@app.route('/collections/{path:collection_id}/coverage') async def collection_coverage(request: Request, collection_id=None): """ OGC API - Coverages coverage endpoint @@ -268,7 +268,7 @@ async def collection_coverage(request: Request, collection_id=None): return get_response(api_.get_collection_coverage(request, collection_id)) -@app.route('/collections/{collection_id}/coverage/domainset') +@app.route('/collections/{path:collection_id}/coverage/domainset') async def collection_coverage_domainset(request: Request, collection_id=None): """ OGC API - Coverages coverage domainset endpoint @@ -285,7 +285,7 @@ async def collection_coverage_domainset(request: Request, collection_id=None): request, collection_id)) -@app.route('/collections/{collection_id}/coverage/rangetype') +@app.route('/collections/{path:collection_id}/coverage/rangetype') async def collection_coverage_rangetype(request: Request, collection_id=None): """ OGC API - Coverages coverage rangetype endpoint @@ -304,8 +304,8 @@ async def collection_coverage_rangetype(request: Request, collection_id=None): @app.route('/processes') @app.route('/processes/') -@app.route('/processes/{process_id}') -@app.route('/processes/{process_id}/') +@app.route('/processes/{path:process_id}') +@app.route('/processes/{path:process_id}/') async def get_processes(request: Request, process_id=None): """ OGC API - Processes description endpoint @@ -346,8 +346,8 @@ async def get_jobs(request: Request, job_id=None): return get_response(api_.get_jobs(request, job_id)) -@app.route('/processes/{process_id}/execution', methods=['POST']) -@app.route('/processes/{process_id}/execution/', methods=['POST']) +@app.route('/processes/{path:process_id}/execution', methods=['POST']) +@app.route('/processes/{path:process_id}/execution/', methods=['POST']) async def execute_process_jobs(request: Request, process_id=None): """ OGC API - Processes jobs endpoint @@ -407,16 +407,16 @@ async def get_job_result_resource(request: Request, request, job_id, resource)) -@app.route('/collections/{collection_id}/position') -@app.route('/collections/{collection_id}/area') -@app.route('/collections/{collection_id}/cube') -@app.route('/collections/{collection_id}/trajectory') -@app.route('/collections/{collection_id}/corridor') -@app.route('/collections/{collection_id}/instances/{instance_id}/position') -@app.route('/collections/{collection_id}/instances/{instance_id}/area') -@app.route('/collections/{collection_id}/instances/{instance_id}/cube') -@app.route('/collections/{collection_id}/instances/{instance_id}/trajectory') -@app.route('/collections/{collection_id}/instances/{instance_id}/corridor') +@app.route('/collections/{path:collection_id}/position') +@app.route('/collections/{path:collection_id}/area') +@app.route('/collections/{path:collection_id}/cube') +@app.route('/collections/{path:collection_id}/trajectory') +@app.route('/collections/{path:collection_id}/corridor') +@app.route('/collections/{path:collection_id}/instances/{instance_id}/position') # noqa +@app.route('/collections/{path:collection_id}/instances/{instance_id}/area') +@app.route('/collections/{path:collection_id}/instances/{instance_id}/cube') +@app.route('/collections/{path:collection_id}/instances/{instance_id}/trajectory') # noqa +@app.route('/collections/{path:collection_id}/instances/{instance_id}/corridor') # noqa async def get_collection_edr_query(request: Request, collection_id=None, instance_id=None): # noqa """ OGC EDR API endpoints diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html index d77cba1..65f8e67 100644 --- a/pygeoapi/templates/collections/collection.html +++ b/pygeoapi/templates/collections/collection.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {{ data['title'] }} {% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} / {{ data['title'] | truncate( 25 ) }} {% endblock %} @@ -33,7 +33,7 @@ @@ -41,7 +41,7 @@ @@ -51,7 +51,7 @@ diff --git a/pygeoapi/templates/collections/coverage/domainset.html b/pygeoapi/templates/collections/coverage/domainset.html index bf11413..8ca91a4 100644 --- a/pygeoapi/templates/collections/coverage/domainset.html +++ b/pygeoapi/templates/collections/coverage/domainset.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {{ data['title'] }} {% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} / {{ data['title'] }} {% endblock %} {% block body %} diff --git a/pygeoapi/templates/collections/coverage/rangetype.html b/pygeoapi/templates/collections/coverage/rangetype.html index 7d6ced6..d047594 100644 --- a/pygeoapi/templates/collections/coverage/rangetype.html +++ b/pygeoapi/templates/collections/coverage/rangetype.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {{ data['title'] }} {% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} / {{ data['title'] }} {% endblock %} {% block body %} diff --git a/pygeoapi/templates/collections/index.html b/pygeoapi/templates/collections/index.html index 651a1ae..b5ce1a3 100644 --- a/pygeoapi/templates/collections/index.html +++ b/pygeoapi/templates/collections/index.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {% trans %}Collections{% endtrans %}{% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} {% endblock %} {% block body %}
@@ -19,7 +19,7 @@ + href="{{ data['collections_path'] }}/{{ col.id }}"> {{ col['title'] | striptags | truncate }} {{ col["itemType"] }} diff --git a/pygeoapi/templates/collections/items/item.html b/pygeoapi/templates/collections/items/item.html index bddc91c..7d76b1b 100644 --- a/pygeoapi/templates/collections/items/item.html +++ b/pygeoapi/templates/collections/items/item.html @@ -22,7 +22,7 @@ {%- endmacro %} {% block title %}{{ ptitle }} - {{ super() }}{% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} {% for link in data['links'] %} {% if link.rel == 'collection' %} / {{ link['title'] | truncate( 25 ) }} diff --git a/pygeoapi/templates/collections/queryables.html b/pygeoapi/templates/collections/queryables.html index 63dc1fd..4ef9626 100644 --- a/pygeoapi/templates/collections/queryables.html +++ b/pygeoapi/templates/collections/queryables.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {{ data['title'] }} {% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} / {{ data['title'] | truncate( 25 ) }} / {% trans %}Queryables{% endtrans %} {% endblock %} diff --git a/pygeoapi/templates/collections/tiles/index.html b/pygeoapi/templates/collections/tiles/index.html index d63471a..b8f2436 100644 --- a/pygeoapi/templates/collections/tiles/index.html +++ b/pygeoapi/templates/collections/tiles/index.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {{ data['title'] }} {% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} / {{ data['title'] }} / {% trans %}Tiles{% endtrans %} {% endblock %} diff --git a/pygeoapi/templates/collections/tiles/metadata.html b/pygeoapi/templates/collections/tiles/metadata.html index bae75ae..4721b38 100644 --- a/pygeoapi/templates/collections/tiles/metadata.html +++ b/pygeoapi/templates/collections/tiles/metadata.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} {% block title %}{{ super() }} {{ data['title'] }} {% endblock %} {% block crumbs %}{{ super() }} -/ {% trans %}Collections{% endtrans %} +/ {% trans %}Collections{% endtrans %} / {{ data['title'] }} / {% trans %}Tiles{% endtrans %} / Tile Metadata diff --git a/tests/pygeoapi-test-config.yml b/tests/pygeoapi-test-config.yml index f24a1ca..d883f17 100644 --- a/tests/pygeoapi-test-config.yml +++ b/tests/pygeoapi-test-config.yml @@ -176,7 +176,7 @@ resources: name: NetCDF mimetype: application/x-netcdf - lakes: + naturalearth/lakes: type: collection title: en: Large Lakes diff --git a/tests/test_api.py b/tests/test_api.py index aac99b8..8864eee 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -235,6 +235,8 @@ def test_api(config, api_, openapi): assert rsp_headers['Content-Language'] == 'en-US' assert code == 400 + assert api_.get_collections_url() == 'http://localhost:5000/collections' + def test_api_exception(config, api_): req = mock_request({'f': 'foo'}) @@ -491,6 +493,12 @@ def test_describe_collections(config, api_): assert collection['id'] == 'gdps-temperature' assert len(collection['links']) == 12 + # hiearchical collections + rsp_headers, code, response = api_.describe_collections( + req, 'naturalearth/lakes') + collection = json.loads(response) + assert collection['id'] == 'naturalearth/lakes' + def test_get_collection_queryables(config, api_): req = mock_request() @@ -775,7 +783,8 @@ def test_get_collection_items(config, api_): assert code == 200 req = mock_request({'scalerank': 1}) - rsp_headers, code, response = api_.get_collection_items(req, 'lakes') + rsp_headers, code, response = api_.get_collection_items( + req, 'naturalearth/lakes') features = json.loads(response) assert len(features['features']) == 10 @@ -783,7 +792,8 @@ def test_get_collection_items(config, api_): assert features['numberReturned'] == 10 req = mock_request({'datetime': '2005-04-22'}) - rsp_headers, code, response = api_.get_collection_items(req, 'lakes') + rsp_headers, code, response = api_.get_collection_items( + req, 'naturalearth/lakes') assert code == 400 @@ -1081,12 +1091,14 @@ def test_get_collection_tiles(config, api_): rsp_headers, code, response = api_.get_collection_tiles(req, 'obs') assert code == 400 - rsp_headers, code, response = api_.get_collection_tiles(req, 'lakes') + rsp_headers, code, response = api_.get_collection_tiles( + req, 'naturalearth/lakes') assert code == 200 # Language settings should be ignored (return system default) req = mock_request({'lang': 'fr'}) - rsp_headers, code, response = api_.get_collection_tiles(req, 'lakes') + rsp_headers, code, response = api_.get_collection_tiles( + req, 'naturalearth/lakes') assert rsp_headers['Content-Language'] == 'en-US' content = json.loads(response) assert content['description'] == 'lakes of the world, public domain'