Skip to content

Commit

Permalink
#1174 merge master - #1152 #1203 etc
Browse files Browse the repository at this point in the history
  • Loading branch information
justb4 committed Apr 4, 2023
2 parents b1ffaad + 48b5b3b commit ea1168a
Show file tree
Hide file tree
Showing 21 changed files with 1,251 additions and 262 deletions.
61 changes: 61 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pygeoapi configuration contains the following core sections:
- ``metadata``: server-wide metadata (contact, licensing, etc.)
- ``resources``: dataset collections, processes and stac-collections offered by the server

The full configuration schema with descriptions of all available properties can be found `here <https://github.com/geopython/pygeoapi/blob/master/pygeoapi/schemas/config/pygeoapi-config-0.x.yml>`_.

.. note::
`Standard YAML mechanisms <https://en.wikipedia.org/wiki/YAML#Advanced_components>`_ can be used (anchors, references, etc.) for reuse and compactness.

Expand All @@ -30,6 +32,7 @@ Reference
^^^^^^^^^^

The ``server`` section provides directives on binding and high level tuning.
Please find more information related to API design rules (the property at the bottom of the example below) :ref:`further down<API Design Rules>`.

.. code-block:: yaml
Expand Down Expand Up @@ -61,6 +64,12 @@ The ``server`` section provides directives on binding and high level tuning.
connection: /tmp/pygeoapi-process-manager.db # connection info to store jobs (e.g. filepath)
output_dir: /tmp/ # temporary file area for storing job results (files)
api_rules: # optional API design rules to which pygeoapi should adhere
api_version: 1.2.3 # omit to use pygeoapi's software version
strict_slashes: true # trailing slashes will not be allowed and result in a 404
url_prefix: 'v{api_major}' # adds a /v1 prefix to all URL paths
version_header: X-API-Version # add a response header of this name with the API version
``logging``
^^^^^^^^^^^
Expand Down Expand Up @@ -303,6 +312,58 @@ Examples:
curl https://example.org/collections/foo # user can access resource normally
API Design Rules
----------------

Some pygeoapi setups may wish to adhere to specific API design rules that apply at an organization.
The ``api_rules`` object in the ``server`` section of the configuration can be used for this purpose.

Note that the entire ``api_rules`` object is optional. No rules will be applied if the object is omitted.

The following properties can be set:

``api_version``
^^^^^^^^^^^^^^^

If specified, this property is a string that defines the semantic version number of the API.
Note that this number should reflect the state of the *API data model* (request and response object structure, API endpoints, etc.)
and does not necessarily correspond to the *software* version of pygeoapi. For example, the software could have been
completely rewritten (which changes the software version number), but the API data model might still be the same as before.

Unfortunately, pygeoapi currently does not offer a way to keep track of the API version.
This means that you need to set (and maintain) your own version here or leave it empty or unset.
In the latter case, the software version of pygeoapi will be used instead.

``strict_slashes``
^^^^^^^^^^^^^^^^^^

Some API rules state that trailing slashes at the end of a URL are not allowed if they point to a specific resource item.
In that case, you may wish to set this property to ``true``. Doing so will result in a ``404 Not Found`` if a user adds a ``/`` to the end of a URL.
If omitted or ``false`` (default), it does not matter whether the user omits or adds the ``/`` to the end of the URL.

``url_prefix``
^^^^^^^^^^^^^^

Set this property to include a prefix in the URL path (e.g. `https://base.com/<my_prefix>/endpoint`).
Note that you do not need to include slashes (either at the start or the end) here: they will be added automatically.

If you wish to include the API version number (depending on the `api_version`_ property) in the prefix, you can use the following variables:

- ``{api_version}``: full semantic version number
- ``{api_major}``: major version number
- ``{api_minor}``: minor version number
- ``{api_build}``: build number

For example, if the API version is *1.2.3*, then a URL prefix template of ``v{api_major}`` will result in *v1* as the actual prefix.

``version_header``
^^^^^^^^^^^^^^^^^^

