[Git][debian-gis-team/owslib][master] 4 commits: New upstream version 0.20.0
Bas Couwenberg
gitlab at salsa.debian.org
Fri Jun 5 19:04:37 BST 2020
Bas Couwenberg pushed to branch master at Debian GIS Project / owslib
Commits:
1d9cb1b2 by Bas Couwenberg at 2020-06-05T19:56:47+02:00
New upstream version 0.20.0
- - - - -
a01d8869 by Bas Couwenberg at 2020-06-05T19:56:54+02:00
Update upstream source from tag 'upstream/0.20.0'
Update to upstream version '0.20.0'
with Debian dir 076342d95a37927ba7f1d9ecb0769ac4e7592ccc
- - - - -
595638f3 by Bas Couwenberg at 2020-06-05T19:57:30+02:00
New upstream release.
- - - - -
0b81fb7b by Bas Couwenberg at 2020-06-05T19:58:35+02:00
Set distribution to unstable.
- - - - -
22 changed files:
- CHANGES.rst
- VERSION.txt
- debian/changelog
- docs/en/index.rst
- owslib/__init__.py
- owslib/feature/__init__.py
- owslib/feature/schema.py
- owslib/feature/wfs110.py
- owslib/feature/wfs200.py
- owslib/fes.py
- owslib/ogcapi/__init__.py
- owslib/ogcapi/features.py
- + owslib/ogcapi/records.py
- owslib/ows.py
- owslib/util.py
- owslib/wmts.py
- owslib/wps.py
- + tests/resources/wps_DummyExecuteResponseLocalFile.xml
- tests/test_ogcapi_features_pygeoapi.py
- + tests/test_ogcapi_records_pygeoapi.py
- tests/test_wfs_schema.py
- tests/test_wps_response6.py
Changes:
=====================================
CHANGES.rst
=====================================
@@ -1,6 +1,23 @@
Changes
=======
+0.20.0 (2020-06-05)
+-------------------
+
+This release provides initial support for the draft OGC API - Records
+standard.
+
+A full list of commits for 0.20.0 can be found at:
+
+https://github.com/geopython/OWSLib/commits/0.20.0
+
+- WFS: make wfs:FeatureTypeList optional for 1.1 and 2.0 (#673)
+- OGC API - Records: initial draft implementation (#679, #693)
+- WPS: add support for retrieving data from local filesystem (huard, #681)
+- WMTS: add support for boundingboxes (kordian-kowalski, #687)
+- Authentication: Enable switching off SSL verification (Samweli, #685)
+
+
0.19.2 (2020-03-13)
-------------------
=====================================
VERSION.txt
=====================================
@@ -1 +1 @@
-0.19.2
+0.20.0
=====================================
debian/changelog
=====================================
@@ -1,10 +1,11 @@
-owslib (0.19.2-2) UNRELEASED; urgency=medium
+owslib (0.20.0-1) unstable; urgency=medium
* Team upload.
+ * New upstream release.
* Bump debhelper compat to 10, changes:
- Drop --parallel option, enabled by default
- -- Bas Couwenberg <sebastic at debian.org> Thu, 19 Mar 2020 20:07:21 +0100
+ -- Bas Couwenberg <sebastic at debian.org> Fri, 05 Jun 2020 19:58:24 +0200
owslib (0.19.2-1) unstable; urgency=medium
=====================================
docs/en/index.rst
=====================================
@@ -297,15 +297,19 @@ services)
>>> response = wfs20.getfeature(storedQueryID='urn:ogc:def:query:OGC-WFS::GetFeatureById', storedQueryParams={'ID':'gmd_ex.1'})
-OGC API - Features 1.0
-----------------------
+OGC API
+-------
-The OGC API - Features standard is a clean break from the traditional OGC service architecture
-(RESTful, JSON, OpenAPI) and as such OWSLib the code follows the same pattern.
+The `OGC API`_ standards are a clean break from the traditional OGC service architecture
+using current design patterns (RESTful, JSON, OpenAPI). As such, OWSLib the code follows
+the same pattern.
+
+OGC API - Features 1.0
+^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: python
- >>> from owslib.ogcapi import Features
+ >>> from owslib.ogcapi.features import Features
>>> w = Features('https://demo.pygeoapi.io/cite')
>>> w.url
'https://demo.pygeoapi.io/cite'
@@ -316,16 +320,54 @@ The OGC API - Features standard is a clean break from the traditional OGC servic
>>> len(collections)
13
>>> lakes = w.collection('lakes')
- >>> lakes['name']
+ >>> lakes['id']
'lakes'
>>> lakes['title']
'Large Lakes'
>>> lakes['description']
'lakes of the world, public domain'
+ >>> lakes_queryables = w.collection_queryables('lakes')
+ >>> len(lakes_queryables['queryables'])
+ 6
>>> lakes_query = w.collection_items('lakes')
>>> lakes_query['features'][0]['properties']
{u'scalerank': 0, u'name_alt': None, u'admin': None, u'featureclass': u'Lake', u'id': 0, u'name': u'Lake Baikal'}
+OGC API - Records 1.0
+^^^^^^^^^^^^^^^^^^^^^
+
+ >>> from owslib.ogcapi.records import Records
+ >>> w = Records('https://example.org/records-api')
+ >>> w.url
+ 'https://example.org/records-api'
+ >>> conformance = w.conformance()
+ {'conformsTo': [u'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core', u'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30', u'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html', u'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson', u'http://www.opengis.net/spec/ogcapi-records-1/1.0/req/core', u'http://www.opengis.net/spec/ogcapi-records/1.0/req/oas30', u'http://www.opengis.net/spec/ogcapi-records-1/1.0/req/json', u'http://www.opengis.net/spec/ogcapi-records-1/1.0/req/html']}
+ >>> api = w.api() # OpenAPI definition
+ >>> collections = w.collections()
+ >>> len(collections)
+ 1
+ >>> my_catalogue = w.collection('my-catalogue')
+ >>> my_catalogue['id']
+ 'my-catalogue'
+ >>> my_catalogue['title']
+ 'My catalogue'
+ >>> my_catalogue['description']
+ 'My catalogue'
+ >>> my_catalogue_queryables = w.collection_queryables('my-catalogue')
+ >>> len(my_catalogue_queryables['queryables'])
+ 8
+ >>> my_catalogue_query = w.collection_items('my-catalogue')
+ >>> my_catalogue_query['features'][0]['properties'].keys()
+ [u'title', u'abstract', u'keywords']
+ >>> my_catalogue_query['features'][0]['properties']['title']
+ u'Roadrunner ambush locations'
+ >>> my_catalogue_query2 = w.collection_items('my-catalogue', q='birds')
+ >>> msc_wis_dcpc_query2['numberMatched']
+ 2
+ >>> msc_wis_dcpc_query2['numberReturned']
+ 2
+
+
WCS
---
@@ -754,6 +796,7 @@ Credits
.. _`CIA.vc`: http://cia.vc/stats/project/OWSLib
.. _`WaterML`: http://his.cuahsi.org/wofws.html#waterml
.. _`Swiss GM03`: https://www.geocat.admin.ch/en/dokumentation/gm03.html
+.. _`OGC API`: http://www.ogcapi.org
.. include:: ../../CHANGES.rst
=====================================
owslib/__init__.py
=====================================
@@ -1 +1 @@
-__version__ = '0.19.2'
+__version__ = '0.20.0'
=====================================
owslib/feature/__init__.py
=====================================
@@ -176,7 +176,7 @@ class WebFeatureService_(object):
request["query"] = str(filter)
if typename:
typename = (
- [typename] if type(typename) == type("") else typename
+ [typename] if isinstance(typename, str) else typename
) # noqa: E721
if int(self.version.split(".")[0]) >= 2:
request["typenames"] = ",".join(typename)
=====================================
owslib/feature/schema.py
=====================================
@@ -84,7 +84,8 @@ def _construct_schema(elements, nsmap):
:return dict: schema
"""
-
+ if elements is None:
+ return None
schema = {"properties": {}, "required": [], "geometry": None}
schema_key = None
=====================================
owslib/feature/wfs110.py
=====================================
@@ -186,9 +186,10 @@ class WebFeatureService_1_1_0(WebFeatureService_):
features = self._capabilities.findall(
nspath_eval("wfs:FeatureTypeList/wfs:FeatureType", namespaces)
)
- for feature in features:
- cm = ContentMetadata(feature, parse_remote_metadata, headers=self.headers, auth=self.auth)
- self.contents[cm.id] = cm
+ if features is not None:
+ for feature in features:
+ cm = ContentMetadata(feature, parse_remote_metadata, headers=self.headers, auth=self.auth)
+ self.contents[cm.id] = cm
# exceptions
self.exceptions = [
=====================================
owslib/feature/wfs200.py
=====================================
@@ -141,16 +141,17 @@ class WebFeatureService_2_0_0(WebFeatureService_):
featuretypelistelem = self._capabilities.find(
nspath("FeatureTypeList", ns=WFS_NAMESPACE)
)
- featuretypeelems = featuretypelistelem.findall(
- nspath("FeatureType", ns=WFS_NAMESPACE)
- )
- if serviceidentelem is not None:
- for f in featuretypeelems:
- kwds = f.findall(nspath("Keywords/Keyword", ns=OWS_NAMESPACE))
- if kwds is not None:
- for kwd in kwds[:]:
- if kwd.text not in self.identification.keywords:
- self.identification.keywords.append(kwd.text)
+ if featuretypelistelem is not None:
+ featuretypeelems = featuretypelistelem.findall(
+ nspath("FeatureType", ns=WFS_NAMESPACE)
+ )
+ if serviceidentelem is not None:
+ for f in featuretypeelems:
+ kwds = f.findall(nspath("Keywords/Keyword", ns=OWS_NAMESPACE))
+ if kwds is not None:
+ for kwd in kwds[:]:
+ if kwd.text not in self.identification.keywords:
+ self.identification.keywords.append(kwd.text)
# TODO: update serviceProvider metadata, miss it out for now
serviceproviderelem = self._capabilities.find(nspath("ServiceProvider"))
=====================================
owslib/fes.py
=====================================
@@ -184,6 +184,15 @@ class FilterCapabilities(object):
""" Abstraction for Filter_Capabilities """
def __init__(self, elem):
# Spatial_Capabilities
+
+ if elem is None:
+ self.spatial_operands = []
+ self.spatial_operators = []
+ self.temporal_operators = []
+ self.temporal_operands = []
+ self.scalar_comparison_operators = []
+ return
+
self.spatial_operands = [f.text for f in elem.findall(util.nspath_eval(
'ogc:Spatial_Capabilities/ogc:GeometryOperands/ogc:GeometryOperand', namespaces))]
self.spatial_operators = []
@@ -207,6 +216,16 @@ class FilterCapabilities(object):
class FilterCapabilities200(object):
"""Abstraction for Filter_Capabilities 2.0"""
def __init__(self, elem):
+
+ if elem is None:
+ self.spatial_operands = []
+ self.spatial_operators = []
+ self.temporal_operators = []
+ self.temporal_operands = []
+ self.scalar_comparison_operators = []
+ self.conformance = []
+ return
+
# Spatial_Capabilities
self.spatial_operands = [f.attrib.get('name') for f in elem.findall(util.nspath_eval(
'fes:Spatial_Capabilities/fes:GeometryOperands/fes:GeometryOperand', namespaces))]
=====================================
owslib/ogcapi/__init__.py
=====================================
@@ -10,10 +10,11 @@ import json
import logging
from urllib.parse import urljoin
+import requests
import yaml
from owslib import __version__
-from owslib.util import http_get
+from owslib.util import Authentication, http_get
LOGGER = logging.getLogger(__name__)
@@ -23,14 +24,15 @@ REQUEST_HEADERS = {
class API(object):
- """Abstraction for OGC API Common version 1.0"""
+ """Abstraction for OGC API - Common version 1.0"""
- def __init__(self, url, json_=None, timeout=30, headers=None, auth=None):
+ def __init__(self, url: str, json_: str = None, timeout: int = 30,
+ headers: dict = None, auth: Authentication = None):
"""
Initializer; implements /
@type url: string
- @param url: url of WFS root document
+ @param url: url of OGC API landing page document
@type json_: string
@param json_: json object
@param headers: HTTP headers to send with requests
@@ -52,7 +54,7 @@ class API(object):
self.timeout = timeout
self.headers = REQUEST_HEADERS
if headers:
- self.headers = self.headers.update(headers)
+ self.headers.update(headers)
self.auth = auth
if json_ is not None: # static JSON string
@@ -61,7 +63,7 @@ class API(object):
response = http_get(url, headers=self.headers, auth=self.auth).json()
self.links = response['links']
- def api(self):
+ def api(self) -> dict:
"""
implements /api
@@ -75,17 +77,17 @@ class API(object):
openapi_yaml_mimetype = 'application/vnd.oai.openapi;version=3.0'
LOGGER.debug('Searching for OpenAPI JSON Document')
- for l in self.links:
- if l['rel'] == 'service-desc' and l['type'] == openapi_json_mimetype:
+ for link in self.links:
+ if link['rel'] == 'service-desc' and link['type'] == openapi_json_mimetype:
openapi_format = openapi_json_mimetype
- url = l['href']
+ url = link['href']
break
LOGGER.debug('Searching for OpenAPI YAML Document')
if url is None:
- if l['rel'] == 'service-desc' and l['type'] == openapi_yaml_mimetype:
+ if link['rel'] == 'service-desc' and link['type'] == openapi_yaml_mimetype:
openapi_format = openapi_yaml_mimetype
- url = l['href']
+ url = link['href']
break
if url is not None:
@@ -101,33 +103,27 @@ class API(object):
LOGGER.error(msg)
raise RuntimeError(msg)
- def conformance(self):
+ def conformance(self) -> dict:
"""
implements /conformance
@returns: `dict` of conformance object
"""
- url = self._build_url('conformance')
- LOGGER.debug('Request: {}'.format(url))
- response = http_get(url, headers=self.headers, auth=self.auth).json()
-
- return response
+ path = 'conformance'
+ return self._request(path)
- def collections(self):
+ def collections(self) -> dict:
"""
implements /collections
@returns: `dict` of collections object
"""
- url = self._build_url('collections')
- LOGGER.debug('Request: {}'.format(url))
- response = http_get(url, headers=self.headers, auth=self.auth).json()
-
- return response['collections']
+ path = 'collections'
+ return self._request(path)
- def collection(self, collection_id):
+ def collection(self, collection_id) -> dict:
"""
implements /collections/{collectionId}
@@ -138,18 +134,27 @@ class API(object):
"""
path = 'collections/{}'.format(collection_id)
- url = self._build_url(path)
- LOGGER.debug('Request: {}'.format(url))
- response = http_get(url, headers=self.headers, auth=self.auth).json()
+ return self._request(path)
+
+ def collection_queryables(self, collection_id) -> dict:
+ """
+ implements /collections/{collectionId}/queryables
+
+ @type collection_id: string
+ @param collection_id: id of collection
+
+ @returns: `dict` of feature collection queryables
+ """
- return response
+ path = 'collections/{}/queryables'.format(collection_id)
+ return self._request(path)
- def _build_url(self, path=None):
+ def _build_url(self, path=None) -> str:
"""
helper function to build an OGC API URL
@type path: string
- @param path: path of WFS URL
+ @param path: path of OGC API URL
@returns: fully constructed URL path
"""
@@ -165,3 +170,26 @@ class API(object):
LOGGER.debug('URL: {}'.format(url))
return url
+
+ def _request(self, path=None, kwargs={}) -> dict:
+ """
+ helper function for request/response patterns against OGC API endpoints
+
+ @type path: string
+ @param path: path of request
+ @type kwargs: string
+ @param kwargs: ``dict`` of keyword value pair request parameters
+
+ @returns: response as JSON ``dict``
+ """
+
+ url = self._build_url(path)
+
+ LOGGER.debug('Request: {}'.format(url))
+
+ response = http_get(url, headers=self.headers, auth=self.auth,
+ params=kwargs)
+ if response.status_code != requests.codes.ok:
+ raise RuntimeError(response.text)
+
+ return response.json()
=====================================
owslib/ogcapi/features.py
=====================================
@@ -6,13 +6,10 @@
# Contact email: tomkralidis at gmail.com
# =============================================================================
-import json
import logging
-from urllib.parse import urljoin
-
-from owslib.ogcapi import API, REQUEST_HEADERS
-from owslib.util import http_get
+from owslib.ogcapi import API
+from owslib.util import Authentication
LOGGER = logging.getLogger(__name__)
@@ -20,11 +17,12 @@ LOGGER = logging.getLogger(__name__)
class Features(API):
"""Abstraction for OGC API - Features"""
- def __init__(self, url, json_=None, timeout=30, headers=None, auth=None):
+ def __init__(self, url: str, json_: str = None, timeout: int = 30,
+ headers: dict = None, auth: Authentication = None):
__doc__ = API.__doc__ # noqa
super().__init__(url, json_, timeout, headers, auth)
- def collection_items(self, collection_id, **kwargs):
+ def collection_items(self, collection_id: str, **kwargs: dict) -> dict:
"""
implements /collection/{collectionId}/items
@@ -38,6 +36,8 @@ class Features(API):
@param limit: limit number of features
@type startindex: int
@param startindex: start position of results
+ @type q: string
+ @param q: full text search
@returns: feature results
"""
@@ -46,14 +46,9 @@ class Features(API):
kwargs['bbox'] = ','.join(kwargs['bbox'])
path = 'collections/{}/items'.format(collection_id)
- url = self._build_url(path)
- LOGGER.debug('Request: {}'.format(url))
- response = http_get(
- url, headers=self.headers, params=kwargs, auth=self.auth
- ).json()
- return response
+ return self._request(path, kwargs)
- def collection_item(self, collection_id, identifier):
+ def collection_item(self, collection_id: str, identifier: str) -> dict:
"""
implements /collections/{collectionId}/items/{featureId}
@@ -66,7 +61,4 @@ class Features(API):
"""
path = 'collections/{}/items/{}'.format(collection_id, identifier)
- url = self._build_url(path)
- LOGGER.debug('Request: {}'.format(url))
- response = http_get(url, headers=self.headers, auth=self.auth).json()
- return response
+ return self._request(path)
=====================================
owslib/ogcapi/records.py
=====================================
@@ -0,0 +1,23 @@
+# =============================================================================
+# Copyright (c) 2020 Tom Kralidis
+#
+# Author: Tom Kralidis <tomkralidis at gmail.com>
+#
+# Contact email: tomkralidis at gmail.com
+# =============================================================================
+
+import logging
+
+from owslib.ogcapi.features import Features
+from owslib.util import Authentication
+
+LOGGER = logging.getLogger(__name__)
+
+
+class Records(Features):
+ """Abstraction for OGC API - Records"""
+
+ def __init__(self, url: str, json_: str = None, timeout: int = 30,
+ headers: dict = None, auth: Authentication = None):
+ __doc__ = Features.__doc__ # noqa
+ super().__init__(url, json_, timeout, headers, auth)
=====================================
owslib/ows.py
=====================================
@@ -239,7 +239,10 @@ class BoundingBox(object):
self.miny = None
self.maxx = None
self.maxy = None
-
+ self.crs = None
+ self.dimensions = 2
+ if elem is None:
+ return
val = elem.attrib.get('crs') or elem.attrib.get('{{{}}}crs'.format(namespace))
if val:
try:
=====================================
owslib/util.py
=====================================
@@ -170,14 +170,13 @@ def openURL(url_base, data=None, method='Get', cookies=None, username=None, pass
auth.password = password
if cert:
auth.cert = cert
- if verify and not auth.verify:
- auth.verify = verify
+ verify = verify and auth.verify
else:
auth = Authentication(username, password, cert, verify)
if auth.username and auth.password:
rkwargs['auth'] = (auth.username, auth.password)
rkwargs['cert'] = auth.cert
- rkwargs['verify'] = auth.verify
+ rkwargs['verify'] = verify
# FIXUP for WFS in particular, remove xml style namespace
# @TODO does this belong here?
=====================================
owslib/wmts.py
=====================================
@@ -48,6 +48,7 @@ _XLINK_NS = '{http://www.w3.org/1999/xlink}'
# Version 1.0.0, document 07-057r7
_ABSTRACT_TAG = _OWS_NS + 'Abstract'
+_BOUNDING_BOX_TAG = _OWS_NS + 'BoundingBox'
_IDENTIFIER_TAG = _OWS_NS + 'Identifier'
_LOWER_CORNER_TAG = _OWS_NS + 'LowerCorner'
_OPERATIONS_METADATA_TAG = _OWS_NS + 'OperationsMetadata'
@@ -659,6 +660,31 @@ class TileMatrixSetLink(object):
return fmt.format(self=self)
+class BoundingBox(object):
+ """
+ Represents a BoundingBox element
+ """
+
+ def __init__(self, elem) -> None:
+ if elem.tag != _BOUNDING_BOX_TAG:
+ raise ValueError('%s should be a BoundingBox' % elem)
+
+ lc = elem.find(_LOWER_CORNER_TAG)
+ uc = elem.find(_UPPER_CORNER_TAG)
+
+ self.ll = [float(s) for s in lc.text.split()]
+ self.ur = [float(s) for s in uc.text.split()]
+
+ self.crs = elem.attrib.get('crs')
+ self.extent = (self.ll[0], self.ll[1], self.ur[0], self.ur[1])
+
+ def __repr__(self):
+ fmt = ('<BoundingBox'
+ ', crs={self.crs}'
+ ', extent={self.extent}>')
+ return fmt.format(self=self)
+
+
class ContentMetadata:
"""
Abstraction for WMTS layer metadata.
@@ -684,9 +710,16 @@ class ContentMetadata:
self.abstract = testXMLValue(elem.find(_ABSTRACT_TAG))
- # bboxes
+ # Bounding boxes
+ # There may be multiple, using different CRSes
+ self.boundingBox = []
+
+ bbs = elem.findall(_BOUNDING_BOX_TAG)
+ for b in bbs:
+ self.boundingBox.append(BoundingBox(b))
+
+ # WGS84 Bounding box
b = elem.find(_WGS84_BOUNDING_BOX_TAG)
- self.boundingBox = None
if b is not None:
lc = b.find(_LOWER_CORNER_TAG)
uc = b.find(_UPPER_CORNER_TAG)
=====================================
owslib/wps.py
=====================================
@@ -84,7 +84,7 @@ the live USGS and PML servers. To run:
* python wps-pml-script-2.py
The file wps-client.py contains a command-line client that can be used to submit a "GetCapabilities",
-"DescribeProcess" or "Execute" request to an arbitratry WPS server. For example, you can run it as follows:
+"DescribeProcess" or "Execute" request to an arbitrary WPS server. For example, you can run it as follows:
* cd examples
* To prints out usage and example invocations: wps-client -help
@@ -492,7 +492,7 @@ class WPSReader(object):
"""
Method to get and parse a WPS document, returning an elementtree instance.
:param str url: WPS service base url.
- :param str data: GET: dictionary of HTTP (key, value) parameter pairs, POST: XML document to post
+ :param {} data: GET: dictionary of HTTP (key, value) parameter pairs, POST: XML document to post
"""
_fix_auth(self.auth, username, password, verify, cert)
if method == 'Get':
@@ -851,46 +851,52 @@ class WPSExecution(object):
def isNotComplete(self):
return not self.isComplete()
- def getOutput(self, filepath=None):
+ def getOutput(self, filepath=None, identifier=None):
"""
Method to write the outputs of a WPS process to a file:
either retrieves the referenced files from the server, or writes out the content of response embedded output.
:param filepath: optional path to the output file, otherwise a file will be created in the local directory with
the name assigned by the server, or default name 'wps.out' for embedded output.
+ :param: identifier: optional identifier of the output that should be written.
+ For backward compatibility it will default to the first output.
"""
if self.isSucceded():
content = b''
- for output in self.processOutputs:
-
- output_content = output.retrieveData(
- self.auth.username, self.auth.password,
- headers=self.headers, verify=self.auth.verify, cert=self.auth.cert)
-
+ output = None
+ if self.processOutputs:
+ if identifier:
+ # filter outputs by identifier
+ outputs = [o for o in self.processOutputs if o.identifier == identifier]
+ if outputs:
+ output = outputs[0]
+ else:
+ # take the first found output
+ output = self.processOutputs[0]
+ if output:
# ExecuteResponse contains reference to server-side output
- if output_content != b'':
- content = content + output_content
+ if output.reference:
+ content = output.retrieveData(
+ self.auth.username, self.auth.password,
+ headers=self.headers, verify=self.auth.verify, cert=self.auth.cert)
if filepath is None:
filepath = output.fileName
-
# ExecuteResponse contain embedded output
- if len(output.data) > 0:
+ elif len(output.data) > 0:
if filepath is None:
filepath = 'wps.out'
for data in output.data:
content = content + data.encode()
-
# write out content
- if content != '':
+ if content != b'':
out = open(filepath, 'wb')
out.write(content)
out.close()
- log.info('Output written to file: %s' % filepath)
-
+ log.info(f'Output written to file: {filepath}')
else:
raise Exception(
- "Execution not successfully completed: status=%s" % self.status)
+ f"Execution not successfully completed: status={self.status}")
def submitRequest(self, request):
"""
@@ -1417,19 +1423,28 @@ class Output(InputOutput):
# a) 'http://cida.usgs.gov/climate/gdp/process/RetrieveResultServlet?id=1318528582026OUTPUT.601bb3d0-547f-4eab-8642-7c7d2834459e' # noqa
# b) 'http://rsg.pml.ac.uk/wps/wpsoutputs/outputImage-11294Bd6l2a.tif'
log.info('Output URL=%s' % url)
+
+ # Extract output filepath from base URL
+ self.fileName = url.split('/')[-1]
+
+ # The link is a local file.
+ # Useful when running local tests during development.
+ if url.startswith("file://"):
+ with open(url[7:]) as f:
+ return f.read()
+
if '?' in url:
spliturl = url.split('?')
+ # Extract output filepath from URL query string
+ self.fileName = spliturl[1].split('=')[1]
+
u = openURL(spliturl[0], spliturl[
1], method='Get', username=username, password=password,
headers=headers, verify=verify, cert=cert)
- # extract output filepath from URL query string
- self.fileName = spliturl[1].split('=')[1]
else:
u = openURL(
url, '', method='Get', username=username, password=password,
headers=headers, verify=verify, cert=cert)
- # extract output filepath from base URL
- self.fileName = url.split('/')[-1]
return u.read()
@@ -1439,7 +1454,7 @@ class Output(InputOutput):
Method to write an output of a WPS process to disk:
it either retrieves the referenced file from the server, or write out the content of response embedded output.
- :param filepath: optional path to the output file, otherwise a file will be created in the local directory
+ :param path: optional path to the output file, otherwise a file will be created in the local directory
with the name assigned by the server,
:param username: credentials to access the remote WPS server
:param password: credentials to access the remote WPS server
=====================================
tests/resources/wps_DummyExecuteResponseLocalFile.xml
=====================================
@@ -0,0 +1,24 @@
+<wps:ExecuteResponse xmlns:gml="http://www.opengis.net/gml" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:wps="http://www.opengis.net/wps/1.0.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wps/1.0.0 http://schemas.opengis.net/wps/1.0.0/wpsExecute_response.xsd" service="WPS" version="1.0.0" xml:lang="en-US" serviceInstance="http://localhost:8092/wps?service=WPS&request=GetCapabilities" statusLocation="http://localhost:8090/wpsoutputs/hummingbird/731f5bdc-52b7-11e8-a0a9-109836a7cf3a.xml">
+ <wps:Process wps:processVersion="0.1.0">
+ <ows:Identifier>dummy</ows:Identifier>
+ <ows:Title>Test response with reference to file on local filesystem</ows:Title>
+ </wps:Process>
+ <wps:Status creationTime="2020-05-08T14:00:54Z">
+ <wps:ProcessSucceeded>PyWPS Dummy Process finished</wps:ProcessSucceeded>
+ </wps:Status>
+ <wps:OutputDefinitions>
+ <wps:Output>
+ <ows:Identifier>output</ows:Identifier>
+ <ows:Title>Test output</ows:Title>
+ <ows:Abstract>Test output file on the local filesystem.</ows:Abstract>
+ </wps:Output>
+ </wps:OutputDefinitions>
+ <wps:ProcessOutputs>
+ <wps:Output>
+ <ows:Identifier>output</ows:Identifier>
+ <ows:Title>Test Report</ows:Title>
+ <ows:Abstract>Compliance checker test report.</ows:Abstract>
+ <wps:Reference xlink:href="file:///{tmpdir}/output.txt" mimeType="text/plain"/>
+ </wps:Output>
+ </wps:ProcessOutputs>
+</wps:ExecuteResponse>
=====================================
tests/test_ogcapi_features_pygeoapi.py
=====================================
@@ -33,9 +33,12 @@ def test_ogcapi_features_pygeoapi():
assert lakes['title'] == 'Large Lakes'
assert lakes['description'] == 'lakes of the world, public domain'
+ lakes_queryables = w.collection_queryables('lakes')
+ assert len(lakes_queryables['queryables']) == 6
+
# Minimum of limit param is 1
- lakes_query = w.collection_items('lakes', limit=0)
- assert lakes_query['code'] == 'InvalidParameterValue'
+ with pytest.raises(RuntimeError):
+ lakes_query = w.collection_items('lakes', limit=0)
lakes_query = w.collection_items('lakes', limit=1)
assert lakes_query['numberMatched'] == 25
=====================================
tests/test_ogcapi_records_pygeoapi.py
=====================================
@@ -0,0 +1,51 @@
+from tests.utils import service_ok
+
+import pytest
+
+from owslib.ogcapi.records import Records
+
+SERVICE_URL = 'https://dev.api.weather.gc.ca/msc-wis-dcpc'
+
+
+ at pytest.mark.online
+ at 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://dev.api.weather.gc.ca/msc-wis-dcpc/'
+ 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/discovery-metadata'] is not None
+
+ conformance = w.conformance()
+ assert len(conformance['conformsTo']) == 8
+
+ collections = w.collections()
+ assert len(collections) > 0
+
+ msc_wis_dcpc = w.collection('discovery-metadata')
+ assert msc_wis_dcpc['id'] == 'discovery-metadata'
+ assert msc_wis_dcpc['title'] == 'MSC discovery metadata'
+ assert msc_wis_dcpc['description'] == 'MSC discovery metadata'
+
+ msc_wis_dcpc_queryables = w.collection_queryables('discovery-metadata')
+ assert len(msc_wis_dcpc_queryables['queryables']) == 7
+
+ # Minimum of limit param is 1
+ with pytest.raises(RuntimeError):
+ msc_wis_dcpc_query = w.collection_items('discovery-metadata', limit=0)
+
+ msc_wis_dcpc_query = w.collection_items('discovery-metadata', limit=1)
+ assert msc_wis_dcpc_query['numberMatched'] == 178
+ assert msc_wis_dcpc_query['numberReturned'] == 1
+ assert len(msc_wis_dcpc_query['features']) == 1
+
+ msc_wis_dcpc_query = w.collection_items('discovery-metadata', q='metar')
+ assert msc_wis_dcpc_query['numberMatched'] == 2
+ assert msc_wis_dcpc_query['numberReturned'] == 2
+ assert len(msc_wis_dcpc_query['features']) == 2
=====================================
tests/test_wfs_schema.py
=====================================
@@ -87,7 +87,7 @@ class TestOnline(object):
@pytest.mark.online
@pytest.mark.skipif(not service_ok(WFS_SERVICE_URL),
reason="WFS service is unreachable")
- @pytest.mark.parametrize("wfs_version", ["1.0.0", "1.1.0", "2.0.0"])
+ @pytest.mark.parametrize("wfs_version", ["1.1.0", "2.0.0"])
def test_get_schema(self, wfs_version):
"""Test the get_schema method for a standard schema."""
wfs = WebFeatureService(WFS_SERVICE_URL, version=wfs_version)
@@ -96,7 +96,7 @@ class TestOnline(object):
@pytest.mark.online
@pytest.mark.skipif(not service_ok(WFS_SERVICE_URL),
reason="WFS service is unreachable")
- @pytest.mark.parametrize("wfs_version", ["1.0.0", "1.1.0", "2.0.0"])
+ @pytest.mark.parametrize("wfs_version", ["1.1.0", "2.0.0"])
def test_schema_result(self, wfs_version):
"""Test whether the output from get_schema is a wellformed dictionary."""
wfs = WebFeatureService(WFS_SERVICE_URL, version=wfs_version)
=====================================
tests/test_wps_response6.py
=====================================
@@ -30,3 +30,22 @@ def test_wps_response6():
response = output.data[0]
should_return = '''<ns3:FeatureCollection xmlns:ns3="http://ogr.maptools.org/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns0="http://www.opengis.net/wps/1.0.0" xsi:schemaLocation="http://ogr.maptools.org/ output_0n7ij9D.xsd">\n\t\t\t\t\t <gml:boundedBy xmlns:gml="http://www.opengis.net/gml">\n\t\t\t\t\t <gml:Box>\n\t\t\t\t\t <gml:coord><gml:X>-960123.1421801626</gml:X><gml:Y>4665723.56559387</gml:Y></gml:coord>\n\t\t\t\t\t <gml:coord><gml:X>-101288.6510608822</gml:X><gml:Y>5108200.011823481</gml:Y></gml:coord>\n\t\t\t\t\t </gml:Box>\n\t\t\t\t\t </gml:boundedBy> \n\t\t\t\t\t <gml:featureMember xmlns:gml="http://www.opengis.net/gml">\n\t\t\t\t\t <ns3:output fid="F0">\n\t\t\t\t\t <ns3:geometryProperty><gml:LineString><gml:coordinates>-960123.142180162365548,4665723.565593870356679,0 -960123.142180162365548,4665723.565593870356679,0 -960123.142180162598379,4665723.565593870356679,0 -960123.142180162598379,4665723.565593870356679,0 -711230.141176006174646,4710278.48552671354264,0 -711230.141176006174646,4710278.48552671354264,0 -623656.677859728806652,4848552.374973464757204,0 -623656.677859728806652,4848552.374973464757204,0 -410100.337491964863148,4923834.82589447684586,0 -410100.337491964863148,4923834.82589447684586,0 -101288.651060882242746,5108200.011823480948806,0 -101288.651060882242746,5108200.011823480948806,0 -101288.651060882257298,5108200.011823480948806,0 -101288.651060882257298,5108200.011823480948806,0</gml:coordinates></gml:LineString></ns3:geometryProperty>\n\t\t\t\t\t <ns3:cat>1</ns3:cat>\n\t\t\t\t\t <ns3:id>1</ns3:id>\n\t\t\t\t\t <ns3:fcat>0</ns3:fcat>\n\t\t\t\t\t <ns3:tcat>0</ns3:tcat>\n\t\t\t\t\t <ns3:sp>0</ns3:sp>\n\t\t\t\t\t <ns3:cost>1002619.181</ns3:cost>\n\t\t\t\t\t <ns3:fdist>0</ns3:fdist>\n\t\t\t\t\t <ns3:tdist>0</ns3:tdist>\n\t\t\t\t\t </ns3:output>\n\t\t\t\t\t </gml:featureMember>\n\t\t\t\t\t</ns3:FeatureCollection>''' # noqa
assert compare_xml(should_return, response) is True
+
+
+def test_wps_response_local_file(tmpdir):
+ # Build WPS object; service has been down for some time so skip caps here
+ wps = WebProcessingService('http://localhost', skip_caps=True)
+
+ # Write dummy output file
+ out_fn = tmpdir / "output.txt"
+ content = 'hi there'
+ out_fn.write_text(content, encoding="utf8")
+
+ # Execute fake WPS invocation
+ response = open(resource_file('wps_DummyExecuteResponseLocalFile.xml'), 'r').read()
+ execution = wps.execute(None, [], response=response.format(tmpdir=str(tmpdir)))
+
+ # Retrieve data from local file system
+ out = execution.processOutputs[0]
+ txt = out.retrieveData()
+ assert txt == content
View it on GitLab: https://salsa.debian.org/debian-gis-team/owslib/-/compare/4cbafcb5d2bac7087a080952b76c9e2a20c322e5...0b81fb7ba60461446bf29bb2c95ed84de1372ae1
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/owslib/-/compare/4cbafcb5d2bac7087a080952b76c9e2a20c322e5...0b81fb7ba60461446bf29bb2c95ed84de1372ae1
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20200605/717e978f/attachment-0001.html>
More information about the Pkg-grass-devel
mailing list