Skip to content

Commit 9402444

Browse files
huardcehbrecht
authored andcommitted
Io tests (#444)
* created tests for io data round-trip. simplified json output logic. * pep8 * added other option to create request * reintroduced CDATA tag for types other than xml or base64 * renamed test file * remove cdata for base64
1 parent c351a11 commit 9402444

7 files changed

Lines changed: 191 additions & 33 deletions

File tree

pywps/inout/formats/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,11 @@ def json(self, jsonin):
161161
FORMATS = _FORMATS(
162162
Format('application/vnd.geo+json', extension='.geojson'),
163163
Format('application/json', extension='.json'),
164-
Format('application/x-zipped-shp', extension='.zip'),
164+
Format('application/x-zipped-shp', extension='.zip', encoding='base64'),
165165
Format('application/gml+xml', extension='.gml'),
166166
Format('application/vnd.google-earth.kml+xml', extension='.kml'),
167-
Format('application/vnd.google-earth.kmz', extension='.kmz'),
168-
Format('image/tiff; subtype=geotiff', extension='.tiff'),
167+
Format('application/vnd.google-earth.kmz', extension='.kmz', encoding='base64'),
168+
Format('image/tiff; subtype=geotiff', extension='.tiff', encoding='base64'),
169169
Format('application/x-ogc-wcs', extension='.xml'),
170170
Format('application/x-ogc-wcs; version=1.0.0', extension='.xml'),
171171
Format('application/x-ogc-wcs; version=1.1.0', extension='.xml'),

pywps/inout/inputs.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,20 +134,25 @@ def _json_data(self, data):
134134

135135
data["type"] = "complex"
136136

137-
try:
138-
data_doc = etree.parse(self.file)
139-
data["data"] = etree.tostring(data_doc, pretty_print=True).decode("utf-8")
140-
except Exception:
137+
if self.data:
141138

142-
if self.data:
143-
if isinstance(self.data, six.string_types):
139+
if self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
140+
# Note that in a client-server round trip, the original and returned file will not be identical.
141+
data_doc = etree.parse(self.file)
142+
data["data"] = etree.tostring(data_doc, pretty_print=True).decode('utf-8')
143+
144+
else:
145+
if self.data_format.encoding == 'base64':
146+
data["data"] = self.base64.decode('utf-8')
147+
148+
else:
149+
# Otherwise we assume all other formats are unsafe and need to be enclosed in a CDATA tag.
144150
if isinstance(self.data, bytes):
145-
data["data"] = self.data.decode("utf-8")
151+
out = self.data.encode(self.data_format.encoding or 'utf-8')
146152
else:
147-
data["data"] = self.data
153+
out = self.data
148154

149-
else:
150-
data["data"] = etree.tostring(etree.CDATA(self.base64))
155+
data["data"] = u'<![CDATA[{}]]>'.format(out)
151156

152157
return data
153158

pywps/inout/outputs.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -123,29 +123,25 @@ def _json_data(self, data):
123123

124124
data["type"] = "complex"
125125

126-
try:
127-
data_doc = etree.parse(self.file)
128-
data["data"] = etree.tostring(data_doc, pretty_print=True).decode("utf-8")
129-
except Exception:
130-
131-
if self.data:
132-
# XML compatible formats don't have to be wrapped in a CDATA tag.
133-
if self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
134-
fmt = "{}"
135-
else:
136-
fmt = "<![CDATA[{}]]>"
126+
if self.data:
127+
128+
if self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
129+
# Note that in a client-server round trip, the original and returned file will not be identical.
130+
data_doc = etree.parse(self.file)
131+
data["data"] = etree.tostring(data_doc, pretty_print=True).decode('utf-8')
137132

133+
else:
138134
if self.data_format.encoding == 'base64':
139-
data["data"] = fmt.format(etree.CDATA(self.base64))
135+
data["data"] = self.base64.decode('utf-8')
140136

141-
elif isinstance(self.data, six.string_types):
137+
else:
138+
# Otherwise we assume all other formats are unsafe and need to be enclosed in a CDATA tag.
142139
if isinstance(self.data, bytes):
143-
data["data"] = fmt.format(self.data.decode("utf-8"))
140+
out = self.data.encode(self.data_format.encoding or 'utf-8')
144141
else:
145-
data["data"] = fmt.format(self.data)
142+
out = self.data
146143

147-
else:
148-
raise NotImplementedError
144+
data["data"] = u'<![CDATA[{}]]>'.format(out)
149145

150146
return data
151147

pywps/templates/1.0.0/execute/main.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<ows:Title>{{ input.title }}</ows:Title>
4040
{% if input.type == "complex" %}
4141
<wps:Data>
42-
<wps:ComplexData mimeType="{{ input.mimetype }}" encoding="{{ input.encoding }}" schema="{{ input.schema }}">{{ input.data }}</wps:ComplexData>
42+
<wps:ComplexData mimeType="{{ input.mimetype }}" encoding="{{ input.encoding }}" schema="{{ input.schema }}">{{ input.data | safe }}</wps:ComplexData>
4343
</wps:Data>
4444
{% elif input.type == "literal" %}
4545
<wps:Data>

tests/data/text/unsafe.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
< Bunch of characters that would break XML <> & "" '

tests/test_complexdata_io.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Test embedding different file formats and different encodings within the <Data> tag."""
2+
3+
import unittest
4+
import os
5+
from pywps import get_ElementMakerForVersion, E
6+
from pywps.app.basic import get_xpath_ns
7+
from pywps import Service, Process, ComplexInput, ComplexOutput, FORMATS
8+
from pywps.tests import client_for, assert_response_success
9+
from owslib.wps import WPSExecution, ComplexDataInput
10+
from lxml import etree
11+
12+
VERSION = "1.0.0"
13+
WPS, OWS = get_ElementMakerForVersion(VERSION)
14+
xpath_ns = get_xpath_ns(VERSION)
15+
16+
17+
def get_resource(path):
18+
return os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data', path)
19+
20+
21+
test_fmts = {'json': (get_resource('json/point.geojson'), FORMATS.JSON),
22+
'geojson': (get_resource('json/point.geojson'), FORMATS.GEOJSON),
23+
'netcdf': (get_resource('netcdf/time.nc'), FORMATS.NETCDF),
24+
'geotiff': (get_resource('geotiff/dem.tiff'), FORMATS.GEOTIFF),
25+
'gml': (get_resource('gml/point.gml'), FORMATS.GML),
26+
'shp': (get_resource('shp/point.shp.zip'), FORMATS.SHP),
27+
'txt': (get_resource('text/unsafe.txt'), FORMATS.TEXT),
28+
}
29+
30+
31+
def create_fmt_process(name, fn, fmt):
32+
"""Create a dummy process comparing the input file on disk and the data that was passed in the request."""
33+
def handler(request, response):
34+
# Load output from file and convert to data
35+
response.outputs['complex'].file = fn
36+
o = response.outputs['complex'].data
37+
38+
# Get input data from the request
39+
i = request.inputs['complex'][0].data
40+
41+
assert i == o
42+
return response
43+
44+
return Process(handler=handler,
45+
identifier='test-fmt',
46+
title='Complex fmt test process',
47+
inputs=[ComplexInput('complex', 'Complex input',
48+
supported_formats=(fmt, ))],
49+
outputs=[ComplexOutput('complex', 'Complex output',
50+
supported_formats=(fmt, ))])
51+
52+
53+
def get_data(fn, encoding=None):
54+
"""Read the data from file and encode."""
55+
import base64
56+
mode = 'rb' if encoding == 'base64' else 'r'
57+
with open(fn, mode) as fp:
58+
data = fp.read()
59+
60+
if encoding == 'base64':
61+
data = base64.b64encode(data)
62+
63+
if isinstance(data, bytes):
64+
return data.decode('utf-8')
65+
else:
66+
return data
67+
68+
69+
class RawInput(unittest.TestCase):
70+
71+
def make_request(self, name, fn, fmt):
72+
"""Create XML request embedding encoded data."""
73+
data = get_data(fn, fmt.encoding)
74+
75+
doc = WPS.Execute(
76+
OWS.Identifier('test-fmt'),
77+
WPS.DataInputs(
78+
WPS.Input(
79+
OWS.Identifier('complex'),
80+
WPS.Data(
81+
WPS.ComplexData(data, mimeType=fmt.mime_type, encoding=fmt.encoding)))),
82+
version='1.0.0')
83+
84+
return doc
85+
86+
def compare_io(self, name, fn, fmt):
87+
"""Start the dummy process, post the request and check the response matches the input data."""
88+
89+
# Note that `WPSRequest` calls `get_inputs_from_xml` which converts base64 input to bytes
90+
# See `_get_rawvalue_value`
91+
client = client_for(Service(processes=[create_fmt_process(name, fn, fmt)]))
92+
data = get_data(fn, fmt.encoding)
93+
94+
wps = WPSExecution()
95+
doc = wps.buildRequest('test-fmt',
96+
inputs=[('complex', ComplexDataInput(data, mimeType=fmt.mime_type,
97+
encoding=fmt.encoding))],
98+
mode='sync')
99+
resp = client.post_xml(doc=doc)
100+
assert_response_success(resp)
101+
wps.parseResponse(resp.xml)
102+
out = wps.processOutputs[0].data[0]
103+
104+
if 'gml' in fmt.mime_type:
105+
xml_orig = etree.tostring(etree.fromstring(data.encode('utf-8'))).decode('utf-8')
106+
xml_out = etree.tostring(etree.fromstring(out.decode('utf-8'))).decode('utf-8')
107+
# Not equal because the output includes additional namespaces compared to the origin.
108+
# self.assertEqual(xml_out, xml_orig)
109+
110+
else:
111+
self.assertEqual(out.strip(), data.strip())
112+
113+
def test_json(self):
114+
key = 'json'
115+
self.compare_io(key, *test_fmts[key])
116+
117+
def test_geojson(self):
118+
key = 'geojson'
119+
self.compare_io(key, *test_fmts[key])
120+
121+
def test_geotiff(self):
122+
key = 'geotiff'
123+
self.compare_io(key, *test_fmts[key])
124+
125+
def test_netcdf(self):
126+
key = 'netcdf'
127+
self.compare_io(key, *test_fmts[key])
128+
129+
def test_gml(self):
130+
key = 'gml'
131+
self.compare_io(key, *test_fmts[key])
132+
133+
def test_shp(self):
134+
key = 'shp'
135+
self.compare_io(key, *test_fmts[key])
136+
137+
def test_txt(self):
138+
key = 'txt'
139+
self.compare_io(key, *test_fmts[key])

tests/test_execute.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,29 @@ def _handler(request, response):
165165

166166

167167
def get_output(doc):
168+
"""Return the content of LiteralData, Reference or ComplexData."""
169+
168170
output = {}
169171
for output_el in xpath_ns(doc, '/wps:ExecuteResponse'
170172
'/wps:ProcessOutputs/wps:Output'):
171173
[identifier_el] = xpath_ns(output_el, './ows:Identifier')
172-
[value_el] = xpath_ns(output_el, './wps:Data/wps:LiteralData')
173-
output[identifier_el.text] = value_el.text
174+
175+
lit_el = xpath_ns(output_el, './wps:Data/wps:LiteralData')
176+
if lit_el != []:
177+
output[identifier_el.text] = lit_el[0].text
178+
179+
ref_el = xpath_ns(output_el, './wps:Reference')
180+
if ref_el != []:
181+
output[identifier_el.text] = ref_el[0].attrib['href']
182+
183+
data_el = xpath_ns(output_el, './wps:Data/wps:ComplexData')
184+
if data_el != []:
185+
if data_el[0].text:
186+
output[identifier_el.text] = data_el[0].text
187+
else: # XML children
188+
ch = list(data_el[0])[0]
189+
output[identifier_el.text] = lxml.etree.tostring(ch)
190+
174191
return output
175192

176193

0 commit comments

Comments
 (0)