various fixes (#538)
* fix version output * sort directory names * unify tile tests * fix typo in docs * set OpenAPI info.version to pygeoapi version, add tiles output format * align request/response headers, update not found tiles to return 404
This commit is contained in:
@@ -8,7 +8,7 @@ Publishing map tiles to OGC API - Tiles
|
||||
|
||||
pygeoapi can publish tiles from local or remote data sources (including cloud
|
||||
object storage). To integrate tiles from a local data source, it is assumed
|
||||
that a directory tree of static tiles has been created on disk. Example of
|
||||
that a directory tree of static tiles has been created on disk. Examples of
|
||||
tile generation software include (but are not limited to):
|
||||
|
||||
- `MapProxy`_
|
||||
|
||||
@@ -33,8 +33,10 @@ import click
|
||||
from pygeoapi.openapi import generate_openapi_document
|
||||
|
||||
|
||||
cli = click.Group()
|
||||
cli.version = __version__
|
||||
@click.group()
|
||||
@click.version_option(version=__version__)
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
||||
+67
-38
@@ -52,7 +52,8 @@ from pygeoapi.provider.base import (
|
||||
ProviderInvalidQueryError, ProviderNoDataError, ProviderQueryError,
|
||||
ProviderItemNotFoundError, ProviderTypeError)
|
||||
|
||||
from pygeoapi.provider.tile import (ProviderTileQueryError,
|
||||
from pygeoapi.provider.tile import (ProviderTileNotFoundError,
|
||||
ProviderTileQueryError,
|
||||
ProviderTilesetIdNotFoundError)
|
||||
from pygeoapi.util import (dategetter, filter_dict_by_key_value,
|
||||
get_provider_by_type, get_provider_default,
|
||||
@@ -1212,7 +1213,7 @@ class API:
|
||||
return headers_, 200, to_json(content, self.pretty_print)
|
||||
|
||||
@jsonldify
|
||||
def get_collection_coverage(self, headers_, args, dataset,
|
||||
def get_collection_coverage(self, headers, args, dataset,
|
||||
pathinfo=None):
|
||||
"""
|
||||
Returns a subset of a collection coverage
|
||||
@@ -1225,6 +1226,7 @@ class API:
|
||||
:returns: tuple of headers, status code, content
|
||||
"""
|
||||
|
||||
headers_ = HEADERS.copy()
|
||||
query_args = {}
|
||||
format_ = 'json'
|
||||
|
||||
@@ -1238,14 +1240,20 @@ class API:
|
||||
self.config['resources'][dataset]['providers'], 'coverage')
|
||||
|
||||
p = load_plugin('provider', collection_def)
|
||||
except KeyError:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
'description': 'collection does not exist'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return headers_, 404, to_json(exception, self.pretty_print)
|
||||
except ProviderTypeError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
'description': 'invalid provider type'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'}, 400,
|
||||
to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
except ProviderConnectionError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
@@ -1270,11 +1278,10 @@ class API:
|
||||
'description': 'Invalid field specified'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'}, 400,
|
||||
to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
|
||||
if 'subset' in args:
|
||||
LOGGER.debug('Processing subset parameters')
|
||||
LOGGER.debug('Processing subset parameter')
|
||||
for s in args['subset'].split(','):
|
||||
try:
|
||||
if '"' not in s:
|
||||
@@ -1290,8 +1297,8 @@ class API:
|
||||
'description': 'Invalid axis name'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'}, 400,
|
||||
to_json(exception, self.pretty_print))
|
||||
return (headers_, 400, to_json(exception,
|
||||
self.pretty_print))
|
||||
|
||||
subsets[subset_name] = list(map(
|
||||
get_typed_value, m.group(2, 3)))
|
||||
@@ -1315,43 +1322,40 @@ class API:
|
||||
'description': 'query error: {}'.format(err),
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'},
|
||||
400, to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
except ProviderNoDataError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
'description': 'No data found'
|
||||
}
|
||||
LOGGER.debug(exception)
|
||||
return ({'Content-type': 'application/json'},
|
||||
204, to_json(exception, self.pretty_print))
|
||||
return headers_, 204, to_json(exception, self.pretty_print)
|
||||
except ProviderQueryError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
'description': 'query error (check logs)'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'},
|
||||
500, to_json(exception, self.pretty_print))
|
||||
return headers_, 500, to_json(exception, self.pretty_print)
|
||||
|
||||
mt = collection_def['format']['name']
|
||||
|
||||
if format_ == mt:
|
||||
return ({'Content-type': mt}, 200, data)
|
||||
headers_['Content-type'] = mt
|
||||
return headers_, 200, data
|
||||
elif format_ == 'json':
|
||||
return ({'Content-type': 'application/prs.coverage+json'},
|
||||
200, to_json(data, self.pretty_print))
|
||||
headers_['Content-type'] = 'application/prs.coverage+json'
|
||||
return headers_, 200, to_json(data, self.pretty_print)
|
||||
else:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
'description': 'invalid format parameter'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'},
|
||||
400, to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(data, self.pretty_print)
|
||||
|
||||
@jsonldify
|
||||
def get_collection_coverage_domainset(self, headers_, args, dataset,
|
||||
def get_collection_coverage_domainset(self, headers, args, dataset,
|
||||
pathinfo=None):
|
||||
"""
|
||||
Returns a collection coverage domainset
|
||||
@@ -1364,6 +1368,8 @@ class API:
|
||||
:returns: tuple of headers, status code, content
|
||||
"""
|
||||
|
||||
headers_ = HEADERS.copy()
|
||||
|
||||
format_ = check_format(args, headers_)
|
||||
if format_ is None:
|
||||
format_ = 'json'
|
||||
@@ -1376,14 +1382,20 @@ class API:
|
||||
p = load_plugin('provider', collection_def)
|
||||
|
||||
data = p.get_coverage_domainset()
|
||||
except KeyError:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
'description': 'collection does not exist'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return headers_, 404, to_json(exception, self.pretty_print)
|
||||
except ProviderTypeError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
'description': 'invalid provider type'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'}, 400,
|
||||
to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
except ProviderConnectionError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
@@ -1393,25 +1405,24 @@ class API:
|
||||
return headers_, 500, to_json(exception, self.pretty_print)
|
||||
|
||||
if format_ == 'json':
|
||||
return ({'Content-type': 'application/json'},
|
||||
200, to_json(data, self.pretty_print))
|
||||
return headers_, 200, to_json(data, self.pretty_print)
|
||||
elif format_ == 'html':
|
||||
data['id'] = dataset
|
||||
data['title'] = self.config['resources'][dataset]['title']
|
||||
content = render_j2_template(self.config, 'domainset.html',
|
||||
data)
|
||||
return {'Content-type': 'text/html'}, 200, content
|
||||
headers_['Content-type'] = 'text/html'
|
||||
return headers_, 200, content
|
||||
else:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
'description': 'invalid format parameter'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'},
|
||||
400, to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
|
||||
@jsonldify
|
||||
def get_collection_coverage_rangetype(self, headers_, args, dataset,
|
||||
def get_collection_coverage_rangetype(self, headers, args, dataset,
|
||||
pathinfo=None):
|
||||
"""
|
||||
Returns a collection coverage rangetype
|
||||
@@ -1424,7 +1435,8 @@ class API:
|
||||
:returns: tuple of headers, status code, content
|
||||
"""
|
||||
|
||||
format_ = check_format(args, headers_)
|
||||
headers_ = HEADERS.copy()
|
||||
format_ = check_format(args, headers)
|
||||
if format_ is None:
|
||||
format_ = 'json'
|
||||
|
||||
@@ -1436,14 +1448,20 @@ class API:
|
||||
p = load_plugin('provider', collection_def)
|
||||
|
||||
data = p.get_coverage_rangetype()
|
||||
except KeyError:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
'description': 'collection does not exist'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return headers_, 404, to_json(exception, self.pretty_print)
|
||||
except ProviderTypeError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
'description': 'invalid provider type'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'}, 400,
|
||||
to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
except ProviderConnectionError:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
@@ -1460,15 +1478,15 @@ class API:
|
||||
data['title'] = self.config['resources'][dataset]['title']
|
||||
content = render_j2_template(self.config, 'rangetype.html',
|
||||
data)
|
||||
return {'Content-type': 'text/html'}, 200, content
|
||||
headers_['Content-type'] = 'text/html'
|
||||
return headers_, 200, content
|
||||
else:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
'description': 'invalid format parameter'
|
||||
}
|
||||
LOGGER.error(exception)
|
||||
return ({'Content-type': 'application/json'},
|
||||
400, to_json(exception, self.pretty_print))
|
||||
return headers_, 400, to_json(exception, self.pretty_print)
|
||||
|
||||
@pre_process
|
||||
@jsonldify
|
||||
@@ -1589,13 +1607,13 @@ tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt'
|
||||
return headers_, 200, to_json(tiles, self.pretty_print)
|
||||
|
||||
@jsonldify
|
||||
def get_collection_tiles_data(self, headers_, format_, dataset=None,
|
||||
def get_collection_tiles_data(self, headers, format_, dataset=None,
|
||||
matrix_id=None, z_idx=None, y_idx=None,
|
||||
x_idx=None):
|
||||
"""
|
||||
Get collection items tiles
|
||||
|
||||
:param headers_: copy of HEADERS object
|
||||
:param headers: copy of HEADERS object
|
||||
:param format_: format of requests,
|
||||
pre checked by pre_process decorator
|
||||
:param dataset: dataset name
|
||||
@@ -1607,6 +1625,9 @@ tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt'
|
||||
:returns: tuple of headers, status code, content
|
||||
"""
|
||||
|
||||
headers_ = HEADERS.copy()
|
||||
# format_ = check_format({}, headers)
|
||||
|
||||
if format_ is None and format_ not in ['mvt']:
|
||||
exception = {
|
||||
'code': 'InvalidParameterValue',
|
||||
@@ -1635,6 +1656,7 @@ tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt'
|
||||
p = load_plugin('provider', t)
|
||||
|
||||
format_ = p.format_type
|
||||
headers_['Content-Type'] = format_
|
||||
|
||||
LOGGER.debug('Fetching tileset id {} and tile {}/{}/{}'.format(
|
||||
matrix_id, z_idx, y_idx, x_idx))
|
||||
@@ -1648,7 +1670,7 @@ tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt'
|
||||
LOGGER.error(exception)
|
||||
return headers_, 404, to_json(exception)
|
||||
else:
|
||||
return headers_, 200, content
|
||||
return headers_, 202, content
|
||||
# @TODO: figure out if the spec requires to return json errors
|
||||
except KeyError:
|
||||
exception = {
|
||||
@@ -1678,6 +1700,13 @@ tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt'
|
||||
}
|
||||
LOGGER.error(err)
|
||||
return headers_, 500, to_json(exception)
|
||||
except ProviderTileNotFoundError as err:
|
||||
exception = {
|
||||
'code': 'NoMatch',
|
||||
'description': 'tile not found (check logs)'
|
||||
}
|
||||
LOGGER.error(err)
|
||||
return headers_, 404, to_json(exception)
|
||||
except ProviderGenericError as err:
|
||||
exception = {
|
||||
'code': 'NoApplicableCode',
|
||||
|
||||
+29
-6
@@ -34,6 +34,7 @@ import os
|
||||
import click
|
||||
import yaml
|
||||
|
||||
from pygeoapi import __version__
|
||||
from pygeoapi.plugin import load_plugin
|
||||
from pygeoapi.provider.base import ProviderTypeError
|
||||
from pygeoapi.util import (filter_dict_by_key_value, get_provider_by_type,
|
||||
@@ -145,7 +146,7 @@ def get_oas_30(cfg):
|
||||
'name': cfg['metadata']['license']['name'],
|
||||
'url': cfg['metadata']['license']['url']
|
||||
},
|
||||
'version': '3.0.2'
|
||||
'version': __version__
|
||||
}
|
||||
oas['info'] = info
|
||||
|
||||
@@ -575,7 +576,9 @@ def get_oas_30(cfg):
|
||||
LOGGER.debug('setting up tiles endpoints')
|
||||
tile_extension = filter_providers_by_type(
|
||||
collections[k]['providers'], 'tile')
|
||||
|
||||
if tile_extension:
|
||||
tp = load_plugin('provider', tile_extension)
|
||||
oas['components']['responses'].update({
|
||||
'Tiles': {
|
||||
'description': 'Retrieves the tiles description for this collection', # noqa
|
||||
@@ -652,18 +655,38 @@ def get_oas_30(cfg):
|
||||
'summary': 'Get a {} tile'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'describe{}Tiles'.format(k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
],
|
||||
'operationId': 'get{}Tiles'.format(k.capitalize()),
|
||||
'parameters': [{
|
||||
'name': 'f',
|
||||
'in': 'query',
|
||||
'description': 'The optional f parameter indicates the output format which the server shall provide as part of the response document.', # noqa
|
||||
'required': False,
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'enum': [tp.format_type],
|
||||
'default': tp.format_type
|
||||
},
|
||||
'style': 'form',
|
||||
'explode': False
|
||||
}],
|
||||
'responses': {
|
||||
'200': {'$ref': '#/components/responses/Tiles'}, # noqa # TODO: fix response format
|
||||
'400': {'$ref': '{}#/components/responses/InvalidParameter'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'404': {'$ref': '{}#/components/responses/NotFound'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'500': {'$ref': '{}#/components/responses/ServerError'.format(OPENAPI_YAML['oapif'])} # noqa
|
||||
}
|
||||
}
|
||||
}
|
||||
mimetype = tile_extension['format']['mimetype']
|
||||
paths[tiles_data_path]['get']['responses']['200'] = {
|
||||
'content': {
|
||||
mimetype: {
|
||||
'schema': {
|
||||
'type': 'string',
|
||||
'format': 'binary'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug('setting up STAC')
|
||||
stac_collections = filter_dict_by_key_value(cfg['resources'],
|
||||
|
||||
@@ -140,7 +140,9 @@ class FileSystemProvider(BaseProvider):
|
||||
return fh.read()
|
||||
|
||||
elif resource_type == 'directory':
|
||||
for dc in os.listdir(data_path):
|
||||
dirpath2 = os.listdir(data_path)
|
||||
dirpath2.sort()
|
||||
for dc in dirpath2:
|
||||
# @TODO: handle a generic directory for tiles
|
||||
if dc == "tiles":
|
||||
continue
|
||||
|
||||
@@ -34,7 +34,8 @@ from pathlib import Path
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from pygeoapi.util import is_url
|
||||
from pygeoapi.provider.tile import BaseTileProvider
|
||||
from pygeoapi.provider.tile import (
|
||||
BaseTileProvider, ProviderTileNotFoundError)
|
||||
from pygeoapi.provider.base import ProviderConnectionError
|
||||
|
||||
|
||||
@@ -204,10 +205,13 @@ class MVTProvider(BaseTileProvider):
|
||||
LOGGER.error(msg)
|
||||
raise ProviderConnectionError(msg)
|
||||
else:
|
||||
with open(self.service_url.joinpath(
|
||||
'{z}/{y}/{x}.{f}'.format(
|
||||
z=z, y=y, x=x, f=format_)), 'rb') as tile:
|
||||
return tile.read()
|
||||
try:
|
||||
with open(self.service_url.joinpath(
|
||||
'{z}/{y}/{x}.{f}'.format(
|
||||
z=z, y=y, x=x, f=format_)), 'rb') as tile:
|
||||
return tile.read()
|
||||
except FileNotFoundError as err:
|
||||
raise ProviderTileNotFoundError(err)
|
||||
|
||||
def get_metadata(self, dataset, server_url, layer=None,
|
||||
tileset=None, tilejson=True):
|
||||
|
||||
@@ -125,6 +125,11 @@ class ProviderTileQueryError(ProviderGenericError):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderTileNotFoundError(ProviderGenericError):
|
||||
"""provider tile not found error"""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderTilesetIdNotFoundError(ProviderTileQueryError):
|
||||
"""provider tileset matrix query error"""
|
||||
pass
|
||||
|
||||
+1
-3
@@ -697,15 +697,13 @@ def test_get_collection_coverage(config, api_):
|
||||
assert code == 204
|
||||
|
||||
|
||||
def test_get_collection_tiles_invalid(config, api_):
|
||||
def test_get_collection_tiles(config, api_):
|
||||
req_headers = make_lakes_req_headers()
|
||||
rsp_headers, code, response = api_.get_collection_tiles(
|
||||
req_headers, {}, 'obs')
|
||||
|
||||
assert code == 400
|
||||
|
||||
|
||||
def test_get_collection_tiles(config, api_):
|
||||
req_headers = make_lakes_req_headers()
|
||||
rsp_headers, code, response = api_.get_collection_tiles(
|
||||
req_headers, {}, 'lakes')
|
||||
|
||||
Reference in New Issue
Block a user