431 lines
14 KiB
Python
431 lines
14 KiB
Python
# =================================================================
|
|
#
|
|
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
|
#
|
|
# Copyright (c) 2018 Tom Kralidis
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation
|
|
# files (the "Software"), to deal in the Software without
|
|
# restriction, including without limitation the rights to use,
|
|
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the
|
|
# Software is furnished to do so, subject to the following
|
|
# conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
# OTHER DEALINGS IN THE SOFTWARE.
|
|
#
|
|
# =================================================================
|
|
|
|
from datetime import datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
from pygeoapi import __version__
|
|
from pygeoapi.provider import load_provider
|
|
from pygeoapi.provider.base import ProviderConnectionError, ProviderQueryError
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
TEMPLATES = '{}{}templates'.format(os.path.dirname(
|
|
os.path.realpath(__file__)), os.sep)
|
|
|
|
|
|
class API(object):
|
|
"""API object"""
|
|
|
|
def __init__(self, config):
|
|
"""
|
|
constructor
|
|
|
|
:param config: configuration dict
|
|
|
|
:returns: `pygeoapi.API` instance
|
|
"""
|
|
|
|
self.config = config
|
|
self.config['server']['url'] = self.config['server']['url'].rstrip('/')
|
|
|
|
def root(self, headers, args):
|
|
"""
|
|
Provide API
|
|
|
|
:param headers: dict of HTTP headers
|
|
:param args: dict of HTTP request parameters
|
|
|
|
:returns: tuple of headers, status code, content
|
|
"""
|
|
|
|
headers_ = {
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
formats = ['json', 'html']
|
|
|
|
format_ = args.get('f')
|
|
if format_ is not None and format_ not in formats:
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid format'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
fcm = {
|
|
'links': [],
|
|
}
|
|
|
|
LOGGER.debug('Creating links')
|
|
fcm['links'] = [{
|
|
'rel': 'self',
|
|
'type': 'application/json',
|
|
'title': 'this document',
|
|
'href': self.config['server']['url']
|
|
}, {
|
|
'rel': 'self',
|
|
'type': 'text/html',
|
|
'title': 'this document as HTML',
|
|
'href': '{}/?f=html'.format(self.config['server']['url'])
|
|
}, {
|
|
'rel': 'self',
|
|
'type': 'application/openapi+json;version=3.0',
|
|
'title': 'the OpenAPI definition as JSON',
|
|
'href': '{}/api'.format(self.config['server']['url'])
|
|
}, {
|
|
'rel': 'self',
|
|
'type': 'text/html',
|
|
'title': 'the OpenAPI definition as HTML',
|
|
'href': '{}/api?f=html'.format(self.config['server']['url'])
|
|
}
|
|
]
|
|
|
|
if format_ == 'html': # render
|
|
headers_['Content-type'] = 'text/html'
|
|
content = _render_j2_template(self.config, 'root.html', fcm)
|
|
return headers_, 200, content
|
|
|
|
return headers_, 200, json.dumps(fcm)
|
|
|
|
def api(self, headers, args, openapi):
|
|
"""
|
|
Provide OpenAPI document
|
|
|
|
:param headers: dict of HTTP headers
|
|
:param args: dict of HTTP request parameters
|
|
:param openapi: dict of OpenAPI definition
|
|
|
|
:returns: tuple of headers, status code, content
|
|
"""
|
|
|
|
headers_ = {
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
return headers_, 200, json.dumps(openapi)
|
|
|
|
def api_conformance(self, headers, args):
|
|
"""
|
|
Provide conformance definition
|
|
|
|
:param headers: dict of HTTP headers
|
|
:param args: dict of HTTP request parameters
|
|
|
|
:returns: tuple of headers, status code, content
|
|
"""
|
|
|
|
headers_ = {
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
formats = ['json', 'html']
|
|
|
|
format_ = args.get('f')
|
|
if format_ is not None and format_ not in formats:
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid format'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
conformance = {
|
|
'conformsTo': [
|
|
'http://www.opengis.net/spec/wfs-1/3.0/req/core',
|
|
'http://www.opengis.net/spec/wfs-1/3.0/req/oas30',
|
|
'http://www.opengis.net/spec/wfs-1/3.0/req/html',
|
|
'http://www.opengis.net/spec/wfs-1/3.0/req/geojson'
|
|
]
|
|
}
|
|
|
|
if format_ == 'html': # render
|
|
headers_['Content-type'] = 'text/html'
|
|
content = _render_j2_template(self.config, 'conformance.html',
|
|
conformance)
|
|
return headers_, 200, content
|
|
|
|
return headers_, 200, json.dumps(conformance)
|
|
|
|
def describe_collections(self, headers, args, dataset=None):
|
|
"""
|
|
Provide feature collection metadata
|
|
|
|
:param headers: dict of HTTP headers
|
|
:param args: dict of HTTP request parameters
|
|
|
|
:returns: tuple of headers, status code, content
|
|
"""
|
|
|
|
headers_ = {
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
formats = ['json', 'html']
|
|
|
|
format_ = args.get('f')
|
|
if format_ is not None and format_ not in formats:
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid format'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
fcm = {
|
|
'collections': []
|
|
}
|
|
|
|
if all([dataset is not None,
|
|
dataset not in self.config['datasets'].keys()]):
|
|
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid feature collection'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
LOGGER.debug('Creating collections')
|
|
for k, v in self.config['datasets'].items():
|
|
collection = {'links': [], 'crs': []}
|
|
collection['name'] = k
|
|
collection['title'] = v['title']
|
|
collection['description'] = v['description']
|
|
for crs in v['crs']:
|
|
collection['crs'].append(
|
|
'http://www.opengis.net/def/crs/OGC/1.3/{}'.format(crs))
|
|
collection['extent'] = v['extents']['spatial']['bbox']
|
|
|
|
for link in v['links']:
|
|
lnk = {
|
|
'rel': 'alternate',
|
|
'type': link['type'],
|
|
'href': link['url']
|
|
}
|
|
collection['links'].append(lnk)
|
|
|
|
if dataset is not None and k == dataset:
|
|
fcm = collection
|
|
break
|
|
|
|
fcm['collections'].append(collection)
|
|
|
|
if format_ == 'html': # render
|
|
headers_['Content-type'] = 'text/html'
|
|
if dataset is not None:
|
|
content = _render_j2_template(self.config, 'collection.html',
|
|
fcm)
|
|
else:
|
|
content = _render_j2_template(self.config, 'collections.html',
|
|
fcm)
|
|
|
|
return headers_, 200, content
|
|
|
|
return headers_, 200, json.dumps(fcm)
|
|
|
|
def get_features(self, headers, args, dataset):
|
|
"""
|
|
Queries feature collection
|
|
|
|
:param headers: dict of HTTP headers
|
|
:param args: dict of HTTP request parameters
|
|
:param dataset: dataset name
|
|
|
|
:returns: tuple of headers, status code, content
|
|
"""
|
|
|
|
headers_ = {
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
formats = ['json', 'html']
|
|
|
|
format_ = args.get('f')
|
|
if format_ is not None and format_ not in formats:
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid format'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
LOGGER.debug('Processing query parameters')
|
|
try:
|
|
startindex = int(args.get('startindex'))
|
|
except TypeError:
|
|
startindex = 0
|
|
try:
|
|
limit = int(args.get('limit'))
|
|
except TypeError:
|
|
limit = self.config['server']['limit']
|
|
|
|
resulttype = args.get('resulttype') or 'results'
|
|
|
|
try:
|
|
bbox = args.get('bbox').split(',')
|
|
except AttributeError:
|
|
bbox = []
|
|
|
|
time = args.get('time')
|
|
|
|
if dataset not in self.config['datasets'].keys():
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid feature collection'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
LOGGER.debug('Loading provider')
|
|
p = load_provider(self.config['datasets'][dataset]['provider'])
|
|
LOGGER.debug('Querying provider')
|
|
LOGGER.debug('startindex: {}'.format(startindex))
|
|
LOGGER.debug('limit: {}'.format(limit))
|
|
LOGGER.debug('resulttype: {}'.format(resulttype))
|
|
|
|
try:
|
|
content = p.query(startindex=int(startindex), limit=int(limit),
|
|
resulttype=resulttype, bbox=bbox, time=time)
|
|
except ProviderConnectionError:
|
|
exception = {
|
|
'code': 'NoApplicableCode',
|
|
'description': 'connection error (check logs)'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 500, json.dumps(exception)
|
|
except ProviderQueryError:
|
|
exception = {
|
|
'code': 'NoApplicableCode',
|
|
'description': 'query error (check logs)'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 500, json.dumps(exception)
|
|
|
|
next_ = startindex + self.config['server']['limit']
|
|
|
|
content['links'] = [{
|
|
'rel': 'self',
|
|
'type': 'application/json',
|
|
'href': '/collections/{}/items'.format(dataset)
|
|
}, {
|
|
'rel': 'next',
|
|
'type': 'application/json',
|
|
'href': '/collections/{}/items/?startindex={}'.format(dataset,
|
|
next_)
|
|
}, {
|
|
'rel': 'collection',
|
|
'type': 'application/json',
|
|
'href': '/collections/{}'.format(dataset)
|
|
}
|
|
]
|
|
|
|
content['timeStamp'] = datetime.utcnow().isoformat()
|
|
|
|
if format_ == 'html': # render
|
|
headers_['Content-type'] = 'text/html'
|
|
content = _render_j2_template(self.config, 'items.html',
|
|
content)
|
|
return headers_, 200, content
|
|
|
|
return headers_, 200, json.dumps(content)
|
|
|
|
def get_feature(self, headers, args, dataset, identifier):
|
|
"""
|
|
Get a single feature
|
|
|
|
:param headers: dict of HTTP headers
|
|
:param args: dict of HTTP request parameters
|
|
:param dataset: dataset name
|
|
:param identifier: feature identifier
|
|
|
|
:returns: tuple of headers, status code, content
|
|
"""
|
|
|
|
headers_ = {
|
|
'Content-type': 'application/json'
|
|
}
|
|
|
|
if dataset not in self.config['datasets'].keys():
|
|
exception = {
|
|
'code': 'InvalidParameterValue',
|
|
'description': 'Invalid feature collection'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 400, json.dumps(exception)
|
|
|
|
LOGGER.debug('Loading provider')
|
|
p = load_provider(self.config['datasets'][dataset]['provider'])
|
|
|
|
LOGGER.debug('Fetching id {}'.format(identifier))
|
|
content = p.get(identifier)
|
|
|
|
if content is None:
|
|
exception = {
|
|
'code': 'NotFound',
|
|
'description': 'identifier not found'
|
|
}
|
|
LOGGER.error(exception)
|
|
return headers_, 404, json.dumps(exception)
|
|
|
|
content['links'] = [{
|
|
'rel': 'self',
|
|
'type': 'application/json',
|
|
'href': '/collections/{}/items/{}'.format(dataset, identifier)
|
|
}, {
|
|
'rel': 'collection',
|
|
'type': 'application/json',
|
|
'href': '/collections/{}'.format(dataset)
|
|
}
|
|
]
|
|
|
|
return headers_, 200, json.dumps(content)
|
|
|
|
|
|
def _render_j2_template(config, template, data):
|
|
"""
|
|
render Jinja2 template
|
|
|
|
:param config: dict of configuration
|
|
:param template: template (relative path)
|
|
:param data: dict of data
|
|
|
|
:returns: string of rendered template
|
|
"""
|
|
|
|
env = Environment(loader=FileSystemLoader(TEMPLATES))
|
|
print(TEMPLATES)
|
|
template = env.get_template(template)
|
|
return template.render(config=config, data=data, version=__version__)
|