Set this property to add a header to each pygeoapi response that includes the semantic API version (see `api_version`_).
If omitted, no header will be added. Common names for this header are ``API-Version`` or ``X-API-Version``.
Note that pygeoapi already adds a ``X-Powered-By`` header by default that includes the software version number.


Validating the configuration
----------------------------

Expand Down
71 changes: 38 additions & 33 deletions pygeoapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
from http import HTTPStatus
import json
import logging
import os
import re
from typing import Any, Tuple, Union, Optional
import urllib.parse
Expand Down Expand Up @@ -76,18 +75,16 @@
ProviderTileQueryError,
ProviderTilesetIdNotFoundError)
from pygeoapi.models.cql import CQLModel

from pygeoapi.util import (dategetter, DATETIME_FORMAT, UrlPrefetcher,
filter_dict_by_key_value, get_crs_from_uri,
get_supported_crs_list, CrsTransformSpec,
get_provider_by_type, get_provider_default,
get_typed_value, JobStatus, json_serial,
render_j2_template, str2bool,
transform_bbox, TEMPLATES, to_json)
transform_bbox, TEMPLATES, to_json, get_api_rules, get_base_url)

from pygeoapi.models.provider.base import TilesMetadataFormat


LOGGER = logging.getLogger(__name__)

#: Return headers for requests (e.g:X-Powered-By)
Expand Down Expand Up @@ -567,7 +564,8 @@ def is_valid(self, additional_formats=None) -> bool:

def get_response_headers(self, force_lang: l10n.Locale = None,
force_type: str = None,
force_encoding: str = None) -> dict:
force_encoding: str = None,
**custom_headers) -> dict:
"""
Prepares and returns a dictionary with Response object headers.
Expand Down Expand Up @@ -595,6 +593,7 @@ def get_response_headers(self, force_lang: l10n.Locale = None,
"""

