* add support for OGC API - Coverages * fix coverage CRS ref * fix ref to OACov schemas for testing * move spectral testing to after_success * update docs * add mask param to rasterio provider
This commit is contained in:
+3
-3
@@ -82,8 +82,6 @@ script:
|
||||
- pytest --cov=pygeoapi
|
||||
- make -C ./docs html
|
||||
- find . -type f -name "*.py" | xargs flake8
|
||||
# run linting openapi document through dockerized spectral cli
|
||||
- docker run --rm -it -v $(pwd):/tmp stoplight/spectral lint "/tmp/pygeoapi-openapi.yml"
|
||||
# run docker image with cite configuration
|
||||
- docker run -d -p 5001:5001 --network host --add-host="localhost:127.0.0.1" --rm -it -v $(pwd)/tests/cite/ogcapi-features/cite.config.yml:/pygeoapi/local.config.yml --name pygeoapi-travis-master geopython/pygeoapi:latest run
|
||||
- docker ps | grep -wq 'pygeoapi-travis-master'
|
||||
@@ -92,6 +90,8 @@ script:
|
||||
- docker run --rm --name pygeoapi-travis-runtests-master geopython/pygeoapi:latest test
|
||||
|
||||
after_success:
|
||||
# run linting openapi document through dockerized spectral cli
|
||||
- docker run --rm -it -v $(pwd):/tmp stoplight/spectral lint "/tmp/pygeoapi-openapi.yml"
|
||||
- python3 setup.py sdist bdist_wheel --universal
|
||||
- sudo debuild -b -uc -us
|
||||
|
||||
@@ -99,4 +99,4 @@ notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#pygeoapi-activity"
|
||||
use_notice: true
|
||||
use_notice: true
|
||||
|
||||
@@ -158,12 +158,17 @@ default.
|
||||
# see pygeoapi.plugin for supported providers
|
||||
# for custom built plugins, use the import path (e.g. mypackage.provider.MyProvider)
|
||||
# see Plugins section for more information
|
||||
- type: feature # underlying data geospatial type: (allowed values are: feature)
|
||||
- type: feature # underlying data geospatial type: (allowed values are: feature, coverage)
|
||||
default: true # optional: if not specified, the first provider definition is considered the default
|
||||
name: CSV
|
||||
data: tests/data/obs.csv # required: the data filesystem path or URL, depending on plugin setup
|
||||
id_field: id # required for vector data, the field corresponding to the ID
|
||||
time_field: datetimestamp # optional field corresponding to the temporal propert of the dataset
|
||||
format: # optional default format
|
||||
name: GeoJSON # required: format name
|
||||
mimetype: application/json # required: format mimetype
|
||||
options: # optional options to pass to provider (i.e. GDAL creation)
|
||||
option_name: option_value
|
||||
properties: # optional: only return the following properties, in order
|
||||
- stn_id
|
||||
- value
|
||||
|
||||
@@ -19,5 +19,6 @@ return back data to the pygeoapi API framework in a plug and play fashion.
|
||||
:name: Data publishing
|
||||
|
||||
ogcapi-features
|
||||
ogcapi-coverages
|
||||
ogcapi-processes
|
||||
stac
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
.. _ogcapi-coverages:
|
||||
|
||||
Publishing raster data to OGC API - Coverages
|
||||
=============================================
|
||||
|
||||
`OGC API - Coverages`_ provides geospatial data access functionality to raster data.
|
||||
|
||||
To add raster data to pygeoapi, you can use the dataset example in :ref:`configuration`
|
||||
as a baseline and modify accordingly.
|
||||
|
||||
Providers
|
||||
---------
|
||||
|
||||
pygeoapi core feature providers are listed below, along with a matrix of supported query
|
||||
parameters.
|
||||
|
||||
.. csv-table::
|
||||
:header: Provider, rangeSubset, subsets
|
||||
:align: left
|
||||
|
||||
rasterio,✔️,✔️,✔️,
|
||||
|
||||
|
||||
Below are specific connection examples based on supported providers.
|
||||
|
||||
Connection examples
|
||||
-------------------
|
||||
|
||||
rasterio
|
||||
^^^^^^^^
|
||||
|
||||
The `rasterio`_ provider plugin reads and extracts any data that rasterio is
|
||||
capable of handling.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
providers:
|
||||
- type: coverage
|
||||
name: rasterio
|
||||
data: tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2
|
||||
options: # optional creation options
|
||||
DATA_ENCODING: COMPLEX_PACKING
|
||||
format:
|
||||
name: GRIB2
|
||||
mimetype: application/x-grib2
|
||||
|
||||
Data access examples
|
||||
--------------------
|
||||
|
||||
- list all collections
|
||||
- http://localhost:5000/collections
|
||||
- overview of dataset
|
||||
- http://localhost:5000/collections/foo
|
||||
- coverage rangetype
|
||||
- http://localhost:5000/collections/foo/coverage/rangetype
|
||||
- coverage domainset
|
||||
- http://localhost:5000/collections/foo/coverage/domainset
|
||||
- coverage access via CoverageJSON (default)
|
||||
- http://localhost:5000/collections/foo/coverage?f=json
|
||||
- coverage access via native format (as defined in ``provider.format.name``)
|
||||
- http://localhost:5000/collections/foo/coverage?f=GRIB2
|
||||
- coverage access with comma-separated rangeSubset
|
||||
- http://localhost:5000/collections/foo/coverage?rangeSubset=1,3
|
||||
- coverage access with subsetting
|
||||
- http://localhost:5000/collections/foo/coverage?subset=lat(10,20)&subset=long(10,20)
|
||||
|
||||
.. _`OGC API - Coverages`: https://github.com/opengeospatial/ogc_api_coverages
|
||||
.. _`rasterio`: https://rasterio.readthedocs.io
|
||||
@@ -10,8 +10,8 @@ Features
|
||||
|
||||
- out of the box modern OGC API server
|
||||
- certified OGC Compliant and Reference Implementation for OGC API - Features
|
||||
- additionally implements OGC API - Processes and SpatioTemporal Asset Library
|
||||
- out of the box data provider plugins for GDAL/OGR, Elasticsearch, PostgreSQL/PostGIS
|
||||
- additionally implements OGC API - Coverages, OGC API - Processes and SpatioTemporal Asset Library
|
||||
- out of the box data provider plugins for rasterio, GDAL/OGR, Elasticsearch, PostgreSQL/PostGIS
|
||||
- easy to use OpenAPI / Swagger documentation for developers
|
||||
- supports JSON, GeoJSON, HTML and CSV output
|
||||
- supports data filtering by spatial, temporal or attribute queries
|
||||
@@ -36,6 +36,7 @@ Standards are at the core of pygeoapi. Below is the project's standards support
|
||||
:widths: 20, 20
|
||||
|
||||
`OGC API - Features`_,Reference Implementation
|
||||
`OGC API - Coverages`_,Implementing
|
||||
`OGC API - Processes`_,Implementing
|
||||
`SpatioTemporal Asset Catalog`_,Implementing
|
||||
|
||||
@@ -43,5 +44,6 @@ Standards are at the core of pygeoapi. Below is the project's standards support
|
||||
.. _`pygeoapi`: https://pygeoapi.io
|
||||
.. _`OGC API`: https://ogcapi.ogc.org
|
||||
.. _`OGC API - Features`: https://www.ogc.org/standards/ogcapi-features
|
||||
.. _`OGC API - Coverages`: https://github.com/opengeospatial/ogc_api_coverages
|
||||
.. _`OGC API - Processes`: https://github.com/opengeospatial/wps-rest-binding
|
||||
.. _`SpatioTemporal Asset Catalog`: https://stacspec.org
|
||||
|
||||
+63
-12
@@ -38,22 +38,22 @@ pygeoapi for easier maintenance of software updates.
|
||||
updates and package management
|
||||
|
||||
|
||||
Example: custom pygeoapi data provider
|
||||
--------------------------------------
|
||||
Example: custom pygeoapi vector data provider
|
||||
---------------------------------------------
|
||||
|
||||
Lets consider the steps for a data provider plugin (source code is located here: :ref:`data Provider`).
|
||||
Lets consider the steps for a vector data provider plugin (source code is located here: :ref:`data Provider`).
|
||||
|
||||
Python code
|
||||
^^^^^^^^^^^
|
||||
|
||||
The below template provides a minimal example (let's call the file ``mycooldata.py``:
|
||||
The below template provides a minimal example (let's call the file ``mycoolvectordata.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pygeoapi.provider.base import BaseProvider
|
||||
|
||||
class MyCoolDataProvider(BaseProvider):
|
||||
"""My cool data provider"""
|
||||
class MyCoolVectorDataProvider(BaseProvider):
|
||||
"""My cool vector data provider"""
|
||||
|
||||
def __init__(self, provider_def):
|
||||
"""Inherit from parent class"""
|
||||
@@ -91,7 +91,7 @@ The below template provides a minimal example (let's call the file ``mycooldata.
|
||||
|
||||
|
||||
For brevity, the above code will always return the single feature of the dataset. In reality, the plugin
|
||||
developer would connect to a data source with capabilities to run queries and return relevant a result set,
|
||||
developer would connect to a data source with capabilities to run queries and return a relevant result set,
|
||||
as well as implement the ``get`` method accordingly. As long as the plugin implements the API contract of
|
||||
its base provider, all other functionality is left to the provider implementation.
|
||||
|
||||
@@ -104,23 +104,23 @@ The following methods are options to connect the plugin to pygeoapi:
|
||||
|
||||
**Option 1**: Update in core pygeoapi:
|
||||
|
||||
- copy ``mycooldata.py`` into ``pygeoapi/provider``
|
||||
- copy ``mycoolvectordata.py`` into ``pygeoapi/provider``
|
||||
- update the plugin registry in ``pygeoapi/plugin.py:PLUGINS['provider']`` with the plugin's
|
||||
shortname (say ``MyCoolData``) and dotted path to the class (i.e. ``pygeoapi.provider.mycooldata.MyCoolDataProvider``)
|
||||
shortname (say ``MyCoolVectorData``) and dotted path to the class (i.e. ``pygeoapi.provider.mycoolvectordata.MyCoolVectorDataProvider``)
|
||||
- specify in your dataset provider configuration as follows:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
providers:
|
||||
- type: feature
|
||||
name: MyCoolData
|
||||
name: MyCoolVectorData
|
||||
data: /path/to/file
|
||||
id_field: stn_id
|
||||
|
||||
|
||||
**Option 2**: implement outside of pygeoapi and add to configuration (recommended)
|
||||
|
||||
- create a Python package of the ``mycooldata.py`` module (see `Cookiecutter`_ as an example)
|
||||
- create a Python package of the ``mycoolvectordata.py`` module (see `Cookiecutter`_ as an example)
|
||||
- install your Python package onto your system (``python setup.py install``). At this point your new package
|
||||
should be in the ``PYTHONPATH`` of your pygeoapi installation
|
||||
- specify in your dataset provider configuration as follows:
|
||||
@@ -129,10 +129,61 @@ The following methods are options to connect the plugin to pygeoapi:
|
||||
|
||||
providers:
|
||||
- type: feature
|
||||
name: mycooldatapackage.mycooldata.MyCoolDataProvider
|
||||
name: mycooldatapackage.mycoolvectordata.MyCoolVectorDataProvider
|
||||
data: /path/to/file
|
||||
id_field: stn_id
|
||||
|
||||
BEGIN
|
||||
|
||||
Example: custom pygeoapi raster data provider
|
||||
---------------------------------------------
|
||||
|
||||
Lets consider the steps for a raster data provider plugin (source code is located here: :ref:`data Provider`).
|
||||
|
||||
Python code
|
||||
^^^^^^^^^^^
|
||||
|
||||
The below template provides a minimal example (let's call the file ``mycoolrasterdata.py``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pygeoapi.provider.base import BaseProvider
|
||||
|
||||
class MyCoolRasterDataProvider(BaseProvider):
|
||||
"""My cool raster data provider"""
|
||||
|
||||
def __init__(self, provider_def):
|
||||
"""Inherit from parent class"""
|
||||
|
||||
BaseProvider.__init__(self, provider_def)
|
||||
self.num_bands = 4
|
||||
self.axes = ['Lat', 'Long']
|
||||
|
||||
def get_coverage_domainset(self):
|
||||
# return a CIS JSON DomainSet
|
||||
|
||||
def get_coverage_rangetype(self):
|
||||
# return a CIS JSON RangeType
|
||||
|
||||
def query(self, bands=[], subsets={}, format_='json'):
|
||||
# process bands and subsets parameters
|
||||
# query/extract coverage data
|
||||
if format_ == 'json':
|
||||
# return a CoverageJSON representation
|
||||
return {'type': 'Coverage', ...} # trimmed for brevity
|
||||
else:
|
||||
# return default (likely binary) representation
|
||||
return bytes(112)
|
||||
|
||||
For brevity, the above code will always JSON for metadata and binary or CoverageJSON for the data. In reality, the plugin
|
||||
developer would connect to a data source with capabilities to run queries and return a relevant result set,
|
||||
As long as the plugin implements the API contract of its base provider, all other functionality is left to the provider
|
||||
implementation.
|
||||
|
||||
Each base class documents the functions, arguments and return types required for implementation.
|
||||
|
||||
END
|
||||
|
||||
Example: custom pygeoapi formatter
|
||||
----------------------------------
|
||||
|
||||
|
||||
+29
-1
@@ -145,12 +145,40 @@ resources:
|
||||
data: tests/data/ne_110m_lakes.geojson
|
||||
id_field: id
|
||||
|
||||
gdps-temperature:
|
||||
type: collection
|
||||
title: Global Deterministic Prediction System sample
|
||||
description: Global Deterministic Prediction System sample
|
||||
keywords:
|
||||
- gdps
|
||||
- global
|
||||
extents:
|
||||
spatial:
|
||||
bbox: [-180,-90,180,90]
|
||||
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
|
||||
links:
|
||||
- type: text/html
|
||||
rel: canonical
|
||||
title: information
|
||||
href: https://eccc-msc.github.io/open-data/msc-data/nwp_gdps/readme_gdps_en
|
||||
hreflang: en-CA
|
||||
providers:
|
||||
- type: coverage
|
||||
name: rasterio
|
||||
data: tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2
|
||||
options:
|
||||
DATA_ENCODING: COMPLEX_PACKING
|
||||
format:
|
||||
name: GRIB2
|
||||
mimetype: application/x-grib2
|
||||
|
||||
test-data:
|
||||
type: stac-collection
|
||||
title: pygeoapi test data
|
||||
description: pygeoapi test data
|
||||
keywords:
|
||||
- poi,portugal
|
||||
- poi
|
||||
- portugal
|
||||
links:
|
||||
- type: text/html
|
||||
rel: canonical
|
||||
|
||||
+538
-192
File diff suppressed because it is too large
Load Diff
@@ -217,6 +217,69 @@ def collection_items(collection_id, item_id=None):
|
||||
return response
|
||||
|
||||
|
||||
@APP.route('/collections/<collection_id>/coverage')
|
||||
def collection_coverage(collection_id):
|
||||
"""
|
||||
OGC API - Coverages coverage endpoint
|
||||
|
||||
:param collection_id: collection identifier
|
||||
|
||||
:returns: HTTP response
|
||||
"""
|
||||
|
||||
headers, status_code, content = api_.get_collection_coverage(
|
||||
request.headers, request.args, collection_id)
|
||||
|
||||
response = make_response(content, status_code)
|
||||
|
||||
if headers:
|
||||
response.headers = headers
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@APP.route('/collections/<collection_id>/coverage/domainset')
|
||||
def collection_coverage_domainset(collection_id):
|
||||
"""
|
||||
OGC API - Coverages coverage domainset endpoint
|
||||
|
||||
:param collection_id: collection identifier
|
||||
|
||||
:returns: HTTP response
|
||||
"""
|
||||
|
||||
headers, status_code, content = api_.get_collection_coverage_domainset(
|
||||
request.headers, request.args, collection_id)
|
||||
|
||||
response = make_response(content, status_code)
|
||||
|
||||
if headers:
|
||||
response.headers = headers
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@APP.route('/collections/<collection_id>/coverage/rangetype')
|
||||
def collection_coverage_rangetype(collection_id):
|
||||
"""
|
||||
OGC API - Coverages coverage rangetype endpoint
|
||||
|
||||
:param collection_id: collection identifier
|
||||
|
||||
:returns: HTTP response
|
||||
"""
|
||||
|
||||
headers, status_code, content = api_.get_collection_coverage_rangetype(
|
||||
request.headers, request.args, collection_id)
|
||||
|
||||
response = make_response(content, status_code)
|
||||
|
||||
if headers:
|
||||
response.headers = headers
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@APP.route('/processes')
|
||||
@APP.route('/processes/<process_id>')
|
||||
def processes(process_id=None):
|
||||
|
||||
+164
-85
@@ -35,6 +35,7 @@ import click
|
||||
import yaml
|
||||
|
||||
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,
|
||||
yaml_load)
|
||||
|
||||
@@ -42,7 +43,9 @@ LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
'oapip': 'https://raw.githubusercontent.com/opengeospatial/wps-rest-binding/master/core/openapi', # noqa
|
||||
# 'oacov': 'https://raw.githubusercontent.com/opengeospatial/OGC-API-Sprint-August-2020/master/docs/Draft_Spring_Guide_for_OGC_API_Coverages/openapi' # noqa
|
||||
'oacov': 'https://raw.githubusercontent.com/tomkralidis/ogcapi-coverages-1/fix-cis/yaml-unresolved' # noqa
|
||||
}
|
||||
|
||||
|
||||
@@ -384,47 +387,28 @@ def get_oas_30(cfg):
|
||||
}
|
||||
}
|
||||
|
||||
items_path = '{}/items'.format(collection_name_path)
|
||||
LOGGER.debug('setting up feature endpoints')
|
||||
try:
|
||||
p = load_plugin('provider', get_provider_by_type(
|
||||
collections[k]['providers'], 'feature'))
|
||||
|
||||
paths[items_path] = {
|
||||
'get': {
|
||||
'summary': 'Get {} items'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}Features'.format(k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
{'$ref': '{}#/components/parameters/bbox'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
{'$ref': '{}#/components/parameters/limit'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
{'$ref': '#/components/parameters/sortby'},
|
||||
{'$ref': '#/components/parameters/startindex'}
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '{}#/components/responses/Features'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
items_path = '{}/items'.format(collection_name_path)
|
||||
|
||||
p = load_plugin('provider', get_provider_by_type(
|
||||
collections[k]['providers'], 'feature'))
|
||||
|
||||
if p.fields:
|
||||
queryables_path = '{}/queryables'.format(collection_name_path)
|
||||
|
||||
paths[queryables_path] = {
|
||||
paths[items_path] = {
|
||||
'get': {
|
||||
'summary': 'Get {} queryables'.format(v['title']),
|
||||
'summary': 'Get {} items'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}Queryables'.format(k.capitalize()),
|
||||
'operationId': 'get{}Features'.format(k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
{'$ref': '{}#/components/parameters/bbox'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
{'$ref': '{}#/components/parameters/limit'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
{'$ref': '#/components/parameters/sortby'},
|
||||
{'$ref': '#/components/parameters/startindex'}
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '#/components/responses/Queryables'},
|
||||
'200': {'$ref': '{}#/components/responses/Features'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'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
|
||||
@@ -432,64 +416,159 @@ def get_oas_30(cfg):
|
||||
}
|
||||
}
|
||||
|
||||
if p.time_field is not None:
|
||||
paths[items_path]['get']['parameters'].append(
|
||||
{'$ref': '{}#/components/parameters/datetime'.format(OPENAPI_YAML['oapif'])}) # noqa
|
||||
if p.fields:
|
||||
queryables_path = '{}/queryables'.format(collection_name_path)
|
||||
|
||||
for field, type in p.fields.items():
|
||||
|
||||
if p.properties and field not in p.properties:
|
||||
LOGGER.debug('Provider specified not to advertise property')
|
||||
continue
|
||||
|
||||
if type == 'date':
|
||||
schema = {
|
||||
'type': 'string',
|
||||
'format': 'date'
|
||||
}
|
||||
elif type == 'float':
|
||||
schema = {
|
||||
'type': 'number',
|
||||
'format': 'float'
|
||||
}
|
||||
elif type == 'long':
|
||||
schema = {
|
||||
'type': 'integer',
|
||||
'format': 'int64'
|
||||
}
|
||||
else:
|
||||
schema = {
|
||||
'type': type
|
||||
paths[queryables_path] = {
|
||||
'get': {
|
||||
'summary': 'Get {} queryables'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}Queryables'.format(
|
||||
k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '#/components/responses/Queryables'}, # noqa
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path_ = '{}/items'.format(collection_name_path)
|
||||
paths['{}'.format(path_)]['get']['parameters'].append({
|
||||
'name': field,
|
||||
'in': 'query',
|
||||
'required': False,
|
||||
'schema': schema,
|
||||
'style': 'form',
|
||||
'explode': False
|
||||
})
|
||||
if p.time_field is not None:
|
||||
paths[items_path]['get']['parameters'].append(
|
||||
{'$ref': '{}#/components/parameters/datetime'.format(OPENAPI_YAML['oapif'])}) # noqa
|
||||
|
||||
paths['{}/items/{{featureId}}'.format(collection_name_path)] = {
|
||||
'get': {
|
||||
'summary': 'Get {} item by id'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}Feature'.format(k.capitalize()),
|
||||
'parameters': [
|
||||
{'$ref': '{}#/components/parameters/featureId'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
{'$ref': '#/components/parameters/f'}
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '{}#/components/responses/Feature'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'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
|
||||
for field, type in p.fields.items():
|
||||
|
||||
if p.properties and field not in p.properties:
|
||||
LOGGER.debug('Provider specified not to advertise property') # noqa
|
||||
continue
|
||||
|
||||
if type == 'date':
|
||||
schema = {
|
||||
'type': 'string',
|
||||
'format': 'date'
|
||||
}
|
||||
elif type == 'float':
|
||||
schema = {
|
||||
'type': 'number',
|
||||
'format': 'float'
|
||||
}
|
||||
elif type == 'long':
|
||||
schema = {
|
||||
'type': 'integer',
|
||||
'format': 'int64'
|
||||
}
|
||||
else:
|
||||
schema = {
|
||||
'type': type
|
||||
}
|
||||
|
||||
path_ = '{}/items'.format(collection_name_path)
|
||||
paths['{}'.format(path_)]['get']['parameters'].append({
|
||||
'name': field,
|
||||
'in': 'query',
|
||||
'required': False,
|
||||
'schema': schema,
|
||||
'style': 'form',
|
||||
'explode': False
|
||||
})
|
||||
|
||||
paths['{}/items/{{featureId}}'.format(collection_name_path)] = {
|
||||
'get': {
|
||||
'summary': 'Get {} item by id'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}Feature'.format(k.capitalize()),
|
||||
'parameters': [
|
||||
{'$ref': '{}#/components/parameters/featureId'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
{'$ref': '#/components/parameters/f'}
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '{}#/components/responses/Feature'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
except ProviderTypeError:
|
||||
LOGGER.debug('collection is not feature based')
|
||||
|
||||
LOGGER.debug('setting up coverage endpoints')
|
||||
try:
|
||||
load_plugin('provider', get_provider_by_type(
|
||||
collections[k]['providers'], 'coverage'))
|
||||
|
||||
coverage_path = '{}/coverage'.format(collection_name_path)
|
||||
|
||||
paths[coverage_path] = {
|
||||
'get': {
|
||||
'summary': 'Get {} coverage'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}Coverage'.format(k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '{}#/components/responses/Features'.format(OPENAPI_YAML['oapif'])}, # noqa
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coverage_domainset_path = '{}/coverage/domainset'.format(
|
||||
collection_name_path)
|
||||
|
||||
paths[coverage_domainset_path] = {
|
||||
'get': {
|
||||
'summary': 'Get {} coverage domain set'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}CoverageDomainSet'.format(
|
||||
k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '{}/schemas/cis_1.1/domainSet.yaml'.format(OPENAPI_YAML['oacov'])}, # noqa
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coverage_rangetype_path = '{}/coverage/rangetype'.format(
|
||||
collection_name_path)
|
||||
|
||||
paths[coverage_rangetype_path] = {
|
||||
'get': {
|
||||
'summary': 'Get {} coverage range type'.format(v['title']),
|
||||
'description': v['description'],
|
||||
'tags': [k],
|
||||
'operationId': 'get{}CoverageRangeType'.format(
|
||||
k.capitalize()),
|
||||
'parameters': [
|
||||
items_f,
|
||||
],
|
||||
'responses': {
|
||||
'200': {'$ref': '{}/schemas/cis_1.1/rangeType.yaml'.format(OPENAPI_YAML['oacov'])}, # noqa
|
||||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
except ProviderTypeError:
|
||||
LOGGER.debug('collection is not coverage based')
|
||||
|
||||
LOGGER.debug('setting up STAC')
|
||||
stac_collections = filter_dict_by_key_value(cfg['resources'],
|
||||
|
||||
+2
-1
@@ -44,7 +44,8 @@ PLUGINS = {
|
||||
'PostgreSQL': 'pygeoapi.provider.postgresql.PostgreSQLProvider',
|
||||
'SQLiteGPKG': 'pygeoapi.provider.sqlite.SQLiteGPKGProvider',
|
||||
'MongoDB': 'pygeoapi.provider.mongo.MongoProvider',
|
||||
'FileSystem': 'pygeoapi.provider.filesystem.FileSystemProvider'
|
||||
'FileSystem': 'pygeoapi.provider.filesystem.FileSystemProvider',
|
||||
'rasterio': 'pygeoapi.provider.rasterio_.RasterioProvider'
|
||||
},
|
||||
'formatter': {
|
||||
'CSV': 'pygeoapi.formatter.csv_.CSVFormatter'
|
||||
|
||||
@@ -44,14 +44,25 @@ class BaseProvider:
|
||||
:returns: pygeoapi.providers.base.BaseProvider
|
||||
"""
|
||||
|
||||
self.name = provider_def['name']
|
||||
self.data = provider_def['data']
|
||||
try:
|
||||
self.name = provider_def['name']
|
||||
self.type = provider_def['type']
|
||||
self.data = provider_def['data']
|
||||
except KeyError:
|
||||
raise RuntimeError('name/type/data are required')
|
||||
|
||||
self.options = provider_def.get('options', None)
|
||||
self.id_field = provider_def.get('id_field', None)
|
||||
self.time_field = provider_def.get('time_field')
|
||||
self.properties = provider_def.get('properties', [])
|
||||
self.file_types = provider_def.get('file_types', [])
|
||||
self.fields = {}
|
||||
|
||||
# for coverage providers
|
||||
self.axes = []
|
||||
self.crs = None
|
||||
self.num_bands = None
|
||||
|
||||
def get_fields(self):
|
||||
"""
|
||||
Get provider field information (names, types)
|
||||
@@ -74,11 +85,21 @@ class BaseProvider:
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_metadata(self):
|
||||
"""
|
||||
Provide data/file metadata
|
||||
|
||||
:returns: `dict` of metadata construct (format
|
||||
determined by provider/standard)
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def query(self):
|
||||
"""
|
||||
query the provider
|
||||
|
||||
:returns: dict of 0..n GeoJSON features
|
||||
:returns: dict of 0..n GeoJSON features or coverage data
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
@@ -108,6 +129,24 @@ class BaseProvider:
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_coverage_domainset(self):
|
||||
"""
|
||||
Provide coverage domainset
|
||||
|
||||
:returns: CIS JSON object of domainset metadata
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_coverage_rangetype(self):
|
||||
"""
|
||||
Provide coverage rangetype
|
||||
|
||||
:returns: CIS JSON object of rangetype metadata
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete(self, identifier):
|
||||
"""Deletes an existing feature
|
||||
|
||||
@@ -130,13 +169,23 @@ class ProviderConnectionError(ProviderGenericError):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderTypeError(ProviderGenericError):
|
||||
"""provider type error"""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderInvalidQueryError(ProviderGenericError):
|
||||
"""provider invalid query error"""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderQueryError(ProviderGenericError):
|
||||
"""provider query error"""
|
||||
pass
|
||||
|
||||
|
||||
class ProviderItemNotFoundError(ProviderGenericError):
|
||||
"""provider query error"""
|
||||
"""provider item not found query error"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
# =================================================================
|
||||
#
|
||||
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# =================================================================
|
||||
|
||||
import io
|
||||
import logging
|
||||
|
||||
import rasterio
|
||||
from rasterio.io import MemoryFile
|
||||
import rasterio.mask
|
||||
|
||||
from pygeoapi.provider.base import (BaseProvider, ProviderConnectionError,
|
||||
ProviderQueryError)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RasterioProvider(BaseProvider):
|
||||
"""Rasterio Provider"""
|
||||
|
||||
def __init__(self, provider_def):
|
||||
"""
|
||||
Initialize object
|
||||
|
||||
:param provider_def: provider definition
|
||||
|
||||
:returns: pygeoapi.providers.rasterio_.RasterioProvider
|
||||
"""
|
||||
|
||||
BaseProvider.__init__(self, provider_def)
|
||||
|
||||
try:
|
||||
self._data = rasterio.open(self.data)
|
||||
self._coverage_properties = self._get_coverage_properties()
|
||||
self.axes = self._coverage_properties['axes']
|
||||
self.crs = self._coverage_properties['bbox_crs']
|
||||
self.num_bands = self._coverage_properties['num_bands']
|
||||
except Exception as err:
|
||||
LOGGER.warning(err)
|
||||
raise ProviderConnectionError(err)
|
||||
|
||||
def get_coverage_domainset(self):
|
||||
"""
|
||||
Provide coverage domainset
|
||||
|
||||
:returns: CIS JSON object of domainset metadata
|
||||
"""
|
||||
|
||||
domainset = {
|
||||
'type': 'DomainSetType',
|
||||
'generalGrid': {
|
||||
'type': 'GeneralGridCoverageType',
|
||||
'srsName': self._coverage_properties['bbox_crs'],
|
||||
'axisLabels': [
|
||||
self._coverage_properties['x_axis_label'],
|
||||
self._coverage_properties['y_axis_label']
|
||||
],
|
||||
'axis': [{
|
||||
'type': 'RegularAxisType',
|
||||
'axisLabel': self._coverage_properties['x_axis_label'],
|
||||
'lowerBound': self._coverage_properties['bbox'][0],
|
||||
'upperBound': self._coverage_properties['bbox'][2],
|
||||
'uomLabel': self._coverage_properties['bbox_units'],
|
||||
'resolution': self._coverage_properties['resx']
|
||||
}, {
|
||||
'type': 'RegularAxisType',
|
||||
'axisLabel': self._coverage_properties['y_axis_label'],
|
||||
'lowerBound': self._coverage_properties['bbox'][1],
|
||||
'upperBound': self._coverage_properties['bbox'][3],
|
||||
'uomLabel': self._coverage_properties['bbox_units'],
|
||||
'resolution': self._coverage_properties['resy']
|
||||
}],
|
||||
'gridLimits': {
|
||||
'type': 'GridLimitsType',
|
||||
'srsName': 'http://www.opengis.net/def/crs/OGC/0/Index2D',
|
||||
'axisLabels': ['i', 'j'],
|
||||
'axis': [{
|
||||
'type': 'IndexAxisType',
|
||||
'axisLabel': 'i',
|
||||
'lowerBound': 0,
|
||||
'upperBound': self._coverage_properties['width']
|
||||
}, {
|
||||
'type': 'IndexAxisType',
|
||||
'axisLabel': 'j',
|
||||
'lowerBound': 0,
|
||||
'upperBound': self._coverage_properties['height']
|
||||
}]
|
||||
}
|
||||
},
|
||||
'_meta': {
|
||||
'tags': self._coverage_properties['tags']
|
||||
}
|
||||
}
|
||||
|
||||
return domainset
|
||||
|
||||
def get_coverage_rangetype(self):
|
||||
"""
|
||||
Provide coverage rangetype
|
||||
|
||||
:returns: CIS JSON object of rangetype metadata
|
||||
"""
|
||||
|
||||
rangetype = {
|
||||
'type': 'DataRecordType',
|
||||
'field': []
|
||||
}
|
||||
|
||||
for i, dtype, nodataval in zip(self._data.indexes, self._data.dtypes,
|
||||
self._data.nodatavals):
|
||||
LOGGER.debug('Determing rangetype for band {}'.format(i))
|
||||
|
||||
name, units = None, None
|
||||
if self._data.units[i-1] is None:
|
||||
parameter = _get_parameter_metadata(
|
||||
self._data.profile['driver'], self._data.tags(i))
|
||||
name = parameter['description']
|
||||
units = parameter['unit_label']
|
||||
|
||||
rangetype['field'].append({
|
||||
'id': i,
|
||||
'type': 'QuantityType',
|
||||
'name': name,
|
||||
'definition': dtype,
|
||||
'nodata': nodataval,
|
||||
'uom': {
|
||||
'id': 'http://www.opengis.net/def/uom/UCUM/{}'.format(
|
||||
units),
|
||||
'type': 'UnitReference',
|
||||
'code': units
|
||||
},
|
||||
'_meta': {
|
||||
'tags': self._data.tags(i)
|
||||
}
|
||||
})
|
||||
|
||||
return rangetype
|
||||
|
||||
def query(self, bands=[], subsets={}, format_='json'):
|
||||
"""
|
||||
Extract data from collection collection
|
||||
|
||||
:param bands: list of bands (int)
|
||||
:param subsets: dict of subset names with lists of ranges
|
||||
|
||||
:returns: coverage data as dict of CoverageJSON or native format
|
||||
"""
|
||||
|
||||
LOGGER.debug('Bands: {}, subsets: {}'.format(bands, subsets))
|
||||
|
||||
args = {
|
||||
'indexes': None
|
||||
}
|
||||
shapes = []
|
||||
|
||||
if not bands and not subsets and format_ != 'json':
|
||||
LOGGER.debug('No parameters specified, returning native file')
|
||||
with io.open(self.data, 'rb') as fh:
|
||||
return fh.read()
|
||||
|
||||
if (self._coverage_properties['x_axis_label'] in subsets and
|
||||
self._coverage_properties['y_axis_label'] in subsets):
|
||||
LOGGER.debug('Creating spatial subset')
|
||||
|
||||
x = self._coverage_properties['x_axis_label']
|
||||
y = self._coverage_properties['y_axis_label']
|
||||
|
||||
shapes = [{
|
||||
'type': 'Polygon',
|
||||
'coordinates': [[
|
||||
[subsets[x][0], subsets[y][0]],
|
||||
[subsets[x][0], subsets[y][1]],
|
||||
[subsets[x][1], subsets[y][1]],
|
||||
[subsets[x][1], subsets[y][0]],
|
||||
[subsets[x][0], subsets[y][0]]
|
||||
]]
|
||||
}]
|
||||
|
||||
if bands:
|
||||
LOGGER.debug('Selecting bands')
|
||||
args['indexes'] = list(map(int, bands))
|
||||
|
||||
with rasterio.open(self.data) as _data:
|
||||
LOGGER.debug('Creating output coverage metadata')
|
||||
out_meta = _data.meta
|
||||
|
||||
if self.options is not None:
|
||||
LOGGER.debug('Adding dataset options')
|
||||
for key, value in self.options.items():
|
||||
out_meta[key] = value
|
||||
|
||||
if shapes: # spatial subset
|
||||
LOGGER.debug('Clipping data with bbox')
|
||||
out_image, out_transform = rasterio.mask.mask(
|
||||
_data,
|
||||
filled=False,
|
||||
shapes=shapes,
|
||||
crop=True,
|
||||
indexes=args['indexes'])
|
||||
|
||||
out_meta.update({"driver": "GRIB",
|
||||
"height": out_image.shape[1],
|
||||
"width": out_image.shape[2],
|
||||
"transform": out_transform})
|
||||
else: # no spatial subset
|
||||
LOGGER.debug('Creating data in memory with band selection')
|
||||
out_image = _data.read(indexes=args['indexes'])
|
||||
|
||||
if shapes:
|
||||
out_meta['bbox'] = [
|
||||
subsets[x][0], subsets[y][0],
|
||||
subsets[x][1], subsets[y][1]
|
||||
]
|
||||
else:
|
||||
out_meta['bbox'] = [
|
||||
_data.bounds.left,
|
||||
_data.bounds.bottom,
|
||||
_data.bounds.right,
|
||||
_data.bounds.top
|
||||
]
|
||||
|
||||
out_meta['units'] = _data.units
|
||||
|
||||
LOGGER.debug('Serializing data in memory')
|
||||
with MemoryFile() as memfile:
|
||||
with memfile.open(**out_meta) as dest:
|
||||
dest.write(out_image)
|
||||
|
||||
if format_ == 'json':
|
||||
LOGGER.debug('Creating output in CoverageJSON')
|
||||
out_meta['bands'] = args['indexes']
|
||||
return self.gen_covjson(out_meta, out_image)
|
||||
|
||||
else: # return data in native format
|
||||
LOGGER.debug('Returning data in native format')
|
||||
return memfile.read()
|
||||
|
||||
def gen_covjson(self, metadata, data):
|
||||
"""
|
||||
Generate coverage as CoverageJSON representation
|
||||
|
||||
:param metadata: coverage metadata
|
||||
:param data: rasterio DatasetReader object
|
||||
|
||||
:returns: dict of CoverageJSON representation
|
||||
"""
|
||||
|
||||
LOGGER.debug('Creating CoverageJSON domain')
|
||||
minx, miny, maxx, maxy = metadata['bbox']
|
||||
|
||||
cj = {
|
||||
'type': 'Coverage',
|
||||
'domain': {
|
||||
'type': 'Domain',
|
||||
'domainType': 'Grid',
|
||||
'axes': {
|
||||
'x': {
|
||||
'start': minx,
|
||||
'stop': maxx,
|
||||
'num': metadata['width']
|
||||
},
|
||||
'y': {
|
||||
'start': maxy,
|
||||
'stop': miny,
|
||||
'num': metadata['height']
|
||||
}
|
||||
},
|
||||
'referencing': [{
|
||||
'coordinates': ['x', 'y'],
|
||||
'system': {
|
||||
'type': self._coverage_properties['crs_type'],
|
||||
'id': self._coverage_properties['bbox_crs']
|
||||
}
|
||||
}]
|
||||
},
|
||||
'parameters': {},
|
||||
'ranges': {}
|
||||
}
|
||||
|
||||
if metadata['bands'] is None: # all bands
|
||||
bands_select = range(1, len(self._data.dtypes) + 1)
|
||||
else:
|
||||
bands_select = metadata['bands']
|
||||
|
||||
LOGGER.debug('bands selected: {}'.format(bands_select))
|
||||
for bs in bands_select:
|
||||
pm = _get_parameter_metadata(
|
||||
self._data.profile['driver'], self._data.tags(bs))
|
||||
|
||||
parameter = {
|
||||
'type': 'Parameter',
|
||||
'description': pm['description'],
|
||||
'unit': {
|
||||
'symbol': pm['unit_label']
|
||||
},
|
||||
'observedProperty': {
|
||||
'id': pm['observed_property_id'],
|
||||
'label': {
|
||||
'en': pm['observed_property_name']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cj['parameters'][pm['id']] = parameter
|
||||
|
||||
try:
|
||||
for key in cj['parameters'].keys():
|
||||
cj['ranges'][key] = {
|
||||
'type': 'NdArray',
|
||||
# 'dataType': metadata.dtypes[0],
|
||||
'dataType': 'float',
|
||||
'axisNames': ['y', 'x'],
|
||||
'shape': [metadata['height'], metadata['width']],
|
||||
}
|
||||
# TODO: deal with multi-band value output
|
||||
cj['ranges'][key]['values'] = data.flatten().tolist()
|
||||
except IndexError as err:
|
||||
LOGGER.warning(err)
|
||||
raise ProviderQueryError('Invalid query parameter')
|
||||
|
||||
return cj
|
||||
|
||||
def _get_coverage_properties(self):
|
||||
"""
|
||||
Helper function to normalize coverage properties
|
||||
|
||||
:returns: `dict` of coverage properties
|
||||
"""
|
||||
|
||||
properties = {
|
||||
'bbox': [
|
||||
self._data.bounds.left,
|
||||
self._data.bounds.bottom,
|
||||
self._data.bounds.right,
|
||||
self._data.bounds.top
|
||||
],
|
||||
'bbox_crs': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
|
||||
'crs_type': 'GeographicCRS',
|
||||
'bbox_units': 'deg',
|
||||
'x_axis_label': 'Long',
|
||||
'y_axis_label': 'Lat',
|
||||
'width': self._data.width,
|
||||
'height': self._data.height,
|
||||
'resx': self._data.res[0],
|
||||
'resy': self._data.res[1],
|
||||
'num_bands': self._data.count,
|
||||
'tags': self._data.tags()
|
||||
}
|
||||
|
||||
if self._data.crs is not None:
|
||||
if self._data.crs.is_projected:
|
||||
properties['bbox_crs'] = '{}/{}'.format(
|
||||
'http://www.opengis.net/def/crs/OGC/1.3/',
|
||||
self._data.crs.to_epsg())
|
||||
|
||||
properties['x_axis_label'] = 'x'
|
||||
properties['y_axis_label'] = 'y'
|
||||
properties['bbox_units'] = self._data.crs.linear_units
|
||||
properties['crs_type'] = 'ProjectedCRS'
|
||||
|
||||
properties['axes'] = [
|
||||
properties['x_axis_label'], properties['y_axis_label']
|
||||
]
|
||||
|
||||
return properties
|
||||
|
||||
|
||||
def _get_parameter_metadata(driver, band):
|
||||
"""
|
||||
Helper function to derive parameter name and units
|
||||
|
||||
:param driver: rasterio/GDAL driver name
|
||||
:param band: int of band number
|
||||
|
||||
:returns: dict of parameter metadata
|
||||
"""
|
||||
|
||||
parameter = {
|
||||
'id': None,
|
||||
'description': None,
|
||||
'unit_label': None,
|
||||
'unit_symbol': None,
|
||||
'observed_property_id': None,
|
||||
'observed_property_name': None
|
||||
}
|
||||
|
||||
if driver == 'GRIB':
|
||||
parameter['id'] = band['GRIB_ELEMENT']
|
||||
parameter['description'] = band['GRIB_COMMENT']
|
||||
parameter['unit_label'] = band['GRIB_UNIT']
|
||||
parameter['unit_symbol'] = band['GRIB_UNIT']
|
||||
parameter['observed_property_id'] = band['GRIB_SHORT_NAME']
|
||||
parameter['observed_property_name'] = band['GRIB_COMMENT']
|
||||
|
||||
return parameter
|
||||
@@ -212,6 +212,69 @@ async def collection_items(request: Request, collection_id=None, item_id=None):
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/collections/<collection_id>/coverage')
|
||||
def collection_coverage(request: Request, collection_id):
|
||||
"""
|
||||
OGC API - Coverages coverage endpoint
|
||||
|
||||
:param collection_id: collection identifier
|
||||
|
||||
:returns: Starlette HTTP Response
|
||||
"""
|
||||
|
||||
headers, status_code, content = api_.get_collection_coverage(
|
||||
request.headers, request.query_params, collection_id)
|
||||
|
||||
response = Response(content=content, status_code=status_code)
|
||||
|
||||
if headers:
|
||||
response.headers.update(headers)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/collections/<collection_id>/coverage/domainset')
|
||||
def collection_coverage_domainset(request: Request, collection_id):
|
||||
"""
|
||||
OGC API - Coverages coverage domainset endpoint
|
||||
|
||||
:param collection_id: collection identifier
|
||||
|
||||
:returns: Starlette HTTP Response
|
||||
"""
|
||||
|
||||
headers, status_code, content = api_.get_collection_coverage_domainset(
|
||||
request.headers, request.query_params, collection_id)
|
||||
|
||||
response = Response(content=content, status_code=status_code)
|
||||
|
||||
if headers:
|
||||
response.headers.update(headers)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/collections/<collection_id>/coverage/rangetype')
|
||||
def collection_coverage_rangetype(request: Request, collection_id):
|
||||
"""
|
||||
OGC API - Coverages coverage rangetype endpoint
|
||||
|
||||
:param collection_id: collection identifier
|
||||
|
||||
:returns: Starlette HTTP Response
|
||||
"""
|
||||
|
||||
headers, status_code, content = api_.get_collection_coverage_rangetype(
|
||||
request.headers, request.query_params, collection_id)
|
||||
|
||||
response = Response(content=content, status_code=status_code)
|
||||
|
||||
if headers:
|
||||
response.headers.update(headers)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/processes')
|
||||
@app.route('/processes/')
|
||||
@app.route('/processes/{process_id}')
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div>
|
||||
<meta itemprop="encodingFormat" content="text/html" />
|
||||
<a title="Display Queryables" itemprop="contentURL" href="{{ config['server']['url'] }}/collections/{{ data['id'] }}/queryables">
|
||||
Display Queryables of "{{ data['title'] }}</a>"</div>
|
||||
Display Queryables of "{{ data['title'] }}"</a></div>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>View</h3>
|
||||
@@ -35,7 +35,7 @@
|
||||
<div itemprop="distribution" itemscope itemtype="https://schema.org/DataDownload">
|
||||
<meta itemprop="encodingFormat" content="text/html" />
|
||||
<a title="Browse Items" itemprop="contentURL" href="{{ config['server']['url'] }}/collections/{{ data['id'] }}/items">
|
||||
Browse through the items of "{{ data['title'] }}</a>"</div>
|
||||
Browse through the items of "{{ data['title'] }}"</a></div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ super() }} {{ data['title'] }} {% endblock %}
|
||||
{% block crumbs %}{{ super() }}
|
||||
/ <a href="../../">Collections</a>
|
||||
/ <a href="../../../collections/{{ data['id'] }}">{{ data['title'] }}</a>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<section id="collection" itemscope itemtype="https://schema.org/Dataset">
|
||||
<span itemprop="includedInDataCatalog" itemscope itemtype="https://schema.org/DataCatalog">
|
||||
<meta itemprop="url" content="{{ config['server']['url'] }}/collections" />
|
||||
<meta itemprop="name" content="{{ config['metadata']['identification']['title'] | striptags }}" />
|
||||
<meta itemprop="description" content="{{ config['metadata']['identification']['description'] | striptags }}" />
|
||||
</span>
|
||||
<h1 itemprop="name">{{ data['title'] }}</h1>
|
||||
<meta itemprop="url" content="{{ config['server']['url'] }}" />
|
||||
<p itemprop="description">{{ data['description'] }}</p>
|
||||
<h3>Coverage domain set</h3>
|
||||
<h4>Axis labels</h4>
|
||||
<ul>
|
||||
{% for al in data['generalGrid']['axisLabels'] %}
|
||||
<li>{{ al }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h4>Extent</h4>
|
||||
<ul>
|
||||
<li>minx: {{ data['generalGrid']['axis'][0]['lowerBound'] }}</li>
|
||||
<li>miny: {{ data['generalGrid']['axis'][1]['lowerBound'] }}</li>
|
||||
<li>maxx: {{ data['generalGrid']['axis'][0]['upperBound'] }}</li>
|
||||
<li>maxy: {{ data['generalGrid']['axis'][1]['upperBound'] }}</li>
|
||||
<li>Coordinate reference system: {{ data['generalGrid']['srsName'] }}</li>
|
||||
</ul>
|
||||
<h4>Size</h4>
|
||||
<ul>
|
||||
<li>width: {{ data['generalGrid']['gridLimits']['axis'][0]['upperBound'] }} </li>
|
||||
<li>height: {{ data['generalGrid']['gridLimits']['axis'][1]['upperBound'] }} </li>
|
||||
</ul>
|
||||
<h4>Resolution</h4>
|
||||
<ul>
|
||||
<li>x: {{ data['generalGrid']['axis'][0]['resolution'] }} </li>
|
||||
<li>y: {{ data['generalGrid']['axis'][1]['resolution'] }} </li>
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ super() }} {{ data['title'] }} {% endblock %}
|
||||
{% block crumbs %}{{ super() }}
|
||||
/ <a href="../../">Collections</a>
|
||||
/ <a href="../../../collections/{{ data['id'] }}">{{ data['title'] }}</a>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<section id="collection" itemscope itemtype="https://schema.org/Dataset">
|
||||
<span itemprop="includedInDataCatalog" itemscope itemtype="https://schema.org/DataCatalog">
|
||||
<meta itemprop="url" content="{{ config['server']['url'] }}/collections" />
|
||||
<meta itemprop="name" content="{{ config['metadata']['identification']['title'] | striptags }}" />
|
||||
<meta itemprop="description" content="{{ config['metadata']['identification']['description'] | striptags }}" />
|
||||
</span>
|
||||
<h1 itemprop="name">{{ data['title'] }}</h1>
|
||||
<meta itemprop="url" content="{{ config['server']['url'] }}" />
|
||||
<p itemprop="description">{{ data['description'] }}</p>
|
||||
<h3>Coverage range type</h3>
|
||||
<h4>Fields</h4>
|
||||
<ul>
|
||||
{% for field in data['field'] %}
|
||||
<li>{{ field['id'] }}: {{ field['name'] }} ({{ field['definition'] }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
+11
-4
@@ -43,6 +43,7 @@ from jinja2 import Environment, FileSystemLoader
|
||||
import yaml
|
||||
|
||||
from pygeoapi import __version__
|
||||
from pygeoapi.provider.base import ProviderTypeError
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -142,16 +143,22 @@ def str2bool(value):
|
||||
return value2
|
||||
|
||||
|
||||
def to_json(dict_):
|
||||
def to_json(dict_, pretty=False):
|
||||
"""
|
||||
Serialize dict to json
|
||||
|
||||
:param dict_: `dict` of JSON representation
|
||||
:param pretty: `bool` of whether to prettify JSON (default is `False`)
|
||||
|
||||
:returns: JSON string representation
|
||||
"""
|
||||
|
||||
return json.dumps(dict_, default=json_serial)
|
||||
if pretty:
|
||||
indent = 4
|
||||
else:
|
||||
indent = None
|
||||
|
||||
return json.dumps(dict_, default=json_serial, indent=indent)
|
||||
|
||||
|
||||
def get_path_basename(urlpath):
|
||||
@@ -302,8 +309,8 @@ def get_provider_by_type(providers, provider_type):
|
||||
try:
|
||||
p = (next(d for i, d in enumerate(providers)
|
||||
if d['type'] == provider_type))
|
||||
except StopIteration:
|
||||
raise RuntimeError('Cannot find provider type')
|
||||
except (RuntimeError, StopIteration):
|
||||
raise ProviderTypeError('Invalid provider type requested')
|
||||
|
||||
return p
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ Flask
|
||||
python-dateutil
|
||||
pytz
|
||||
PyYAML
|
||||
rasterio
|
||||
unicodecsv
|
||||
|
||||
Binary file not shown.
@@ -7,23 +7,23 @@ This directory provides test data to demonstrate functionality.
|
||||
### `ne_110m_lakes.geojson`
|
||||
|
||||
- source: Natural Earth Lakes + Reservoirs
|
||||
- URL: [http://www.naturalearthdata.com/downloads/110m-physical-vectors/110mlakes-reservoirs/](http://www.naturalearthdata.com/downloads/110m-physical-vectors/110mlakes-reservoirs/)
|
||||
- URL: [https:/naturalearthdata.com/downloads/110m-physical-vectors/110mlakes-reservoirs/](https://naturalearthdata.com/downloads/110m-physical-vectors/110mlakes-reservoirs/)
|
||||
- Shapefile converted to GeoJSON
|
||||
- Made with Natural Earth. Free vector and raster map data @ [naturalearthdata.com](http://naturalearthdata.com)
|
||||
- Made with Natural Earth. Free vector and raster map data @ [naturalearthdata.com](https://naturalearthdata.com)
|
||||
|
||||
### `ne_110m_admin_0_countries.sqlite`
|
||||
|
||||
- source: Natural Earth Admin 0 - Countries
|
||||
- URL: [http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/](http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/)
|
||||
- URL: [https://naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/](https://naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/)
|
||||
- Shapefile converted to SQLite
|
||||
- Made with Natural Earth. Free vector and raster map data @ [naturalearthdata.com](http://naturalearthdata.com)
|
||||
- Made with Natural Earth. Free vector and raster map data @ [naturalearthdata.com](https://naturalearthdata.com)
|
||||
|
||||
### `ne_110m_populated_places_simple.geojson`
|
||||
|
||||
- source: Natural Earth Populated Places
|
||||
- URL: [http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/](http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/)
|
||||
- URL: [https://naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/](https://naturalearthdata.com/downloads/110m-cultural-vectors/110m-populated-places/)
|
||||
- Shapefile converted to GeoJSON
|
||||
- Made with Natural Earth. Free vector and raster map data @ [naturalearthdata.com](http://naturalearthdata.com)
|
||||
- Made with Natural Earth. Free vector and raster map data @ [naturalearthdata.com](https://naturalearthdata.com)
|
||||
|
||||
### `obs.csv`
|
||||
|
||||
@@ -35,8 +35,8 @@ This directory provides test data to demonstrate functionality.
|
||||
### `poi_portugal.gpkg`
|
||||
|
||||
- source: OpenStreetMap - Natural GIS
|
||||
- URL: [http://www.naturalgis.pt/cgi-bin/opendata/mapserv?service=WFS&request=GetCapabilities](http://www.naturalgis.pt/cgi-bin/opendata/mapserv?service=WFS&request=GetCapabilities)
|
||||
- Data obtained from WFS instance of NaturalGIS company (http://www.naturalgis.pt/en/) and converted to geopackage
|
||||
- URL: [https://naturalgis.pt/cgi-bin/opendata/mapserv?service=WFS&request=GetCapabilities](https://www.naturalgis.pt/cgi-bin/opendata/mapserv?service=WFS&request=GetCapabilities)
|
||||
- Data obtained from WFS instance of NaturalGIS company (https://naturalgis.pt/en/) and converted to geopackage
|
||||
- Upstream data from OpenStreetMap extract for Portugal
|
||||
|
||||
### `hotosm_bdi_waterways.sql.gz`
|
||||
@@ -51,4 +51,4 @@ This directory provides test data to demonstrate functionality.
|
||||
|
||||
- source: [Meteorological Service of Canada Datamrt](https://eccc-msc.github.io/open-data/msc-datamart/readme_en)
|
||||
- URL: https://dd.weather.gc.ca/model_gem_global/15km/grib2/lat_lon/00/000
|
||||
- License: https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
- License: https://eccc-msc.github.io/open-data/licence/readme_en
|
||||
|
||||
@@ -127,6 +127,33 @@ resources:
|
||||
x_field: long
|
||||
y_field: lat
|
||||
|
||||
gdps-temperature:
|
||||
type: collection
|
||||
title: Global Deterministic Prediction System sample
|
||||
description: Global Deterministic Prediction System sample
|
||||
keywords:
|
||||
- gdps
|
||||
- global
|
||||
extents:
|
||||
spatial:
|
||||
bbox: [-180,-90,180,90]
|
||||
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
|
||||
links:
|
||||
- type: text/html
|
||||
rel: canonical
|
||||
title: information
|
||||
href: https://eccc-msc.github.io/open-data/msc-data/nwp_gdps/readme_gdps_en
|
||||
hreflang: en-CA
|
||||
providers:
|
||||
- type: coverage
|
||||
name: rasterio
|
||||
data: tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2
|
||||
options:
|
||||
DATA_ENCODING: COMPLEX_PACKING
|
||||
format:
|
||||
name: GRIB2
|
||||
mimetype: application/x-grib2
|
||||
|
||||
hello-world:
|
||||
type: process
|
||||
processor:
|
||||
|
||||
+106
-4
@@ -31,13 +31,14 @@ import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
from pyld import jsonld
|
||||
import pytest
|
||||
|
||||
from werkzeug.test import create_environ
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from pygeoapi.api import API, check_format
|
||||
from pygeoapi.util import yaml_load
|
||||
from pyld import jsonld
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -169,7 +170,7 @@ def test_conformance(config, api_):
|
||||
|
||||
assert isinstance(root, dict)
|
||||
assert 'conformsTo' in root
|
||||
assert len(root['conformsTo']) == 4
|
||||
assert len(root['conformsTo']) == 7
|
||||
|
||||
rsp_headers, code, response = api_.conformance(req_headers, {'f': 'foo'})
|
||||
assert code == 400
|
||||
@@ -193,7 +194,7 @@ def test_describe_collections(config, api_):
|
||||
collections = json.loads(response)
|
||||
|
||||
assert len(collections) == 2
|
||||
assert len(collections['collections']) == 1
|
||||
assert len(collections['collections']) == 2
|
||||
assert len(collections['links']) == 3
|
||||
|
||||
rsp_headers, code, response = api_.describe_collections(
|
||||
@@ -227,6 +228,13 @@ def test_describe_collections(config, api_):
|
||||
req_headers, {'f': 'html'}, 'obs')
|
||||
assert rsp_headers['Content-Type'] == 'text/html'
|
||||
|
||||
rsp_headers, code, response = api_.describe_collections(
|
||||
req_headers, {}, 'gdps-temperature')
|
||||
collection = json.loads(response)
|
||||
|
||||
assert collection['id'] == 'gdps-temperature'
|
||||
assert len(collection['links']) == 12
|
||||
|
||||
|
||||
def test_get_collection_queryables(config, api_):
|
||||
req_headers = make_req_headers()
|
||||
@@ -533,6 +541,11 @@ def test_get_collection_item(config, api_):
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_item(
|
||||
req_headers, {'f': 'json'}, 'gdps-temperature', '371')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_item(
|
||||
req_headers, {}, 'foo', '371')
|
||||
|
||||
@@ -580,6 +593,95 @@ def test_get_collection_item_json_ld(config, api_):
|
||||
'https://schema.org/identifier'][0]['@value'] == '35'
|
||||
|
||||
|
||||
def test_get_coverage_domainset(config, api_):
|
||||
req_headers = make_req_headers()
|
||||
rsp_headers, code, response = api_.get_collection_coverage_domainset(
|
||||
req_headers, {}, 'obs')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage_domainset(
|
||||
req_headers, {}, 'gdps-temperature')
|
||||
|
||||
domainset = json.loads(response)
|
||||
|
||||
assert domainset['type'] == 'DomainSetType'
|
||||
assert domainset['generalGrid']['axisLabels'] == ['Long', 'Lat']
|
||||
assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j']
|
||||
assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 2400 # noqa
|
||||
assert domainset['generalGrid']['gridLimits']['axis'][1]['upperBound'] == 1201 # noqa
|
||||
|
||||
|
||||
def test_get_collection_coverage_rangetype(config, api_):
|
||||
req_headers = make_req_headers()
|
||||
rsp_headers, code, response = api_.get_collection_coverage_rangetype(
|
||||
req_headers, {}, 'obs')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage_rangetype(
|
||||
req_headers, {}, 'gdps-temperature')
|
||||
|
||||
rangetype = json.loads(response)
|
||||
|
||||
assert rangetype['type'] == 'DataRecordType'
|
||||
assert len(rangetype['field']) == 1
|
||||
assert rangetype['field'][0]['id'] == 1
|
||||
assert rangetype['field'][0]['name'] == 'Temperature [C]'
|
||||
assert rangetype['field'][0]['uom']['code'] == '[C]'
|
||||
|
||||
|
||||
def test_get_collection_coverage(config, api_):
|
||||
req_headers = make_req_headers()
|
||||
rsp_headers, code, response = api_.get_collection_coverage(
|
||||
req_headers, {}, 'obs')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage(
|
||||
req_headers, {'rangeSubset': '12'}, 'gdps-temperature')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage(
|
||||
req_headers,
|
||||
ImmutableMultiDict([('subset', 'bad_axis(10,20)')]),
|
||||
'gdps-temperature')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage(
|
||||
req_headers, {'f': 'blah'}, 'gdps-temperature')
|
||||
|
||||
assert code == 400
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage(
|
||||
req_headers,
|
||||
ImmutableMultiDict([
|
||||
('subset', 'Lat(5,10)'), ('subset', 'Long(5,10)')]),
|
||||
'gdps-temperature')
|
||||
|
||||
assert code == 200
|
||||
content = json.loads(response)
|
||||
|
||||
assert content['domain']['axes']['x']['num'] == 35
|
||||
assert content['domain']['axes']['y']['num'] == 35
|
||||
assert 'TMP' in content['parameters']
|
||||
assert 'TMP' in content['ranges']
|
||||
assert content['ranges']['TMP']['axisNames'] == ['y', 'x']
|
||||
|
||||
rsp_headers, code, response = api_.get_collection_coverage(
|
||||
req_headers,
|
||||
ImmutableMultiDict([
|
||||
('subset', 'Lat(5,10)'), ('subset', 'Long(5,10)'),
|
||||
('f', 'GRIB2')
|
||||
]),
|
||||
'gdps-temperature')
|
||||
|
||||
assert code == 200
|
||||
assert isinstance(response, bytes)
|
||||
|
||||
|
||||
def test_describe_processes(config, api_):
|
||||
req_headers = make_req_headers()
|
||||
rsp_headers, code, response = api_.describe_processes(
|
||||
|
||||
@@ -51,6 +51,7 @@ path = get_test_file_path('data/obs.csv')
|
||||
def config():
|
||||
return {
|
||||
'name': 'CSV',
|
||||
'type': 'feature',
|
||||
'data': path,
|
||||
'id_field': 'id',
|
||||
'geometry': {
|
||||
|
||||
@@ -37,6 +37,7 @@ from pygeoapi.provider.elasticsearch_ import ElasticsearchProvider
|
||||
def config():
|
||||
return {
|
||||
'name': 'Elasticsearch',
|
||||
'type': 'feature',
|
||||
'data': 'http://localhost:9200/ne_110m_populated_places_simple', # noqa
|
||||
'id_field': 'geonameid'
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ THISDIR = os.path.dirname(os.path.realpath(__file__))
|
||||
def config():
|
||||
return {
|
||||
'name': 'FileSystem',
|
||||
'type': 'stac',
|
||||
'data': os.path.join(THISDIR, 'data'),
|
||||
'file_types': ['.gpkg']
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ def fixture():
|
||||
def config():
|
||||
return {
|
||||
'name': 'GeoJSON',
|
||||
'type': 'feature',
|
||||
'data': path,
|
||||
'id_field': 'id'
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ mongocollection = 'testplaces'
|
||||
def config():
|
||||
return {
|
||||
'name': 'MongoDB',
|
||||
'type': 'feature',
|
||||
'data': monogourl,
|
||||
'collection': mongocollection
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def config_vsicurl_csv():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'CSV',
|
||||
'source': '/vsicurl/https://raw.githubusercontent.com/pcm-dpc/COVID-19/master/dati-regioni/dpc-covid19-ita-regioni.csv', # noqa
|
||||
|
||||
@@ -47,6 +47,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def config_ArcGIS_ESRIJSON():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'ESRIJSON',
|
||||
'source': 'https://sampleserver6.arcgisonline.com/arcgis/rest/services/CommunityAddressing/FeatureServer/0/query?where=objectid+%3D+objectid&outfields=*&orderByFields=objectid+ASC&f=json', # noqa
|
||||
|
||||
@@ -44,6 +44,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def config_poi_portugal():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'GPKG',
|
||||
'source': './tests/data/poi_portugal.gpkg',
|
||||
@@ -97,6 +98,7 @@ def test_get_not_existing_feature_raise_exception(
|
||||
def config_gpkg_4326():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'GPKG',
|
||||
'source':
|
||||
@@ -117,6 +119,7 @@ def config_gpkg_4326():
|
||||
def config_gpkg_28992():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'GPKG',
|
||||
'source':
|
||||
|
||||
@@ -47,6 +47,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def config_shapefile_4326():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'ESRI Shapefile',
|
||||
'source':
|
||||
@@ -65,6 +66,7 @@ def config_shapefile_4326():
|
||||
def config_shapefile_28992():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'ESRI Shapefile',
|
||||
'source':
|
||||
|
||||
@@ -46,6 +46,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def config_sqlite_4326():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'SQLite',
|
||||
'source':
|
||||
|
||||
@@ -47,6 +47,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def config_MapServer_WFS():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'WFS',
|
||||
'source': 'WFS:http://geodata.nationaalgeoregister.nl/rdinfo/wfs?',
|
||||
@@ -75,6 +76,7 @@ def config_MapServer_WFS():
|
||||
def config_GeoServer_WFS():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'WFS',
|
||||
'source':
|
||||
@@ -105,6 +107,7 @@ def config_GeoServer_WFS():
|
||||
def config_geosol_gs_WFS():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'WFS',
|
||||
'source':
|
||||
@@ -134,6 +137,7 @@ def config_geosol_gs_WFS():
|
||||
def config_geonode_gs_WFS():
|
||||
return {
|
||||
'name': 'OGR',
|
||||
'type': 'feature',
|
||||
'data': {
|
||||
'source_type': 'WFS',
|
||||
'source':
|
||||
|
||||
@@ -41,6 +41,7 @@ from pygeoapi.provider.postgresql import PostgreSQLProvider
|
||||
def config():
|
||||
return {
|
||||
'name': 'PostgreSQL',
|
||||
'type': 'feature',
|
||||
'data': {'host': '127.0.0.1',
|
||||
'dbname': 'test',
|
||||
'user': 'postgres',
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# =================================================================
|
||||
#
|
||||
# Authors: Tom Kralidis <tomkralidis@gmail.com>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# =================================================================
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from pygeoapi.provider.rasterio_ import RasterioProvider
|
||||
|
||||
|
||||
def get_test_file_path(filename):
|
||||
"""helper function to open test file safely"""
|
||||
|
||||
if os.path.isfile(filename):
|
||||
return filename
|
||||
else:
|
||||
return 'tests/{}'.format(filename)
|
||||
|
||||
|
||||
path = get_test_file_path(
|
||||
'tests/data/CMC_glb_TMP_TGL_2_latlon.15x.15_2020081000_P000.grib2')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def config():
|
||||
return {
|
||||
'name': 'rasterio',
|
||||
'type': 'coverage',
|
||||
'data': path,
|
||||
'options': {
|
||||
'DATA_ENCODING': 'COMPLEX_PACKING'
|
||||
},
|
||||
'format': {
|
||||
'name': 'GRIB2',
|
||||
'mimetype': 'application/x-grib2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_provider(config):
|
||||
p = RasterioProvider(config)
|
||||
|
||||
assert p.num_bands == 1
|
||||
assert len(p.axes) == 2
|
||||
assert p.axes == ['Long', 'Lat']
|
||||
|
||||
|
||||
def test_domainset(config):
|
||||
p = RasterioProvider(config)
|
||||
domainset = p.get_coverage_domainset()
|
||||
|
||||
assert isinstance(domainset, dict)
|
||||
assert domainset['generalGrid']['axisLabels'] == ['Long', 'Lat']
|
||||
assert domainset['generalGrid']['gridLimits']['axisLabels'] == ['i', 'j']
|
||||
assert domainset['generalGrid']['gridLimits']['axis'][0]['upperBound'] == 2400 # noqa
|
||||
assert domainset['generalGrid']['gridLimits']['axis'][1]['upperBound'] == 1201 # noqa
|
||||
|
||||
|
||||
def test_rangetype(config):
|
||||
p = RasterioProvider(config)
|
||||
rangetype = p.get_coverage_rangetype()
|
||||
|
||||
assert isinstance(rangetype, dict)
|
||||
assert len(rangetype['field']) == 1
|
||||
assert rangetype['field'][0]['name'] == 'Temperature [C]'
|
||||
|
||||
|
||||
def test_query(config):
|
||||
p = RasterioProvider(config)
|
||||
|
||||
data = p.query()
|
||||
assert isinstance(data, dict)
|
||||
|
||||
data = p.query(format_='GRIB2')
|
||||
assert isinstance(data, bytes)
|
||||
@@ -43,6 +43,7 @@ from pygeoapi.provider.sqlite import SQLiteGPKGProvider
|
||||
def config_sqlite():
|
||||
return {
|
||||
'name': 'SQLiteGPKG',
|
||||
'type': 'feature',
|
||||
'data': './tests/data/ne_110m_admin_0_countries.sqlite',
|
||||
'id_field': 'ogc_fid',
|
||||
'table': 'ne_110m_admin_0_countries'
|
||||
@@ -53,6 +54,7 @@ def config_sqlite():
|
||||
def config_geopackage():
|
||||
return {
|
||||
'name': 'SQLiteGPKG',
|
||||
'type': 'feature',
|
||||
'data': './tests/data/poi_portugal.gpkg',
|
||||
'id_field': 'osm_id',
|
||||
'table': 'poi_portugal'
|
||||
|
||||
+3
-2
@@ -34,6 +34,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from pygeoapi import util
|
||||
from pygeoapi.provider.base import ProviderTypeError
|
||||
|
||||
|
||||
def get_test_file_path(filename):
|
||||
@@ -123,7 +124,7 @@ def test_filter_dict_by_key_value():
|
||||
|
||||
collections = util.filter_dict_by_key_value(d['resources'],
|
||||
'type', 'collection')
|
||||
assert len(collections) == 1
|
||||
assert len(collections) == 2
|
||||
|
||||
notfound = util.filter_dict_by_key_value(d['resources'],
|
||||
'type', 'foo')
|
||||
@@ -142,7 +143,7 @@ def test_get_provider_by_type():
|
||||
assert p['type'] == 'feature'
|
||||
assert p['name'] == 'CSV'
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
with pytest.raises(ProviderTypeError):
|
||||
p = util.get_provider_by_type(d['resources']['obs']['providers'],
|
||||
'something-else')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user