Skip to content

Commit ec6f6b5

Browse files
authored
OGC API - Processes: BoundingBox, Complex and reference Input/Output + tests (#614)
* Fix BoundingBox JSON input * inputs.py - support json data input inside a json * pywps/inout/basic.py - support json data when saving to input data file * docs/api_rest.rst - add docs for reference and Complex, BoundingBox, and reference inputs * WPSRequest.py - fix complex input * pywps/inout/basic.py - IOHandler.data - support storage json data from loaded via a reference * pywps/inout/outputs.py - support JSON ComplexOutput in JSON format * pywps/response/execute.py - support BoundingBoxOutput in JSON format * tests/test_execute.py - test JSON: In/Out BondingBox, In/Out Complex (GeoJSON), In Reference (GeoJSON) * WPSRequest.py, execute.py - add http_request to preprocess (as **kwargs) * pywps/app/WPSRequest.py - remove a deprecated unused import
1 parent 8624d4d commit ec6f6b5

7 files changed

Lines changed: 210 additions & 56 deletions

File tree

docs/api_rest.rst

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,76 @@ POST Execute Response:
330330
Hello Dude
331331
332332
333-
The example above only shows `LiteralInput`.
333+
Example for a reference input:
334+
335+
.. code-block::
336+
337+
"raster": {
338+
"type": "reference",
339+
"href": "file:./path/to/data/data.tif"
340+
}
341+
342+
Example for a BoundingBox input:
343+
(bbox default axis order is yx (EPSG:4326), i.e. miny, minx, maxy, maxx)
344+
345+
.. code-block::
346+
347+
"extent": {
348+
"type": "bbox",
349+
"bbox": [32, 34.7, 32.1, 34.8]
350+
}
351+
352+
353+
Example for a ComplexInput input:
354+
(the data is a standard GeoJSON)
355+
356+
.. code-block::
357+
358+
"cutline": {
359+
"type": "complex",
360+
"data": {
361+
"type": "FeatureCollection",
362+
"name": "Center",
363+
"crs": {
364+
"type": "name",
365+
"properties": {
366+
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
367+
}
368+
},
369+
"features": [
370+
{
371+
"type": "Feature",
372+
"properties": {},
373+
"geometry": {
374+
"type": "Polygon",
375+
"coordinates": [
376+
[
377+
[
378+
34.76844787397541,
379+
32.07247233606565
380+
],
381+
[
382+
34.78658619364754,
383+
32.07260143442631
384+
],
385+
[
386+
34.77780750512295,
387+
32.09532274590172
388+
],
389+
[
390+
34.76844787397541,
391+
32.07247233606565
392+
]
393+
]
394+
]
395+
}
396+
}
397+
]
398+
}
399+
}
400+
401+
402+
The examples above show some `Literal`, 'Complex', `BoundingBox` inputs.
334403
Internally, PyWPS always keeps the inputs in JSON formats (also in previous versions)
335404
So potentially all input types that are supported in XML should also be supported in JSON,
336405
though only a small subset of them were tested in this preliminary implementation.

pywps/app/WPSRequest.py

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
##################################################################
55

66
import logging
7+
78
import lxml
89
import lxml.etree
910
from werkzeug.exceptions import MethodNotAllowed
@@ -141,7 +142,7 @@ def _post_request(self):
141142
jdoc['default_mimetype'] = self.default_mimetype
142143

143144
if self.preprocess_request is not None:
144-
jdoc = self.preprocess_request(jdoc)
145+
jdoc = self.preprocess_request(jdoc, http_request=self.http_request)
145146
self.json = jdoc
146147

147148
version = jdoc.get('version')
@@ -618,34 +619,20 @@ def get_inputs_from_json(jdoc):
618619
if not isinstance(inpt_def, dict):
619620
inpt_def = {"data": inpt_def}
620621
data_type = inpt_def.get('type', 'literal')
622+
inpt = {'identifier': identifier}
621623
if data_type == 'literal':
622-
inpt = {}
623-
inpt['identifier'] = identifier
624624
inpt['data'] = inpt_def.get('data')
625625
inpt['uom'] = inpt_def.get('uom', '')
626626
inpt['datatype'] = inpt_def.get('datatype', '')
627627
the_inputs[identifier].append(inpt)
628-
continue
629-
630-
if data_type == 'complex':
631-
inpt = {}
632-
inpt['identifier'] = identifier
628+
elif data_type == 'complex':
633629
inpt['mimeType'] = inpt_def.get('mimeType', None)
634630
inpt['encoding'] = inpt_def.get('encoding', '').lower()
635631
inpt['schema'] = inpt_def.get('schema', '')
636632
inpt['method'] = inpt_def.get('method', 'GET')
637-
# if len(complex_data_el.getchildren()) > 0:
638-
# value_el = complex_data_el[0]
639-
# inpt['data'] = _get_dataelement_value(value_el)
640-
# else:
641-
if True:
642-
inpt['data'] = _get_rawvalue_value(inpt_def, inpt['encoding'])
633+
inpt['data'] = _get_rawvalue_value(inpt_def.get('data', ''), inpt['encoding'])
643634
the_inputs[identifier].append(inpt)
644-
continue
645-
646-
if data_type == 'reference':
647-
inpt = {}
648-
inpt['identifier'] = identifier
635+
elif data_type == 'reference':
649636
inpt[identifier] = inpt_def
650637
inpt['href'] = inpt_def.get('href', '')
651638
inpt['mimeType'] = inpt_def.get('mimeType', None)
@@ -654,17 +641,11 @@ def get_inputs_from_json(jdoc):
654641
inpt['body'] = inpt_def.get('body', '')
655642
inpt['bodyreference'] = inpt_def.get('bodyreference', '')
656643
the_inputs[identifier].append(inpt)
657-
continue
658-
659-
if data_type == 'bbox':
660-
# Using OWSlib BoundingBox
661-
from owslib.ows import BoundingBox
662-
bbox_datas = inpt_def
663-
for bbox_data in bbox_datas:
664-
bbox_data_el = bbox_data
665-
bbox = BoundingBox(bbox_data_el)
666-
the_inputs[identifier].append(bbox)
667-
LOGGER.debug("parse bbox: {},{},{},{}".format(bbox.minx, bbox.miny, bbox.maxx, bbox.maxy))
644+
elif data_type == 'bbox':
645+
inpt['data'] = inpt_def['bbox']
646+
inpt['crs'] = inpt_def.get('crs', 'urn:ogc:def:crs:EPSG::4326')
647+
inpt['dimensions'] = inpt_def.get('dimensions', 2)
648+
the_inputs[identifier].append(inpt)
668649
return the_inputs
669650

670651

pywps/inout/basic.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright 2018 Open Source Geospatial Foundation and others #
33
# licensed under MIT, Please consult LICENSE.txt for details #
44
##################################################################
5+
import json
56
from pathlib import PurePath
67

78
from pywps.inout.formats import Supported_Formats
@@ -319,6 +320,14 @@ def data(self):
319320
WARNING: may be bytes or str"""
320321
return self._iohandler.data
321322

323+
def data_as_json(self):
324+
# applies json.loads if needed
325+
data = self._iohandler.data
326+
if data and not isinstance(self._iohandler, DataHandler) and self.extension in ['.geojson', 'json']:
327+
data = json.loads(data)
328+
self.data = data # switch to a DataHandler
329+
return data
330+
322331
@data.setter
323332
def data(self, value):
324333
self._iohandler = DataHandler(value, self)
@@ -463,7 +472,10 @@ def file(self):
463472
openmode = self._openmode(self.data)
464473
kwargs = {} if 'b' in openmode else {'encoding': 'utf8'}
465474
with open(self._file, openmode, **kwargs) as fh:
466-
fh.write(self.data)
475+
if isinstance(self.data, (bytes, str)):
476+
fh.write(self.data)
477+
else:
478+
json.dump(self.data, fh)
467479

468480
return self._file
469481

@@ -646,7 +658,7 @@ def __init__(self, identifier, title=None, abstract=None, keywords=None,
646658
self.title = title
647659
self.abstract = abstract
648660
self.keywords = keywords
649-
self.min_occurs = int(min_occurs)
661+
self.min_occurs = int(min_occurs) if min_occurs is not None else 0
650662
self.max_occurs = int(max_occurs) if max_occurs is not None else None
651663
self.metadata = metadata
652664
self.translations = lower_case_dict(translations)

pywps/inout/inputs.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,16 @@ def json(self):
8383
def from_json(cls, json_input):
8484
instance = cls(
8585
identifier=json_input['identifier'],
86-
title=json_input['title'],
87-
abstract=json_input['abstract'],
88-
crss=json_input['crss'],
89-
keywords=json_input['keywords'],
86+
title=json_input.get('title'),
87+
abstract=json_input.get('abstract'),
88+
crss=json_input.get('crss'),
89+
keywords=json_input.get('keywords'),
9090
metadata=[Metadata.from_json(data) for data in json_input.get('metadata', [])],
91-
dimensions=json_input['dimensions'],
92-
workdir=json_input['workdir'],
93-
mode=json_input['mode'],
94-
min_occurs=json_input['min_occurs'],
95-
max_occurs=json_input['max_occurs'],
91+
dimensions=json_input.get('dimensions'),
92+
workdir=json_input.get('workdir'),
93+
mode=json_input.get('mode'),
94+
min_occurs=json_input.get('min_occurs'),
95+
max_occurs=json_input.get('max_occurs'),
9696
translations=json_input.get('translations'),
9797
)
9898
instance.data = json_input['bbox']
@@ -221,9 +221,10 @@ def from_json(cls, json_input):
221221
elif json_input.get('data'):
222222
data = json_input['data']
223223
# remove cdata tag if it exists (issue #553)
224-
match = CDATA_PATTERN.match(data)
225-
if match:
226-
data = match.group(1)
224+
if isinstance(data, str):
225+
match = CDATA_PATTERN.match(data)
226+
if match:
227+
data = match.group(1)
227228
instance.data = data
228229

229230
return instance

pywps/inout/outputs.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from pywps.inout.types import Translations
1818
from pywps.validator.mode import MODE
1919
from pywps import configuration as config
20-
from pywps.inout.formats import Format, Supported_Formats
20+
from pywps.inout.formats import Format, Supported_Formats, FORMATS
2121

2222

2323
class BoundingBoxOutput(basic.BBoxOutput):
@@ -213,14 +213,12 @@ def _json_reference(self, data):
213213
def _json_data(self, data):
214214
"""Return Data node
215215
"""
216-
# Match only data that are safe CDATA pattern.
217-
CDATA_PATTERN = re.compile(r'^<!\[CDATA\[((?!\]\]>).)*\]\]>$')
218-
219216
data["type"] = "complex"
220217

221218
if self.data:
222-
223-
if self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
219+
if self.data_format.mime_type in [FORMATS.GEOJSON.mime_type, FORMATS.JSON.mime_type]:
220+
data["data"] = self.data
221+
elif self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
224222
# Note that in a client-server round trip, the original and returned file will not be identical.
225223
data_doc = etree.parse(self.file)
226224
data["data"] = etree.tostring(data_doc, pretty_print=True).decode('utf-8')
@@ -229,6 +227,9 @@ def _json_data(self, data):
229227
if self.data_format.encoding == 'base64':
230228
data["data"] = self.base64.decode('utf-8')
231229
else:
230+
# Match only data that are safe CDATA pattern.
231+
CDATA_PATTERN = re.compile(r'^<!\[CDATA\[((?!\]\]>).)*\]\]>$')
232+
232233
# Otherwise we assume all other formats are unsafe and need to be enclosed in a CDATA tag.
233234
if isinstance(self.data, bytes):
234235
# Try to inline data as text but if fail encode is in base64

pywps/response/execute.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,25 @@ def _render_json_response(jdoc):
196196
response = dict()
197197
response['status'] = jdoc['status']
198198
out = jdoc['process']['outputs']
199-
response['outputs'] = {val['identifier']: val['data'] for val in out if ('identifier' in val and 'data' in val)}
199+
d = {}
200+
for val in out:
201+
id = val.get('identifier')
202+
if id is None:
203+
continue
204+
type = val.get('type')
205+
key = 'bbox' if type == 'bbox' else 'data'
206+
if key in val:
207+
d[id] = val[key]
208+
response['outputs'] = d
200209
return response
201210

202211
def _construct_doc(self):
203212
if self.status == WPS_STATUS.SUCCEEDED and \
204213
hasattr(self.wps_request, 'preprocess_response') and \
205214
self.wps_request.preprocess_response:
206-
self.outputs = self.wps_request.preprocess_response(self.outputs)
215+
self.outputs = self.wps_request.preprocess_response(self.outputs,
216+
request=self.wps_request,
217+
http_request=self.wps_request.http_request)
207218
doc = self.json
208219
try:
209220
json_response, mimetype = get_response_type(

0 commit comments

Comments
 (0)