Skip to content

Commit

Permalink
add support for OGC API - Features - Part 3: CQL
Browse files Browse the repository at this point in the history
  • Loading branch information
tomkralidis committed Oct 21, 2021
1 parent 8db1fbd commit 22975cd
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 16 deletions.
17 changes: 10 additions & 7 deletions docs/en/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ Standards Support
OGC API Support
---------------

+---------------------------------------------------+
| Standard | Version(s) |
+======================================+============+
| `OGC API - Features - Part 1: Core`_ | 1.0 |
+--------------------------------------+------------+
| `OGC API - Records - Part 1: Core`_ | draft |
+--------------------------------------+------------+
+--------------------------------------------------------------------------------------+------------+
| Standard | Version(s) |
+======================================================================================+============+
| `OGC API - Features - Part 1: Core`_ | 1.0 |
+--------------------------------------------------------------------------------------+------------+
| `OGC API - Records - Part 1: Core`_ | draft |
+--------------------------------------------------------------------------------------+------------+
| `OGC API - Features - Part 3: Filtering and the Common Query Language (CQL) draft`_ | draft |
+--------------------------------------------------------------------------------------+------------+

.. _`OGC WMS`: https://www.opengeospatial.org/standards/wms
.. _`OGC WFS`: https://www.opengeospatial.org/standards/wfs
Expand Down Expand Up @@ -83,3 +85,4 @@ OGC API Support
.. _`OGC API`: https://ogcapi.ogc.org
.. _`OGC API - Features - Part 1: Core`: https://docs.opengeospatial.org/is/17-069r3/17-069r3.html
.. _`OGC API - Records - Part 1: Core`: https://github.com/opengeospatial/ogcapi-records
.. _`OGC API - Features - Part 3: Filtering and the Common Query Language (CQL) draft`: https://docs.ogc.org/DRAFTS/19-079.html
6 changes: 6 additions & 0 deletions docs/en/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ OGC API - Records 1.0
2
>>> msc_wis_dcpc_query2['numberReturned']
2
>>> my_catalogue_cql_text_query = w.collection_items('my-catalogue', filter="title LIKE 'Roadrunner%'")
>>> my_catalogue_cql_text_query['features'][0]['properties']['title']
u'Roadrunner ambush locations'
>>> my_catalogue_cql_json_query = w.collection_items('my-catalogue', limit=1, cql={'eq': [{ 'property': 'title' }, 'Roadrunner ambush locations']})
>>> my_catalogue_cql_json_query['features'][0]['properties']['title']
u'Roadrunner ambush locations'


WCS
Expand Down
2 changes: 1 addition & 1 deletion owslib/catalogue/csw2.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ def _invoke(self):

self.request = util.element_to_string(self.request, encoding='utf-8')

self.response = http_post(request_url, self.request, self.lang, self.timeout, auth=self.auth)
self.response = http_post(request_url, self.request, self.lang, self.timeout, auth=self.auth).content

# parse result see if it's XML
self._exml = etree.parse(BytesIO(self.response))
Expand Down
2 changes: 1 addition & 1 deletion owslib/catalogue/csw3.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ def _invoke(self):

self.request = util.element_to_string(self.request, encoding='utf-8')

self.response = http_post(request_url, self.request, self.lang, self.timeout, auth=self.auth)
self.response = http_post(request_url, self.request, self.lang, self.timeout, auth=self.auth).content

# parse result see if it's XML
self._exml = etree.parse(BytesIO(self.response))
Expand Down
21 changes: 16 additions & 5 deletions owslib/ogcapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
# Contact email: tomkralidis@gmail.com
# =============================================================================

from copy import deepcopy
import json
import logging
from urllib.parse import urljoin
from urllib.parse import urlencode, urljoin

import requests
import yaml

from owslib import __version__
from owslib.util import Authentication, http_get
from owslib.util import Authentication, http_get, http_post

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -115,7 +116,7 @@ def conformance(self) -> dict:
path = 'conformance'
return self._request(path)

