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:
Tom Kralidis
2020-09-21 06:37:04 -04:00
committed by GitHub
parent 4f52536051
commit f36e16d7e9
8 changed files with 119 additions and 56 deletions
+1 -1
View File
@@ -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`_
+4 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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'],
+3 -1
View File
@@ -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
+9 -5
View File
@@ -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):
+5
View File
@@ -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
View File
@@ -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')