Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-OWSLib for openSUSE:Factory checked in at 2021-01-25 18:23:17 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-OWSLib (Old) and /work/SRC/openSUSE:Factory/.python-OWSLib.new.28504 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-OWSLib" Mon Jan 25 18:23:17 2021 rev:4 rq:866244 version:0.21.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-OWSLib/python-OWSLib.changes 2020-12-08 13:25:49.574747971 +0100 +++ /work/SRC/openSUSE:Factory/.python-OWSLib.new.28504/python-OWSLib.changes 2021-01-25 18:23:36.952428374 +0100 @@ -1,0 +2,18 @@ +Thu Jan 21 09:57:32 UTC 2021 - Guillaume GARDET <guillaume.gar...@opensuse.org> + +- Depend on python-PyYAML instead of python-pyyaml + +------------------------------------------------------------------- +Tue Jan 19 08:47:04 UTC 2021 - Bruno Friedmann <br...@ioda-net.ch> + +- update to version 0.21.1 + + This release adds the draft implementation of OGC API + + Coverages support as well as numerous fixes to the codebase. + + A full list of commits for 0.21.0 can be found at: + https://github.com/geopython/OWSLib/commits/0.21.0 +- packaging + + Update copyright year + + Sync list of Requires (pyproj > 2 and add pyyaml) with + requirements + +------------------------------------------------------------------- Old: ---- OWSLib-0.20.0.tar.gz New: ---- OWSLib-0.21.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-OWSLib.spec ++++++ --- /var/tmp/diff_new_pack.CzQuEx/_old 2021-01-25 18:23:37.624429333 +0100 +++ /var/tmp/diff_new_pack.CzQuEx/_new 2021-01-25 18:23:37.628429338 +0100 @@ -1,9 +1,9 @@ # # spec file for package python-OWSLib # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # Copyright (c) 2015 Angelos Tzotsos <tzot...@opensuse.org> -# Copyright (c) 2020 Ioda-Net S??rl, Bruno Friedmann, Charmoille, Switzerland. +# Copyright (c) 2021 Ioda-Net S??rl, Bruno Friedmann, Charmoille, Switzerland. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -22,7 +22,7 @@ %define oldpython python %define skip_python2 1 Name: python-OWSLib -Version: 0.20.0 +Version: 0.21.0 Release: 0 Summary: Python interface to OGC Web Services License: BSD-3-Clause @@ -33,9 +33,10 @@ BuildRequires: %{python_module setuptools} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-pyproj +Requires: python-pyproj >= 2 Requires: python-python-dateutil >= 1.5 Requires: python-pytz +Requires: python-PyYAML Requires: python-requests >= 1.0 Provides: python-owslib = %{version} Obsoletes: python-owslib < %{version} ++++++ OWSLib-0.20.0.tar.gz -> OWSLib-0.21.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/CHANGES.rst new/OWSLib-0.21.0/CHANGES.rst --- old/OWSLib-0.20.0/CHANGES.rst 2020-06-05 14:29:33.000000000 +0200 +++ new/OWSLib-0.21.0/CHANGES.rst 2020-12-09 03:16:44.000000000 +0100 @@ -1,6 +1,19 @@ Changes ======= +0.21.0 (2020-12-09) +------------------- + +A full list of commits for 0.21.0 can be found at: + +https://github.com/geopython/OWSLib/commits/0.21.0 + +- OGC API: Added support for Coverages (#699) +- WFS: Added POST support to WFS GetFeature (#706) +- WCS: Allow user to specify timeout on WCS GetCoverage calls (#714) +- WPS: fixed bounding-box (#719) +- Various fixes: #695, #707, #702, #716, #718, #722, #691, #720 + 0.20.0 (2020-06-05) ------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/OWSLib.egg-info/PKG-INFO new/OWSLib-0.21.0/OWSLib.egg-info/PKG-INFO --- old/OWSLib-0.20.0/OWSLib.egg-info/PKG-INFO 2020-06-05 14:35:06.000000000 +0200 +++ new/OWSLib-0.21.0/OWSLib.egg-info/PKG-INFO 2020-12-09 03:22:23.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 1.2 +Metadata-Version: 2.1 Name: OWSLib -Version: 0.20.0 +Version: 0.21.0 Summary: OGC Web Service utility library Home-page: https://geopython.github.io/OWSLib Author: Sean Gillies @@ -169,7 +169,13 @@ Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Scientific/Engineering :: GIS Requires-Python: >=3.6 +Description-Content-Type: text/x-rst diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/OWSLib.egg-info/SOURCES.txt new/OWSLib-0.21.0/OWSLib.egg-info/SOURCES.txt --- old/OWSLib-0.20.0/OWSLib.egg-info/SOURCES.txt 2020-06-05 14:35:06.000000000 +0200 +++ new/OWSLib-0.21.0/OWSLib.egg-info/SOURCES.txt 2020-12-09 03:22:23.000000000 +0100 @@ -44,6 +44,7 @@ owslib/coverage/wcsdecoder.py owslib/feature/__init__.py owslib/feature/common.py +owslib/feature/postrequest.py owslib/feature/schema.py owslib/feature/wfs100.py owslib/feature/wfs110.py @@ -53,6 +54,7 @@ owslib/map/wms111.py owslib/map/wms130.py owslib/ogcapi/__init__.py +owslib/ogcapi/coverages.py owslib/ogcapi/features.py owslib/ogcapi/records.py owslib/owscontext/__init__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/PKG-INFO new/OWSLib-0.21.0/PKG-INFO --- old/OWSLib-0.20.0/PKG-INFO 2020-06-05 14:35:06.000000000 +0200 +++ new/OWSLib-0.21.0/PKG-INFO 2020-12-09 03:22:24.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 1.2 +Metadata-Version: 2.1 Name: OWSLib -Version: 0.20.0 +Version: 0.21.0 Summary: OGC Web Service utility library Home-page: https://geopython.github.io/OWSLib Author: Sean Gillies @@ -169,7 +169,13 @@ Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Scientific/Engineering :: GIS Requires-Python: >=3.6 +Description-Content-Type: text/x-rst diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/VERSION.txt new/OWSLib-0.21.0/VERSION.txt --- old/OWSLib-0.20.0/VERSION.txt 2020-06-05 14:29:33.000000000 +0200 +++ new/OWSLib-0.21.0/VERSION.txt 2020-12-09 03:16:55.000000000 +0100 @@ -1 +1 @@ -0.20.0 +0.21.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/__init__.py new/OWSLib-0.21.0/owslib/__init__.py --- old/OWSLib-0.20.0/owslib/__init__.py 2020-06-05 14:29:33.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/__init__.py 2020-12-09 03:17:02.000000000 +0100 @@ -1 +1 @@ -__version__ = '0.20.0' +__version__ = '0.21.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/coverage/wcs100.py new/OWSLib-0.21.0/owslib/coverage/wcs100.py --- old/OWSLib-0.20.0/owslib/coverage/wcs100.py 2020-03-29 03:39:03.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/coverage/wcs100.py 2020-11-30 17:09:17.000000000 +0100 @@ -95,7 +95,7 @@ return items def getCoverage(self, identifier=None, bbox=None, time=None, format=None, crs=None, width=None, height=None, - resx=None, resy=None, resz=None, parameter=None, method='Get', **kwargs): + resx=None, resy=None, resz=None, parameter=None, method='Get', timeout=30, **kwargs): """Request and return a coverage from the WCS as a file-like object note: additional **kwargs helps with multi-version implementation core keyword arguments should be supported cross version @@ -154,7 +154,7 @@ data = urlencode(request) log.debug('WCS 1.0.0 DEBUG: Second part of URL: %s' % data) - u = openURL(base_url, data, method, self.cookies, auth=self.auth) + u = openURL(base_url, data, method, self.cookies, auth=self.auth, timeout=timeout) return u def getOperationByName(self, name): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/coverage/wcs110.py new/OWSLib-0.21.0/owslib/coverage/wcs110.py --- old/OWSLib-0.20.0/owslib/coverage/wcs110.py 2019-11-05 01:04:04.000000000 +0100 +++ new/OWSLib-0.21.0/owslib/coverage/wcs110.py 2020-11-30 17:09:17.000000000 +0100 @@ -141,7 +141,7 @@ # TO DO: Handle rest of the WCS 1.1.0 keyword parameters e.g. GridCRS etc. def getCoverage(self, identifier=None, bbox=None, time=None, format=None, store=False, rangesubset=None, gridbaseCRS=None, gridtype=None, gridCS=None, gridorigin=None, gridoffsets=None, - method='Get', **kwargs): + method='Get', timeout=30, **kwargs): """Request and return a coverage from the WCS as a file-like object note: additional **kwargs helps with multi-version implementation core keyword arguments should be supported cross version @@ -205,7 +205,7 @@ # encode and request data = urlencode(request) - u = openURL(base_url, data, method, self.cookies, auth=self.auth) + u = openURL(base_url, data, method, self.cookies, auth=self.auth, timeout=timeout) return u def getOperationByName(self, name): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/coverage/wcs200.py new/OWSLib-0.21.0/owslib/coverage/wcs200.py --- old/OWSLib-0.20.0/owslib/coverage/wcs200.py 2020-03-29 03:39:03.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/coverage/wcs200.py 2020-11-30 17:09:17.000000000 +0100 @@ -130,6 +130,7 @@ resz=None, parameter=None, method="Get", + timeout=30, **kwargs ): """Request and return a coverage from the WCS as a file-like object @@ -213,7 +214,7 @@ data += param_list_to_url_string(sizes, 'size') log.debug("WCS 2.0.0 DEBUG: Second part of URL: %s" % data) - u = openURL(base_url, data, method, self.cookies, auth=self.auth) + u = openURL(base_url, data, method, self.cookies, auth=self.auth, timeout=timeout) return u def getOperationByName(self, name): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/coverage/wcs201.py new/OWSLib-0.21.0/owslib/coverage/wcs201.py --- old/OWSLib-0.20.0/owslib/coverage/wcs201.py 2020-03-29 03:39:03.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/coverage/wcs201.py 2020-11-30 17:09:17.000000000 +0100 @@ -130,6 +130,7 @@ resz=None, parameter=None, method="Get", + timeout=30, **kwargs ): """Request and return a coverage from the WCS as a file-like object @@ -214,7 +215,7 @@ log.debug("WCS 2.0.1 DEBUG: Second part of URL: %s" % data) - u = openURL(base_url, data, method, self.cookies, auth=self.auth) + u = openURL(base_url, data, method, self.cookies, auth=self.auth, timeout=timeout) return u def getOperationByName(self, name): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/feature/__init__.py new/OWSLib-0.21.0/owslib/feature/__init__.py --- old/OWSLib-0.20.0/owslib/feature/__init__.py 2020-06-05 13:05:14.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/feature/__init__.py 2020-11-30 17:09:17.000000000 +0100 @@ -5,12 +5,11 @@ # # ============================================================================= -from owslib.crs import Crs - from urllib.parse import urlencode -import logging +from owslib.crs import Crs from owslib.util import log, Authentication from owslib.feature.schema import get_schema +from owslib.feature.postrequest import PostRequest_1_1_0, PostRequest_2_0_0 class WebFeatureService_(object): @@ -77,6 +76,49 @@ srs.getcode(), ) + def getBBOXPost(self, bbox, typename): + """Format bounding box for Post requests + + @param bbox: (minx,miny,maxx,maxy[,srs]) + @type bbox: List + @param typename: feature name + @type typename: String + @returns: String properly formated according to version and + coordinate reference system + """ + srs = None + + # srs of the bbox is specified in the bbox as fifth paramter + if len(bbox) == 5: + srs = Crs(bbox[4]) + # take default srs + else: + srs = self.contents[typename[0]].crsOptions[0] + + formatted_bbox = [bbox[0], bbox[1], bbox[2], bbox[3]] + if self.version in ["1.1.0", "2.0.0"]: + if srs.axisorder == "yx" and srs.encoding == "urn": + formatted_bbox = [bbox[1], bbox[0], bbox[3], bbox[2]] + + if self.version == "1.1.0": + formatted_bbox.append(srs.getcodeurn()) + return formatted_bbox + if self.version == "2.0.0": + formatted_bbox.append(srs.getcodeuri1()) + return formatted_bbox + else: + formatted_bbox.append(srs.getcode()) + return formatted_bbox + + def create_post_request(self): + """Creates an xml POST request according to WFS version.""" + + if self.version in ['1.1.0']: + return PostRequest_1_1_0() + + if self.version in ['2.0', '2.0.0']: + return PostRequest_2_0_0() + def getSRS(self, srsname, typename): """Returns None or Crs object for given name @@ -206,6 +248,131 @@ return base_url + data + def getPOSTGetFeatureRequest( + self, + typename=None, + filter=None, + bbox=None, + featureid=None, + featureversion=None, + propertyname=None, + maxfeatures=None, + storedQueryID=None, + storedQueryParams=None, + outputFormat=None, + method="Post", + startindex=None, + sortby=None, + ): + """Formulate proper GetFeature request using KVP encoding + ---------- + typename : list + List of typenames (string) + filter : string + XML-encoded OGC filter expression. + bbox : tuple + (left, bottom, right, top) in the feature type's coordinates == (minx, miny, maxx, maxy) + featureid : list + List of unique feature ids (string) + featureversion : string + Default is most recent feature version. + propertyname : list + List of feature property names. Leave blank (None) to get all properties. + maxfeatures : int + Maximum number of features to be returned. + method : string + Qualified name of the HTTP DCP method to use. + outputFormat: string (optional) + Requested response format of the request. + startindex: int (optional) + Start position to return feature set (paging in combination with maxfeatures) + storedQueryID : string + A name identifying a prepared set available in WFS-service. + WFS version 2.0.0 and above only. + storedQueryParams : dict + Variable amount of extra information sent to server related to + storedQueryID to further define the requested data. + WFS version 2.0.0 and above only. + {'parameter_name': parameter_value} + sortby: list (optional) + List of property names whose values should be used to order + (upon presentation) the set of feature instances that + satify the query. + + There are 5 different modes of use + + 1) typename and bbox (simple spatial query) + 2) typename and filter (==query) (more expressive) + 3) featureid (direct access to known features) + 4) storedQueryID and optional storedQueryParams + 5) filter only via Post method + """ + + try: + base_url = next( + ( + m.get("url") + for m in self.getOperationByName("GetFeature").methods + if m.get("type").lower() == method.lower() + ) + ) + except StopIteration: + base_url = self.url + + if not typename and filter: + return base_url, filter + + request = self.create_post_request() + + if storedQueryID: + if self.version in ["1.0.0", "1.1.0"]: + log.warning("Stored queries are only supported in version 2.0.0 and above.") + return None + + storedQueryParams = storedQueryParams or {} + request.create_storedquery(storedQueryID, storedQueryParams) + data = request.to_string() + return base_url, data + + typename = ( + [typename] if isinstance(typename, str) else typename + ) # noqa: E721 + typenames = ",".join(typename) + + request.create_query(typenames) + + if featureid: + featureid = ( + [featureid] if isinstance(featureid, str) else featureid + ) + request.set_featureid(featureid) + elif bbox: + request.set_bbox(self.getBBOXPost(bbox, typename)) + elif filter: + request.set_filter(filter) + + if featureversion: + request.set_featureversion(str(featureversion)) + if maxfeatures: + request.set_maxfeatures(maxfeatures) + if outputFormat: + request.set_outputformat(outputFormat) + if propertyname: + propertyname = ( + [propertyname] if isinstance(propertyname, str) else propertyname + ) + request.set_propertyname(propertyname) + if sortby: + sortby = ( + [sortby] if isinstance(sortby, str) else sortby + ) + request.set_sortby(sortby) + if startindex: + request.set_startindex(startindex) + + data = request.to_string() + return base_url, data + def get_schema(self, typename): """ Get layer schema compatible with :class:`fiona` schema object diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/feature/postrequest.py new/OWSLib-0.21.0/owslib/feature/postrequest.py --- old/OWSLib-0.20.0/owslib/feature/postrequest.py 1970-01-01 01:00:00.000000000 +0100 +++ new/OWSLib-0.21.0/owslib/feature/postrequest.py 2020-11-30 17:09:17.000000000 +0100 @@ -0,0 +1,197 @@ +# owslib imports: +from owslib import util +from owslib.etree import etree +from owslib.namespaces import Namespaces + +n = Namespaces() +FES_NAMESPACE = n.get_namespace("fes") +GML_NAMESPACE = n.get_namespace("gml") +GML32_NAMESPACE = n.get_namespace("gml32") +OGC_NAMESPACE = n.get_namespace("ogc") +WFS_NAMESPACE = n.get_namespace("wfs") +WFS20_NAMESPACE = n.get_namespace("wfs20") + + +class PostRequest(): + """Superclass for POST request building""" + + def __init__(self, version=None, namespace=None): + self._root = etree.Element(util.nspath('GetFeature', namespace)) + self._root.set("service", "WFS") + self._root.set("version", version) + self._wfsnamespace = namespace + self._query = None + + def _create_query(self, typename): + self._query = etree.SubElement(self._root, util.nspath('Query', self._wfsnamespace)) + + def set_featureversion(self, version): + self._query.set("featureVersion", version) + + def set_propertyname(self, propertyname): + """Set which feature properties will be returned. + + If not set, will return all properties.""" + for pn in propertyname: + etree.SubElement(self._query, "PropertyName").text = pn + + def set_startindex(self, startindex): + """Set the starting index value for the request""" + self._root.set("startIndex", str(startindex)) + + def to_string(self): + """Returns the xml request in string format. + + Required in order to use the request with getfeature()""" + return etree.tostring(self._root) + + +class PostRequest_1_1_0(PostRequest): + """XML Post request payload builder for WFS version 1.1.0""" + + def __init__(self): + super().__init__(version='1.1.0', namespace=WFS_NAMESPACE) + + def create_query(self, typename): + """Creates the query tag with the corresponding typenames. + Required element for each request.""" + super()._create_query(typename) + self._query.set("typeName", typename) + + def set_bbox(self, bbox): + """Set a bbox filter. + + Cannot be used with set_featureid() or set_filter(). + """ + filter_tree = etree.SubElement(self._query, util.nspath('Filter', OGC_NAMESPACE)) + bbox_tree = etree.SubElement(filter_tree, util.nspath('BBOX', OGC_NAMESPACE)) + coords = etree.SubElement(bbox_tree, util.nspath('Envelope', GML_NAMESPACE)) + if len(bbox) > 4: + coords.set('srsName', bbox[4]) + lower = etree.SubElement(coords, util.nspath('lowerCorner', GML_NAMESPACE)) + lower.text = '{} {}'.format(bbox[0], bbox[1]) + + upper = etree.SubElement(coords, util.nspath('upperCorner', GML_NAMESPACE)) + upper.text = '{} {}'.format(bbox[2], bbox[3]) + + def set_featureid(self, featureid): + """Set filter by feature id. + + Cannot be used with set_bbox() or set_filter(). + """ + feature_tree = etree.SubElement(self._query, util.nspath('Filter', OGC_NAMESPACE)) + + for ft in featureid: + prop_id = etree.Element(util.nspath('GmlObjectId', OGC_NAMESPACE)) + prop_id.set(util.nspath('id', GML_NAMESPACE), ft) + feature_tree.append(prop_id) + + def set_filter(self, filter): + """Set filter from existing filter. + + Will integrate the filter tag of a provided xml filter to the query being built. + + Cannot be used with set_bbox() or set_featureid(). + """ + f = etree.fromstring(filter) + sub_elem = f.find(util.nspath("Filter", OGC_NAMESPACE)) + self._query.append(sub_elem) + + def set_maxfeatures(self, maxfeatures): + """Set the maximum number of features to be returned.""" + self._root.set("maxFeatures", str(maxfeatures)) + + def set_outputformat(self, outputFormat): + """Set the output format. + + Verify the available formats with a GetCapabilites request.""" + self._root.set("outputFormat", outputFormat) + + def set_sortby(self, sortby): + """Set the properties by which the response will be sorted.""" + sort_tree = etree.SubElement(self._query, util.nspath("SortBy", OGC_NAMESPACE)) + for s in sortby: + prop_elem = etree.SubElement(sort_tree, util.nspath("SortProperty", OGC_NAMESPACE)) + prop_name = etree.SubElement(prop_elem, util.nspath('PropertyName', OGC_NAMESPACE)) + prop_name.text = s + + +class PostRequest_2_0_0(PostRequest): + """XML Post request payload builder for WFS version 2.0.0.""" + + def __init__(self): + super().__init__(version='2.0.0', namespace=WFS20_NAMESPACE) + + def create_query(self, typename): + """Creates the query tag with the corresponding typenames. + Required element for each request ecept for stored queries.""" + super()._create_query(typename) + self._query.set("typenames", typename) + + def create_storedquery(self, stored_id, parameters): + """Create the storedQuery tag and configure it's sub elements and attributes.""" + storedquery = etree.SubElement(self._root, util.nspath('StoredQuery', self._wfsnamespace)) + storedquery.set("id", str(stored_id)) + for param in parameters: + p = etree.SubElement(storedquery, util.nspath('Parameter', self._wfsnamespace)) + p.set("name", param) + p.text = parameters[param] + + def set_bbox(self, bbox): + """Set a bbox filter. + + Cannot be used with set_featureid() or set_filter(). + """ + filter_tree = etree.SubElement(self._query, util.nspath('Filter', FES_NAMESPACE)) + bbox_tree = etree.SubElement(filter_tree, util.nspath('BBOX', FES_NAMESPACE)) + coords = etree.SubElement(bbox_tree, util.nspath('Envelope', GML32_NAMESPACE)) + if len(bbox) > 4: + coords.set('srsName', bbox[4]) + + lower = etree.SubElement(coords, util.nspath('lowerCorner', GML32_NAMESPACE)) + lower.text = '{} {}'.format(bbox[0], bbox[1]) + + upper = etree.SubElement(coords, util.nspath('upperCorner', GML32_NAMESPACE)) + upper.text = '{} {}'.format(bbox[2], bbox[3]) + + def set_featureid(self, featureid): + """Set filter by feature id. + + Cannot be used with set_bbox() or set_filter(). + """ + feature_tree = etree.SubElement(self._query, util.nspath('Filter', FES_NAMESPACE)) + for ft in featureid: + prop_id = etree.Element(util.nspath('ResourceId', FES_NAMESPACE)) + prop_id.set('rid', ft) + feature_tree.append(prop_id) + + def set_filter(self, filter): + """Set filter from existing filter. + + Will integrate the filter tag of a provided xml filter to the current one + being built. + + Cannot be used with set_bbox() or set_featureid(). + """ + f = etree.fromstring(filter) + sub_elem = f.find(util.nspath("Filter", FES_NAMESPACE)) + self._query.append(sub_elem) + + def set_maxfeatures(self, maxfeatures): + """Set the maximum number of features to be returned.""" + self._root.set("count", str(maxfeatures)) + + def set_outputformat(self, outputFormat): + """Set the output format. + + Verify the available formats with a GetCapabilites request. + """ + self._root.set("outputformat", outputFormat) + + def set_sortby(self, sortby): + """Set the properties by which the response will be sorted.""" + sort_tree = etree.SubElement(self._query, util.nspath("SortBy", FES_NAMESPACE)) + for s in sortby: + prop_elem = etree.SubElement(sort_tree, util.nspath("SortProperty", FES_NAMESPACE)) + value = etree.SubElement(prop_elem, util.nspath('ValueReference', FES_NAMESPACE)) + value.text = s diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/feature/schema.py new/OWSLib-0.21.0/owslib/feature/schema.py --- old/OWSLib-0.20.0/owslib/feature/schema.py 2020-04-23 13:17:57.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/feature/schema.py 2020-10-28 12:48:50.000000000 +0100 @@ -52,6 +52,8 @@ if ":" in typename: typename = typename.split(":")[1] type_element = root.find("./{%s}element" % XS_NAMESPACE) + if type_element is None: + return None complex_type = type_element.attrib["type"].split(":")[1] elements = _get_elements(complex_type, root) nsmap = None @@ -125,7 +127,8 @@ schema["geometry"] = mappings[data_type] schema["geometry_column"] = name else: - schema["properties"][name] = data_type.replace(schema_key + ":", "") + if schema_key is not None: + schema["properties"][name] = data_type.replace(schema_key + ":", "") if non_nillable: schema["required"].append(name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/feature/wfs110.py new/OWSLib-0.21.0/owslib/feature/wfs110.py --- old/OWSLib-0.20.0/owslib/feature/wfs110.py 2020-04-23 13:17:57.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/feature/wfs110.py 2020-11-30 17:09:17.000000000 +0100 @@ -220,7 +220,7 @@ bbox=None, featureid=None, featureversion=None, - propertyname="*", + propertyname=None, maxfeatures=None, srsname=None, outputFormat=None, @@ -243,7 +243,8 @@ featureversion : string Default is most recent feature version. propertyname : list - List of feature property names. '*' matches all. + List of feature property names. For Get request, '*' matches all. + For Post request, leave blank (None) to get all properties. maxfeatures : int Maximum number of features to be returned. method : string @@ -278,53 +279,72 @@ base_url = self.url request = {"service": "WFS", "version": self.version, "request": "GetFeature"} - if not isinstance(typename, list): - typename = [typename] + if method.lower() == "get": + if not isinstance(typename, list): + typename = [typename] + + if srsname is not None: + request["srsname"] = str(srsname) + + # Check, if desired SRS is supported by the service for each + # typename. Warning will be thrown if that SRS is not allowed." + for name in typename: + _ = self.getSRS(srsname, name) + + # check featureid + if featureid: + request["featureid"] = ",".join(featureid) + + # bbox + elif bbox and typename: + request["bbox"] = self.getBBOXKVP(bbox, typename) + + # or filter + elif filter and typename: + request["filter"] = str(filter) - if srsname is not None: - request["srsname"] = str(srsname) + assert len(typename) > 0 + request["typename"] = ",".join(typename) - # Check, if desired SRS is supported by the service for each - # typename. Warning will be thrown if that SRS is not allowed." - for name in typename: - _ = self.getSRS(srsname, name) + if propertyname is None: + propertyname = "*" - # check featureid - if featureid: - request["featureid"] = ",".join(featureid) - - # bbox - elif bbox and typename: - request["bbox"] = self.getBBOXKVP(bbox, typename) - - # or filter - elif filter and typename: - request["filter"] = str(filter) - - assert len(typename) > 0 - request["typename"] = ",".join(typename) - - if propertyname is not None: if not isinstance(propertyname, list): propertyname = [propertyname] request["propertyname"] = ",".join(propertyname) - if sortby is not None: - if not isinstance(sortby, list): - sortby = [sortby] - request["sortby"] = ",".join(sortby) - - if featureversion is not None: - request["featureversion"] = str(featureversion) - if maxfeatures is not None: - request["maxfeatures"] = str(maxfeatures) - if startindex is not None: - request["startindex"] = str(startindex) - if outputFormat is not None: - request["outputFormat"] = outputFormat + if sortby is not None: + if not isinstance(sortby, list): + sortby = [sortby] + request["sortby"] = ",".join(sortby) + + if featureversion is not None: + request["featureversion"] = str(featureversion) + if maxfeatures is not None: + request["maxfeatures"] = str(maxfeatures) + if startindex is not None: + request["startindex"] = str(startindex) + if outputFormat is not None: + request["outputFormat"] = outputFormat + + data = urlencode(request) + log.debug("Making request: %s?%s" % (base_url, data)) + + elif method.lower() == "post": + base_url, data = self.getPOSTGetFeatureRequest( + typename=typename, + filter=filter, + bbox=bbox, + featureid=featureid, + featureversion=featureversion, + propertyname=propertyname, + maxfeatures=maxfeatures, + outputFormat=outputFormat, + method='Post', + startindex=startindex, + sortby=sortby, + ) - data = urlencode(request) - log.debug("Making request: %s?%s" % (base_url, data)) u = openURL(base_url, data, method, timeout=self.timeout, headers=self.headers, auth=self.auth) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/feature/wfs200.py new/OWSLib-0.21.0/owslib/feature/wfs200.py --- old/OWSLib-0.20.0/owslib/feature/wfs200.py 2020-04-23 13:17:57.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/feature/wfs200.py 2020-11-30 17:09:17.000000000 +0100 @@ -234,7 +234,9 @@ sortby=None, ): """Request and return feature data as a file-like object. + #TODO: NOTE: have changed property name from ['*'] to None - check the use of this in WFS 2.0 + Parameters ---------- typename : list @@ -248,9 +250,16 @@ featureversion : string Default is most recent feature version. propertyname : list - List of feature property names. '*' matches all. + List of feature property names. For Get request, '*' matches all. + For Post request, leave blank (None) to get all properties. maxfeatures : int Maximum number of features to be returned. + storedQueryID : string + A name identifying a prepared set available in WFS-service + storedQueryParams : dict + Variable amount of extra information sent to server related to + storedQueryID to further define the requested data + {'parameter_name': parameter_value} method : string Qualified name of the HTTP DCP method to use. outputFormat: string (optional) @@ -262,11 +271,19 @@ (upon presentation) the set of feature instances that satify the query. - There are 3 different modes of use + There are 5 different modes of use 1) typename and bbox (simple spatial query) 2) typename and filter (==query) (more expressive) 3) featureid (direct access to known features) + 4) storedQueryID and optional storedQueryParams + 5) filter only via Post method + + Raises: + ServiceException: If there is an error during the request + + Returns: + BytesIO -- Data returned from the service as a file-like object """ storedQueryParams = storedQueryParams or {} url = data = None @@ -290,9 +307,21 @@ ) log.debug("GetFeature WFS GET url %s" % url) else: - (url, data) = self.getPOSTGetFeatureRequest() + url, data = self.getPOSTGetFeatureRequest( + typename, + filter, + bbox, + featureid, + featureversion, + propertyname, + maxfeatures, + storedQueryID, + storedQueryParams, + outputFormat, + "Post", + startindex, + sortby) - # If method is 'Post', data will be None here u = openURL(url, data, method, timeout=self.timeout, headers=self.headers, auth=self.auth) # check for service exceptions, rewrap, and return diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/map/wms111.py new/OWSLib-0.21.0/owslib/map/wms111.py --- old/OWSLib-0.20.0/owslib/map/wms111.py 2019-11-05 01:04:04.000000000 +0100 +++ new/OWSLib-0.21.0/owslib/map/wms111.py 2020-12-09 03:15:04.000000000 +0100 @@ -535,14 +535,18 @@ for s in elem.findall('Style'): name = s.find('Name') title = s.find('Title') + if name is None and title is None: + raise ValueError('%s missing name and title' % (s,)) if name is None or title is None: - raise ValueError('%s missing name or title' % (s,)) - style = {'title': title.text} + warnings.warn('%s missing name or title' % (s,)) + title_ = title.text if title is not None else name.text + name_ = name.text if name is not None else title.text + style = {'title': title_} # legend url legend = s.find('LegendURL/OnlineResource') if legend is not None: style['legend'] = legend.attrib['{http://www.w3.org/1999/xlink}href'] - self.styles[name.text] = style + self.styles[name_] = style # keywords self.keywords = [f.text for f in elem.findall('KeywordList/Keyword')] @@ -579,9 +583,11 @@ self.dataUrls = [] for m in elem.findall('DataURL'): dataUrl = { - 'format': m.find('Format').text.strip(), + 'format': testXMLValue(m.find('Format')), 'url': m.find('OnlineResource').attrib['{http://www.w3.org/1999/xlink}href'] } + if dataUrl['format']: + dataUrl['format'] = dataUrl['format'].strip() self.dataUrls.append(dataUrl) self.layers = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/map/wms130.py new/OWSLib-0.21.0/owslib/map/wms130.py --- old/OWSLib-0.20.0/owslib/map/wms130.py 2019-11-05 01:04:04.000000000 +0100 +++ new/OWSLib-0.21.0/owslib/map/wms130.py 2020-12-09 03:15:04.000000000 +0100 @@ -581,9 +581,13 @@ for s in elem.findall(nspath('Style', WMS_NAMESPACE)): name = s.find(nspath('Name', WMS_NAMESPACE)) title = s.find(nspath('Title', WMS_NAMESPACE)) + if name is None and title is None: + raise ValueError('%s missing name and title' % (s,)) if name is None or title is None: - raise ValueError('%s missing name or title' % (s,)) - style = {'title': title.text} + warnings.warn('%s missing name or title' % (s,)) + title_ = title.text if title is not None else name.text + name_ = name.text if name is not None else title.text + style = {'title': title_} # legend url legend = s.find(nspath('LegendURL/OnlineResource', WMS_NAMESPACE)) if legend is not None: @@ -599,7 +603,7 @@ lgd_format = lgd.find(nspath('Format', WMS_NAMESPACE)) if lgd_format is not None: style['legend_format'] = lgd_format.text.strip() - self.styles[name.text] = style + self.styles[name_] = style # keywords self.keywords = [f.text for f in elem.findall(nspath('KeywordList/Keyword', WMS_NAMESPACE))] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/ogcapi/__init__.py new/OWSLib-0.21.0/owslib/ogcapi/__init__.py --- old/OWSLib-0.20.0/owslib/ogcapi/__init__.py 2020-06-05 13:39:21.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/ogcapi/__init__.py 2020-12-03 12:53:17.000000000 +0100 @@ -123,7 +123,7 @@ path = 'collections' return self._request(path) - def collection(self, collection_id) -> dict: + def collection(self, collection_id: str) -> dict: """ implements /collections/{collectionId} @@ -136,7 +136,7 @@ path = 'collections/{}'.format(collection_id) return self._request(path) - def collection_queryables(self, collection_id) -> dict: + def collection_queryables(self, collection_id: str) -> dict: """ implements /collections/{collectionId}/queryables @@ -149,7 +149,7 @@ path = 'collections/{}/queryables'.format(collection_id) return self._request(path) - def _build_url(self, path=None) -> str: + def _build_url(self, path: str = None) -> str: """ helper function to build an OGC API URL @@ -171,12 +171,15 @@ return url - def _request(self, path=None, kwargs={}) -> dict: + def _request(self, path: str = None, as_dict: bool = True, + kwargs: dict = {}) -> dict: """ helper function for request/response patterns against OGC API endpoints @type path: string @param path: path of request + @type as_dict: bool + @param as_dict: whether to return JSON dict (default True) @type kwargs: string @param kwargs: ``dict`` of keyword value pair request parameters @@ -186,10 +189,17 @@ url = self._build_url(path) LOGGER.debug('Request: {}'.format(url)) + LOGGER.debug('Params: {}'.format(kwargs)) response = http_get(url, headers=self.headers, auth=self.auth, params=kwargs) + + LOGGER.debug('URL: {}'.format(response.url)) + if response.status_code != requests.codes.ok: raise RuntimeError(response.text) - return response.json() + if as_dict: + return response.json() + else: + return response.content diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/ogcapi/coverages.py new/OWSLib-0.21.0/owslib/ogcapi/coverages.py --- old/OWSLib-0.20.0/owslib/ogcapi/coverages.py 1970-01-01 01:00:00.000000000 +0100 +++ new/OWSLib-0.21.0/owslib/ogcapi/coverages.py 2020-12-02 18:21:37.000000000 +0100 @@ -0,0 +1,98 @@ +# ============================================================================= +# Copyright (c) 2020 Tom Kralidis +# +# Author: Tom Kralidis <tomkrali...@gmail.com> +# +# Contact email: tomkrali...@gmail.com +# ============================================================================= + +from io import BytesIO +import logging + +from owslib.ogcapi import API +from owslib.util import Authentication + +LOGGER = logging.getLogger(__name__) + + +class Coverages(API): + """Abstraction for OGC API - Coverages""" + + 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 coverages(self) -> dict: + """ + implements /collections filtered on coverages + + @returns: `dict` of filtered collections object + """ + + coverages_ = [] + collections_ = super().collections() + + for c_ in collections_['collections']: + for l_ in c_['links']: + if 'coverage' in l_['rel']: + coverages_.append(c_['id']) + break + + return coverages_ + + def coverage_domainset(self, collection_id: str, **kwargs: dict) -> dict: + """ + implements /collection/{collectionId}/coverage/domainset + + @type collection_id: string + @param collection_id: id of collection + + @returns: coverage domainset results + """ + + path = 'collections/{}/coverage/domainset'.format(collection_id) + return self._request(path=path, kwargs=kwargs) + + def coverage_rangetype(self, collection_id: str, **kwargs: dict) -> dict: + """ + implements /collection/{collectionId}/coverage/rangetype + + @type collection_id: string + @param collection_id: id of collection + + @returns: coverage rangetype results + """ + + path = 'collections/{}/coverage/rangetype'.format(collection_id) + return self._request(path=path, kwargs=kwargs) + + def coverage(self, collection_id: str, **kwargs: dict) -> dict: + """ + implements /collection/{collectionId}/coverage/ + + @type collection_id: string + @param collection_id: id of collection + @type range_subset: list + @param range_subset: range subset + @type subsets: list of tuples + @param subsets: [(name, lower bound, upper bound)] + + @returns: coverage data + """ + + kwargs_ = {} + + if 'range_subset' in kwargs: + kwargs_['rangeSubset'] = ','.join( + [str(x) for x in kwargs['range_subset']]) + + if 'subsets' in kwargs: + kwargs_['subset'] = [] + for s in kwargs['subsets']: + val = '{}({},{})'.format(s[0], s[1], s[2]) + kwargs_['subset'].append(val) + + path = 'collections/{}/coverage'.format(collection_id) + + return BytesIO(self._request(path=path, as_dict=False, kwargs=kwargs_)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/ogcapi/features.py new/OWSLib-0.21.0/owslib/ogcapi/features.py --- old/OWSLib-0.20.0/owslib/ogcapi/features.py 2020-06-05 13:39:21.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/ogcapi/features.py 2020-12-02 18:21:37.000000000 +0100 @@ -22,6 +22,22 @@ __doc__ = API.__doc__ # noqa super().__init__(url, json_, timeout, headers, auth) + def feature_collections(self) -> dict: + """ + implements /collections filtered on features + + @returns: `dict` of filtered collections object + """ + + features_ = [] + collections_ = super().collections() + + for c_ in collections_['collections']: + if 'itemType' in c_ and c_['itemType'].lower() == 'feature': + features_.append(c_['id']) + + return features_ + def collection_items(self, collection_id: str, **kwargs: dict) -> dict: """ implements /collection/{collectionId}/items @@ -46,7 +62,7 @@ kwargs['bbox'] = ','.join(kwargs['bbox']) path = 'collections/{}/items'.format(collection_id) - return self._request(path, kwargs) + return self._request(path=path, kwargs=kwargs) def collection_item(self, collection_id: str, identifier: str) -> dict: """ @@ -61,4 +77,4 @@ """ path = 'collections/{}/items/{}'.format(collection_id, identifier) - return self._request(path) + return self._request(path=path) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/ogcapi/records.py new/OWSLib-0.21.0/owslib/ogcapi/records.py --- old/OWSLib-0.20.0/owslib/ogcapi/records.py 2020-06-05 13:39:21.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/ogcapi/records.py 2020-10-12 13:18:50.000000000 +0200 @@ -21,3 +21,19 @@ headers: dict = None, auth: Authentication = None): __doc__ = Features.__doc__ # noqa super().__init__(url, json_, timeout, headers, auth) + + def records(self) -> dict: + """ + implements /collections filtered on records + + @returns: `dict` of filtered collections object + """ + + records_ = [] + collections_ = super().collections() + + for c_ in collections_['collections']: + if 'itemType' in c_ and c_['itemType'].lower() == 'record': + records_.append(c_['id']) + + return records_ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/util.py new/OWSLib-0.21.0/owslib/util.py --- old/OWSLib-0.20.0/owslib/util.py 2020-05-21 03:02:42.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/util.py 2020-12-09 03:15:04.000000000 +0100 @@ -292,7 +292,7 @@ # We can't re-add an existing namespaces. Get a list of current # namespaces in use existing_namespaces = set() - for elem in root.getiterator(): + for elem in root.iter(): if elem.tag[0] == "{": uri, tag = elem.tag[1:].split("}") existing_namespaces.add(namespaces.get_namespace_from_url(uri)) @@ -591,6 +591,10 @@ def getTypedValue(data_type, value): '''Utility function to cast a string value to the appropriate XSD type. ''' + # If the default value is empty + if value is None: + return + if data_type == 'boolean': return True if value.lower() == 'true' else False elif data_type == 'integer': diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/owslib/wps.py new/OWSLib-0.21.0/owslib/wps.py --- old/OWSLib-0.20.0/owslib/wps.py 2020-05-20 17:00:56.000000000 +0200 +++ new/OWSLib-0.21.0/owslib/wps.py 2020-12-03 12:53:17.000000000 +0100 @@ -1398,10 +1398,7 @@ self.dataType = literalDataElement.get('dataType') if literalDataElement.text is not None and literalDataElement.text.strip() != '': self.data.append(literalDataElement.text.strip()) - bboxDataElement = dataElement.find(nspath('BoundingBox', ns=namespaces['ows'])) - if bboxDataElement is not None: - # TODO: just a workaround for data-inputs in lineage - bboxDataElement = dataElement.find(nspath('BoundingBoxData', ns=namespaces['wps'])) + bboxDataElement = dataElement.find(nspath('BoundingBoxData', ns=namespaces['wps'])) if bboxDataElement is not None: self.dataType = "BoundingBoxData" bbox = BoundingBox(bboxDataElement) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OWSLib-0.20.0/setup.py new/OWSLib-0.21.0/setup.py --- old/OWSLib-0.20.0/setup.py 2020-03-29 03:39:03.000000000 +0200 +++ new/OWSLib-0.21.0/setup.py 2020-10-12 13:18:50.000000000 +0200 @@ -20,6 +20,7 @@ version = owslib.__version__, description = 'OGC Web Service utility library', long_description = readme, + long_description_content_type = 'text/x-rst', license = 'BSD', keywords = 'gis ogc iso 19115 fgdc dif ows wfs wms sos csw wps wcs capabilities metadata wmts', author = 'Sean Gillies', @@ -36,8 +37,13 @@ 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Scientific/Engineering :: GIS', ], )