def _build_url(self, path: str = None) -> str:
def _build_url(self, path: str = None, params: dict = {}) -> str:
"""
helper function to build an OGC API URL
Expand All @@ -133,6 +134,9 @@ def _build_url(self, path: str = None) -> str:
else:
url = urljoin(url, path)

if params:
url = '?'.join([url, urlencode(params)])

LOGGER.debug('URL: {}'.format(url))

return url
Expand All @@ -158,8 +162,15 @@ def _request(self, path: str = None, as_dict: bool = True,
LOGGER.debug('Request: {}'.format(url))
LOGGER.debug('Params: {}'.format(kwargs))

response = http_get(url, headers=self.headers, auth=self.auth,
params=kwargs)
if 'cql' not in kwargs:
response = http_get(url, headers=self.headers, auth=self.auth,
params=kwargs)
else:
LOGGER.debug('CQL query detected')
kwargs2 = deepcopy(kwargs)
cql = kwargs2.pop('cql')
url2 = self._build_url(path, kwargs2)
response = http_post(url2, request=cql, auth=self.auth)

LOGGER.debug('URL: {}'.format(response.url))

Expand Down
4 changes: 4 additions & 0 deletions owslib/ogcapi/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ def collection_items(self, collection_id: str, **kwargs: dict) -> dict:
@param startindex: start position of results
@type q: string
@param q: full text search
@type filter: string
@param filter: CQL TEXT expression
@type cql: dict
@param cql: CQL JSON payload
@returns: feature results
"""
Expand Down
10 changes: 8 additions & 2 deletions owslib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,10 @@ def http_post(url=None, request=None, lang='en-US', timeout=10, username=None, p
'Host': u.netloc,
}

if isinstance(request, dict):
headers['Content-type'] = 'application/json'
headers.pop('Accept')

rkwargs = {}

if auth:
Expand All @@ -429,8 +433,10 @@ def http_post(url=None, request=None, lang='en-US', timeout=10, username=None, p
rkwargs['verify'] = auth.verify
rkwargs['cert'] = auth.cert

up = requests.post(url, request, headers=headers, **rkwargs)
return up.content
if not isinstance(request, dict):
return requests.post(url, request, headers=headers, **rkwargs)
else:
return requests.post(url, json=request, headers=headers, **rkwargs)


def http_get(*args, **kwargs):
Expand Down
69 changes: 69 additions & 0 deletions tests/test_ogcapi_records_pycsw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from tests.utils import service_ok

import pytest

from owslib.ogcapi.records import Records

SERVICE_URL = 'https://demo.pycsw.org/cite'


@pytest.mark.online
@pytest.mark.skipif(not service_ok(SERVICE_URL),
reason='service is unreachable')
def test_ogcapi_records_pygeoapi():
w = Records(SERVICE_URL)

assert w.url == 'https://demo.pycsw.org/cite/'
assert w.url_query_string is None

api = w.api()
assert api['components']['parameters'] is not None
paths = api['paths']
assert paths is not None
assert paths['/collections/metadata:main'] is not None

conformance = w.conformance()
assert len(conformance['conformsTo']) == 10

collections = w.collections()
assert len(collections) > 0

record_collections = w.records()
assert record_collections == ['metadata:main']

pycsw_cite_demo = w.collection('metadata:main')
assert pycsw_cite_demo['id'] == 'metadata:main'
assert pycsw_cite_demo['title'] == 'pycsw OGC CITE demo and Reference Implementation' # noqa
assert pycsw_cite_demo['itemType'] == 'record'
assert w.request == 'https://demo.pycsw.org/cite/collections/metadata:main' # noqa
assert w.response is not None
assert isinstance(w.response, dict)

pycsw_cite_demo_queryables = w.collection_queryables('metadata:main')
assert len(pycsw_cite_demo_queryables['properties'].keys()) == 60

# Minimum of limit param is 1
with pytest.raises(RuntimeError):
pycsw_cite_demo_query = w.collection_items('metadata:main', limit=0)

pycsw_cite_demo_query = w.collection_items('metadata:main', limit=1)
assert pycsw_cite_demo_query['numberMatched'] == 12
assert pycsw_cite_demo_query['numberReturned'] == 1
assert len(pycsw_cite_demo_query['features']) == 1

pycsw_cite_demo_query = w.collection_items('metadata:main', q='lorem')
assert pycsw_cite_demo_query['numberMatched'] == 5
assert pycsw_cite_demo_query['numberReturned'] == 5
assert len(pycsw_cite_demo_query['features']) == 5

cql_text = "title LIKE 'Lorem%'"
pycsw_cite_demo_query = w.collection_items('metadata:main', filter=cql_text)
assert pycsw_cite_demo_query['numberMatched'] == 2
assert pycsw_cite_demo_query['numberReturned'] == 2
assert len(pycsw_cite_demo_query['features']) == 2

cql_json = {'eq': [{'property': 'title'}, 'Lorem ipsum']}
pycsw_cite_demo_query = w.collection_items('metadata:main', cql=cql_json)
assert pycsw_cite_demo_query['numberMatched'] == 1
assert pycsw_cite_demo_query['numberReturned'] == 1
assert len(pycsw_cite_demo_query['features']) == 1

0 comments on commit 22975cd

Please sign in to comment.