From d0d7ed48da52f657bf16a817164c2620ccd3b91a Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Mon, 20 Jan 2020 14:56:04 -0500 Subject: [PATCH] Implement configurable OGC schema locations (#347) --- pygeoapi-config.yml | 3 ++- pygeoapi/flask_app.py | 38 ++++++++++++++++++++++++++----- pygeoapi/openapi.py | 24 +++++++++++++++++--- pygeoapi/starlette_app.py | 9 +++++++- pygeoapi/util.py | 18 ++++++++++++++- tests/test_openapi.py | 47 +++++++++++++++++++++++++++++++++++++++ tests/test_util.py | 8 ++++++- 7 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 tests/test_openapi.py diff --git a/pygeoapi-config.yml b/pygeoapi-config.yml index 75ef6a4..7fa40ef 100644 --- a/pygeoapi-config.yml +++ b/pygeoapi-config.yml @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2019 Tom Kralidis +# Copyright (c) 2020 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -42,6 +42,7 @@ server: map: url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png attribution: 'Wikimedia maps | Map data © OpenStreetMap contributors' + # ogc_schemas_location: /opt/schemas.opengis.net logging: level: ERROR diff --git a/pygeoapi/flask_app.py b/pygeoapi/flask_app.py index aca1d85..42d029b 100644 --- a/pygeoapi/flask_app.py +++ b/pygeoapi/flask_app.py @@ -3,7 +3,7 @@ # Authors: Tom Kralidis # Norman Barker # -# Copyright (c) 2018 Tom Kralidis +# Copyright (c) 2020 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -34,10 +34,10 @@ import os import click -from flask import Flask, make_response, request +from flask import Flask, make_response, request, send_from_directory from pygeoapi.api import API -from pygeoapi.util import yaml_load +from pygeoapi.util import get_mimetype, yaml_load APP = Flask(__name__) APP.url_map.strict_slashes = False @@ -55,11 +55,39 @@ if CONFIG['server'].get('cors', False): from flask_cors import CORS CORS(APP) -APP.config['JSONIFY_PRETTYPRINT_REGULAR'] = \ - CONFIG['server'].get('pretty_print', True) +APP.config['JSONIFY_PRETTYPRINT_REGULAR'] = CONFIG['server'].get( + 'pretty_print', True) api_ = API(CONFIG) +OGC_SCHEMAS_LOCATION = CONFIG['server'].get('ogc_schemas_location', None) + +if (OGC_SCHEMAS_LOCATION is not None and + not OGC_SCHEMAS_LOCATION.startswith('http')): + # serve the OGC schemas locally + + if not os.path.exists(OGC_SCHEMAS_LOCATION): + raise RuntimeError('OGC schemas misconfigured') + + @APP.route('/schemas/', methods=['GET']) + def schemas(path): + """ + Serve OGC schemas locally + + :param path: path of the OGC schema document + + :returns: HTTP response + """ + + full_filepath = os.path.join(OGC_SCHEMAS_LOCATION, path) + dirname_ = os.path.dirname(full_filepath) + basename_ = os.path.basename(full_filepath) + + # TODO: better sanitization? + path_ = dirname_.replace('..', '').replace('//', '') + return send_from_directory(path_, basename_, + mimetype=get_mimetype(basename_)) + @APP.route('/') def root(): diff --git a/pygeoapi/openapi.py b/pygeoapi/openapi.py index aa95e9d..8af0d98 100644 --- a/pygeoapi/openapi.py +++ b/pygeoapi/openapi.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2019 Tom Kralidis +# Copyright (c) 2020 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -29,6 +29,7 @@ from copy import deepcopy import logging +import os import click import yaml @@ -38,14 +39,27 @@ from pygeoapi.util import yaml_load LOGGER = logging.getLogger(__name__) -# TODO: handle this better once schemas are public/final -# allow also for schema caching OPENAPI_YAML = { 'oapif': 'http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml', # noqa 'oapip': 'https://raw.githubusercontent.com/opengeospatial/wps-rest-binding/master/core/openapi' # noqa } +def get_ogc_schemas_location(server_config): + + osl = server_config.get('ogc_schemas_location', None) + + value = 'http://schemas.opengis.net' + + if osl is not None: + if osl.startswith('http'): + value = osl + elif osl.startswith('/'): + value = os.path.join(server_config['url'], 'schemas') + + return value + + # TODO: remove this function once OGC API - Processing is final def gen_media_type_object(media_type, api_type, path): """ @@ -101,6 +115,10 @@ def get_oas_30(cfg): """ paths = {} + + osl = get_ogc_schemas_location(cfg['server']) + OPENAPI_YAML['oapif'] = os.path.join(osl, 'ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml') # noqa + LOGGER.debug('setting up server info') oas = { 'openapi': '3.0.2', diff --git a/pygeoapi/starlette_app.py b/pygeoapi/starlette_app.py index 56a9d36..6d5bc9e 100644 --- a/pygeoapi/starlette_app.py +++ b/pygeoapi/starlette_app.py @@ -4,6 +4,7 @@ # # # Copyright (c) 2019 Francesco Bartoli +# Copyright (c) 2020 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -46,7 +47,6 @@ app = Starlette() app.mount('/static', StaticFiles( directory='{}{}static'.format(os.path.dirname(os.path.realpath(__file__)), os.sep))) - CONFIG = None if 'PYGEOAPI_CONFIG' not in os.environ: @@ -60,6 +60,13 @@ if CONFIG['server'].get('cors', False): from starlette.middleware.cors import CORSMiddleware app.add_middleware(CORSMiddleware, allow_origins=['*']) +OGC_SCHEMAS_LOCATION = CONFIG['server'].get('ogc_schemas_location', None) + +if (OGC_SCHEMAS_LOCATION is not None and + not OGC_SCHEMAS_LOCATION.startswith('http')): + if not os.path.exists(OGC_SCHEMAS_LOCATION): + raise RuntimeError('OGC schemas misconfigured') + app.mount('/schemas', StaticFiles(directory=OGC_SCHEMAS_LOCATION)) api_ = API(CONFIG) diff --git a/pygeoapi/util.py b/pygeoapi/util.py index 038e936..3ae7816 100644 --- a/pygeoapi/util.py +++ b/pygeoapi/util.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2019 Tom Kralidis +# Copyright (c) 2020 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -33,6 +33,7 @@ from datetime import date, datetime, time from decimal import Decimal import json import logging +import mimetypes import os import re from urllib.parse import urlparse @@ -47,6 +48,9 @@ LOGGER = logging.getLogger(__name__) TEMPLATES = '{}{}templates'.format(os.path.dirname( os.path.realpath(__file__)), os.sep) +mimetypes.add_type('text/plain', '.yaml') +mimetypes.add_type('text/plain', '.yml') + def dategetter(date_property, collection): """ @@ -199,3 +203,15 @@ def render_j2_template(config, template, data): template = env.get_template(template) return template.render(config=config, data=data, version=__version__) + + +def get_mimetype(filename): + """ + helper function to return MIME type of a given file + + :param filename: filename (with extension) + + :returns: MIME type of given filename + """ + + return mimetypes.guess_type(filename)[0] diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 0000000..7098be6 --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,47 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2020 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 pygeoapi.openapi import get_ogc_schemas_location + + +def test_str2bool(): + + default = { + 'url': 'http://localhost:5000' + } + + osl = get_ogc_schemas_location(default) + assert osl == 'http://schemas.opengis.net' + + default['ogc_schemas_location'] = 'http://example.org/schemas' + osl = get_ogc_schemas_location(default) + assert osl == 'http://example.org/schemas' + + default['ogc_schemas_location'] = '/opt/schemas.opengis.net' + osl = get_ogc_schemas_location(default) diff --git a/tests/test_util.py b/tests/test_util.py index f4b8aca..bd97214 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -2,7 +2,7 @@ # # Authors: Tom Kralidis # -# Copyright (c) 2019 Tom Kralidis +# Copyright (c) 2020 Tom Kralidis # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -96,3 +96,9 @@ def test_json_serial(): with pytest.raises(TypeError): util.json_serial('foo') + + +def test_mimetype(): + assert util.get_mimetype('file.xml') == 'application/xml' + assert util.get_mimetype('file.yml') == 'text/plain' + assert util.get_mimetype('file.yaml') == 'text/plain'