headers = HEADERS.copy()
headers.update(**custom_headers)
l10n.set_response_language(headers, force_lang or self._locale)
if force_type:
# Set custom MIME type if specified
Expand Down Expand Up @@ -638,7 +637,8 @@ def __init__(self, config):
"""

self.config = config
self.config['server']['url'] = self.config['server']['url'].rstrip('/')
self.api_headers = get_api_rules(self.config).response_headers
self.base_url = get_base_url(self.config)
self.prefetcher = UrlPrefetcher()

CHARSET[0] = config['server'].get('encoding', 'utf-8')
Expand All @@ -660,6 +660,10 @@ def __init__(self, config):

setup_logger(self.config['logging'])

# Create config clone for HTML templating with modified base URL
self.tpl_config = deepcopy(self.config)
self.tpl_config['server']['url'] = self.base_url

# TODO: add as decorator
if 'manager' in self.config['server']:
manager_def = self.config['server']['manager']
Expand Down Expand Up @@ -708,34 +712,34 @@ def landing_page(self,
'rel': request.get_linkrel(F_JSON),
'type': FORMAT_TYPES[F_JSON],
'title': 'This document as JSON',
'href': f"{self.config['server']['url']}?f={F_JSON}"
'href': f"{self.base_url}?f={F_JSON}"
}, {
'rel': request.get_linkrel(F_JSONLD),
'type': FORMAT_TYPES[F_JSONLD],
'title': 'This document as RDF (JSON-LD)',
'href': f"{self.config['server']['url']}?f={F_JSONLD}"
'href': f"{self.base_url}?f={F_JSONLD}"
}, {
'rel': request.get_linkrel(F_HTML),
'type': FORMAT_TYPES[F_HTML],
'title': 'This document as HTML',
'href': f"{self.config['server']['url']}?f={F_HTML}",
'href': f"{self.base_url}?f={F_HTML}",
'hreflang': self.default_locale
}, {
'rel': 'service-desc',
'type': 'application/vnd.oai.openapi+json;version=3.0',
'title': 'The OpenAPI definition as JSON',
'href': f"{self.config['server']['url']}/openapi"
'href': f"{self.base_url}/openapi"
}, {
'rel': 'service-doc',
'type': FORMAT_TYPES[F_HTML],
'title': 'The OpenAPI definition as HTML',
'href': f"{self.config['server']['url']}/openapi?f={F_HTML}",
'href': f"{self.base_url}/openapi?f={F_HTML}",
'hreflang': self.default_locale
}, {
'rel': 'conformance',
'type': FORMAT_TYPES[F_JSON],
'title': 'Conformance',
'href': f"{self.config['server']['url']}/conformance"
'href': f"{self.base_url}/conformance"
}, {
'rel': 'data',
'type': FORMAT_TYPES[F_JSON],
Expand All @@ -745,15 +749,15 @@ def landing_page(self,
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/processes',
'type': FORMAT_TYPES[F_JSON],
'title': 'Processes',
'href': f"{self.config['server']['url']}/processes"
'href': f"{self.base_url}/processes"
}, {
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list',
'type': FORMAT_TYPES[F_JSON],
'title': 'Jobs',
'href': f"{self.config['server']['url']}/jobs"
'href': f"{self.base_url}/jobs"
}]

headers = request.get_response_headers()
headers = request.get_response_headers(**self.api_headers)
if request.format == F_HTML: # render

fcm['processes'] = False
Expand All @@ -767,8 +771,8 @@ def landing_page(self,
'type', 'stac-collection'):
fcm['stac'] = True

content = render_j2_template(self.config, 'landing_page.html', fcm,
request.locale)
content = render_j2_template(self.tpl_config, 'landing_page.html',
fcm, request.locale)
return headers, HTTPStatus.OK, content

if request.format == F_JSONLD:
Expand All @@ -793,19 +797,18 @@ def openapi(self, request: Union[APIRequest, Any],
if not request.is_valid():
return self.get_format_exception(request)

headers = request.get_response_headers()
headers = request.get_response_headers(**self.api_headers)

if request.format == F_HTML:
template = 'openapi/swagger.html'
if request._args.get('ui') == 'redoc':
template = 'openapi/redoc.html'

path = '/'.join([self.config['server']['url'].rstrip('/'),
'openapi'])
path = f'{self.base_url}/openapi'
data = {
'openapi-document-path': path
}
content = render_j2_template(self.config, template, data,
content = render_j2_template(self.tpl_config, template, data,
request.locale)
return headers, HTTPStatus.OK, content

Expand Down Expand Up @@ -845,9 +848,9 @@ def conformance(self,
'conformsTo': list(set(conformance_list))
}

headers = request.get_response_headers()
headers = request.get_response_headers(**self.api_headers)
if request.format == F_HTML: # render
content = render_j2_template(self.config, 'conformance.html',
content = render_j2_template(self.tpl_config, 'conformance.html',
conformance, request.locale)
return headers, HTTPStatus.OK, content

Expand All @@ -869,7 +872,7 @@ def describe_collections(self, request: Union[APIRequest, Any],

if not request.is_valid():
return self.get_format_exception(request)
headers = request.get_response_headers()
headers = request.get_response_headers(**self.api_headers)

fcm = {
'collections': [],
Expand Down Expand Up @@ -975,13 +978,13 @@ def describe_collections(self, request: Union[APIRequest, Any],
'type': FORMAT_TYPES[F_JSON],
'rel': 'root',
'title': 'The landing page of this server as JSON',
'href': f"{self.config['server']['url']}?f={F_JSON}"
'href': f"{self.base_url}?f={F_JSON}"
})
collection['links'].append({
'type': FORMAT_TYPES[F_HTML],
'rel': 'root',
'title': 'The landing page of this server as HTML',
'href': f"{self.config['server']['url']}?f={F_HTML}"
'href': f"{self.base_url}?f={F_HTML}"
})
collection['links'].append({
'type': FORMAT_TYPES[F_JSON],
Expand Down Expand Up @@ -1154,7 +1157,7 @@ def describe_collections(self, request: Union[APIRequest, Any],
'type': map_mimetype,
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/map',
'title': f'Map as {map_format}',
'href': f"{self.config['server']['url']}/collections/{k}/map?f={map_format}" # noqa
'href': f"{self.get_collections_url()}/{k}/map?f={map_format}" # noqa
})

try:
Expand Down Expand Up @@ -1225,11 +1228,11 @@ def describe_collections(self, request: Union[APIRequest, Any],
if request.format == F_HTML: # render
fcm['collections_path'] = self.get_collections_url()
if dataset is not None:
content = render_j2_template(self.config,
content = render_j2_template(self.tpl_config,
'collections/collection.html',
fcm, request.locale)
else:
content = render_j2_template(self.config,
content = render_j2_template(self.tpl_config,
'collections/index.html', fcm,
request.locale)

Expand Down Expand Up @@ -1265,7 +1268,7 @@ def get_collection_queryables(self, request: Union[APIRequest, Any],

if not request.is_valid():
return self.get_format_exception(request)
headers = request.get_response_headers()
headers = request.get_response_headers(**self.api_headers)

if any([dataset is None,
dataset not in self.config['resources'].keys()]):
Expand Down Expand Up @@ -1330,7 +1333,7 @@ def get_collection_queryables(self, request: Union[APIRequest, Any],

queryables['collections_path'] = self.get_collections_url()

content = render_j2_template(self.config,
content = render_j2_template(self.tpl_config,
'collections/queryables.html',
queryables, request.locale)

Expand Down Expand Up @@ -1359,7 +1362,8 @@ def get_collection_items(

# Set Content-Language to system locale until provider locale
# has been determined
headers = request.get_response_headers(SYSTEM_LOCALE)
headers = request.get_response_headers(SYSTEM_LOCALE,
**self.api_headers)

properties = []
reserved_fieldnames = ['bbox', 'bbox-crs', 'crs', 'f', 'lang', 'limit',
Expand Down Expand Up @@ -2201,7 +2205,8 @@ def get_collection_item(self, request: Union[APIRequest, Any],

# Set Content-Language to system locale until provider locale
# has been determined
headers = request.get_response_headers(SYSTEM_LOCALE)
headers = request.get_response_headers(SYSTEM_LOCALE,
**self.api_headers)

LOGGER.debug('Processing query parameters')

Expand Down
18 changes: 10 additions & 8 deletions pygeoapi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@
THISDIR = Path(__file__).parent.resolve()


def load_schema() -> dict:
""" Reads the JSON schema YAML file. """
schema_file = THISDIR / 'schemas' / 'config' / 'pygeoapi-config-0.x.yml'

with schema_file.open() as fh2:
return yaml_load(fh2)


def validate_config(instance_dict: dict) -> bool:
"""
Validate pygeoapi configuration against pygeoapi schema
Expand All @@ -48,14 +56,8 @@ def validate_config(instance_dict: dict) -> bool:
:returns: `bool` of validation
"""

schema_file = THISDIR / 'schemas' / 'config' / 'pygeoapi-config-0.x.yml'

with schema_file.open() as fh2:
schema_dict = yaml_load(fh2)
jsonschema_validate(json.loads(to_json(instance_dict)), schema_dict)

return True
jsonschema_validate(json.loads(to_json(instance_dict)), load_schema())
return True


@click.group()
Expand Down
7 changes: 7 additions & 0 deletions pygeoapi/django_/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import os
# pygeoapi specific
from pygeoapi.django_app import config
from pygeoapi.util import get_api_rules

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Expand Down Expand Up @@ -166,3 +167,9 @@

# pygeoapi specific
PYGEOAPI_CONFIG = config()

API_RULES = get_api_rules(PYGEOAPI_CONFIG)

# Defaults to True in Django
# https://docs.djangoproject.com/en/3.2/ref/settings/#append-slash
APPEND_SLASH = not API_RULES.strict_slashes
Loading

0 comments on commit ea1168a

Please sign in to comment.