[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