Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add POST support to WFS GetFeature (Issue #439) #706

Merged
merged 27 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ce030f7
adds quick workaround to make filter post request
f-PLT Jul 22, 2020
1343e57
adds working draft of getPOSTGetFeatureRequest
f-PLT Aug 10, 2020
87dead0
Adds documentation and unimplemented args
f-PLT Aug 10, 2020
c5241d1
Adds post for wfs 1.1.0 + cleanup
f-PLT Aug 10, 2020
dbd0b78
Refactors __init__ for PostRequest
f-PLT Aug 12, 2020
8e4660a
removes print command
f-PLT Aug 12, 2020
4c4ae39
adds unit tests for PostRequest_1_1_0
f-PLT Aug 13, 2020
e6c7e1c
adds unit test for PostRequest_2_0_0
f-PLT Aug 13, 2020
f618a7c
add fixtures to postrequest tests
f-PLT Aug 13, 2020
942129c
reformat of __init__ and typo fix
f-PLT Aug 13, 2020
48c24b4
Formatting
f-PLT Aug 13, 2020
b771823
fix set_featureid according to standard
f-PLT Sep 10, 2020
2f10b50
Add bbox formating function for post
f-PLT Sep 10, 2020
b339ef5
fix list args of getPOSTGetFeatureRequest
f-PLT Sep 10, 2020
d612439
Removes repetitive condition
f-PLT Sep 10, 2020
b56a1c1
Merge branch 'master' of github.com:geopython/OWSLib into wfs-post-ge…
f-PLT Sep 10, 2020
2e33184
Removes unused imports
f-PLT Sep 14, 2020
6a7735e
Add docstring to postrequest.py
f-PLT Sep 15, 2020
a688027
Add featureversion to postrequest.py
f-PLT Sep 17, 2020
e9bd373
Add storedquery to postrequest.py + refactor
f-PLT Sep 17, 2020
f96da4e
Remove spaces from tag names
f-PLT Sep 17, 2020
91fc15f
add test for stored query
f-PLT Sep 22, 2020
17e9e5d
add check to stored query for version
f-PLT Sep 22, 2020
4943782
Modified docstring for storedQueries
f-PLT Sep 23, 2020
c64de7c
Moved propertyname='*' from def to inside method
f-PLT Nov 24, 2020
9ba9b1f
Removed uncessary conditional check
f-PLT Nov 24, 2020
8fb987b
Change docstring for better wording
f-PLT Nov 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 170 additions & 3 deletions owslib/feature/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -77,6 +76,49 @@ def getBBOXKVP(self, bbox, typename):
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

Expand Down Expand Up @@ -206,6 +248,131 @@ def getGETGetFeatureRequest(

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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not understand why using a create_post_request function instead of directly inserting the version-dependent Postrequest object creation. Is this function intended to be used anywhere else ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my aim here was to make the post request creation more testable. It being separated makes it testable by itself.

I chose to make a factory with create_post_request(), so getPOSTGetFeatureRequest() can be used for wfs1.1.0 and 2.0.0 without needing another function that does almost the same thing but not quite, which also seems to be what getGetGetFeatureRequest() is aiming to become.

So this way, getFeature(), whether it's with 1.1.0 or 2.0.0, calls the same function, which checks the WFS version, and builds the appropriate request format. It will also make it easier (I hope) to integrate the next WFS version by simply extending the postrequest.py module so it can format post requests for that new version. All without needing to change getPOSTGetFeatureRequest() or creating a new function.

On the plus side, postrequest.py can also be used by itself to build a request and use it with resquests.post(), which could be useful in identifying if an error is caused by the request itself or if the problem is somewhere else in OWSLib.


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
Expand Down
197 changes: 197 additions & 0 deletions owslib/feature/postrequest.py
Original file line number Diff line number Diff line change
@@ -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
Loading