From 54c1b0932f71225d2c8d6b84a9cb5e665c88f938 Mon Sep 17 00:00:00 2001 From: Idan Miara Date: Thu, 10 Dec 2020 14:53:58 +0200 Subject: [PATCH] implement REST API and some other improvements read accept mimetype and support json output for all request types WPSRequest.py - accept 'json' in http_request.content_type pywps/inout/outputs.py - add type hinting literaltypes.py - add None literal type (for no conversion) basic.py - allow unlimited max_occurs add support for numpy arrays as outputs execute.py, array_encode.py - allows arrays outputs requirements-extra.txt - add numpy as an extra requirement add support: preprocess WPSRequest, preprocess WPSResponse, use api url with identifier and required output name; execute - support raw json response add a some rest API tests add some docstrings and put defaults in configuration.py --- pywps/app/Process.py | 2 +- pywps/app/Service.py | 9 +- pywps/app/WPSRequest.py | 358 +++++++++++++++++++++++++------- pywps/app/basic.py | 115 +++++++++- pywps/configuration.py | 13 +- pywps/exceptions.py | 37 ++-- pywps/inout/array_encode.py | 14 ++ pywps/inout/basic.py | 21 +- pywps/inout/formats/__init__.py | 5 +- pywps/inout/inputs.py | 27 +-- pywps/inout/literaltypes.py | 3 +- pywps/inout/outputs.py | 20 +- pywps/inout/types.py | 8 + pywps/response/__init__.py | 16 +- pywps/response/capabilities.py | 27 ++- pywps/response/describe.py | 28 ++- pywps/response/execute.py | 67 +++++- pywps/tests.py | 34 ++- tests/test_execute.py | 37 +++- tests/test_processing.py | 1 - 20 files changed, 667 insertions(+), 175 deletions(-) create mode 100644 pywps/inout/array_encode.py create mode 100644 pywps/inout/types.py diff --git a/pywps/app/Process.py b/pywps/app/Process.py index 2911caa3e..50ad401dd 100644 --- a/pywps/app/Process.py +++ b/pywps/app/Process.py @@ -252,7 +252,7 @@ def _run_process(self, wps_request, wps_response): if wps_response.status != WPS_STATUS.SUCCEEDED and wps_response.status != WPS_STATUS.FAILED: # if (not wps_response.status_percentage) or (wps_response.status_percentage != 100): LOGGER.debug('Updating process status to 100% if everything went correctly') - wps_response._update_status(WPS_STATUS.SUCCEEDED, 'PyWPS Process {} finished'.format(self.title), 100) + wps_response._update_status(WPS_STATUS.SUCCEEDED, f'PyWPS Process {self.title} finished', 100) except Exception as e: traceback.print_exc() LOGGER.debug('Retrieving file and line number where exception occurred') diff --git a/pywps/app/Service.py b/pywps/app/Service.py index 736438693..4799de965 100755 --- a/pywps/app/Service.py +++ b/pywps/app/Service.py @@ -5,6 +5,8 @@ import logging import tempfile +from typing import Sequence, Optional, Dict + from werkzeug.exceptions import HTTPException from werkzeug.wrappers import Request, Response from urllib.parse import urlparse @@ -39,9 +41,10 @@ class Service(object): :param cfgfiles: A list of configuration files """ - def __init__(self, processes=[], cfgfiles=None): + def __init__(self, processes: Sequence = [], cfgfiles=None, preprocessors: Optional[Dict] = None): # ordered dict of processes self.processes = OrderedDict((p.identifier, p) for p in processes) + self.preprocessors = preprocessors or dict() if cfgfiles: config.load_configuration(cfgfiles) @@ -85,7 +88,7 @@ def prepare_process_for_execution(self, identifier): process = self.processes[identifier] except KeyError: raise InvalidParameterValue("Unknown process '{}'".format(identifier), 'Identifier') - # make deep copy of the process instace + # make deep copy of the process instance # so that processes are not overriding each other # just for execute process = copy.deepcopy(process) @@ -281,7 +284,7 @@ def call(self, http_request): LOGGER.debug('Setting PYWPS_CFG to {}'.format(environ_cfg)) os.environ['PYWPS_CFG'] = environ_cfg - wps_request = WPSRequest(http_request) + wps_request = WPSRequest(http_request, self.preprocessors) LOGGER.info('Request: {}'.format(wps_request.operation)) if wps_request.operation in ['getcapabilities', 'describeprocess', diff --git a/pywps/app/WPSRequest.py b/pywps/app/WPSRequest.py index f6324ba3d..c6aba0275 100644 --- a/pywps/app/WPSRequest.py +++ b/pywps/app/WPSRequest.py @@ -10,26 +10,30 @@ from pywps import get_ElementMakerForVersion import base64 import datetime -from pywps.app.basic import get_xpath_ns +from pywps.app.basic import get_xpath_ns, parse_http_url from pywps.inout.inputs import input_from_json from pywps.exceptions import NoApplicableCode, OperationNotSupported, MissingParameterValue, VersionNegotiationFailed, \ InvalidParameterValue, FileSizeExceeded from pywps import configuration +from pywps.configuration import wps_strict from pywps import get_version_from_ns import json from urllib.parse import unquote LOGGER = logging.getLogger("PYWPS") +default_version = '1.0.0' class WPSRequest(object): - def __init__(self, http_request=None): + def __init__(self, http_request=None, preprocessors=None): self.http_request = http_request self.operation = None self.version = None + self.api = None + self.default_mimetype = None self.language = None self.identifier = None self.identifiers = None @@ -37,13 +41,23 @@ def __init__(self, http_request=None): self.status = None self.lineage = None self.inputs = {} + self.output_ids = None self.outputs = {} self.raw = None self.WPS = None self.OWS = None self.xpath_ns = None - - if self.http_request: + self.preprocessors = preprocessors or dict() + self.preprocess_request = None + self.preprocess_response = None + + if http_request: + d = parse_http_url(http_request) + self.operation = d.get('operation') + self.identifier = d.get('identifier') + self.output_ids = d.get('output_ids') + self.api = d.get('api') + self.default_mimetype = d.get('default_mimetype') request_parser = self._get_request_parser_method(http_request.method) request_parser() @@ -61,7 +75,7 @@ def _get_request(self): """ # service shall be WPS - service = _get_get_param(self.http_request, 'service') + service = _get_get_param(self.http_request, 'service', None if wps_strict else 'wps') if service: if str(service).lower() != 'wps': raise InvalidParameterValue( @@ -69,9 +83,12 @@ def _get_request(self): else: raise MissingParameterValue('service', 'service') - operation = _get_get_param(self.http_request, 'request') + self.operation = _get_get_param(self.http_request, 'request', self.operation) + + language = _get_get_param(self.http_request, 'language') + self.check_and_set_language(language) - request_parser = self._get_request_parser(operation) + request_parser = self._get_request_parser(self.operation) request_parser(self.http_request) def _post_request(self): @@ -84,16 +101,57 @@ def _post_request(self): raise FileSizeExceeded('File size for input exceeded.' ' Maximum request size allowed: {} megabytes'.format(maxsize / 1024 / 1024)) - try: - doc = lxml.etree.fromstring(self.http_request.get_data()) - except Exception as e: - raise NoApplicableCode(e.msg) + content_type = self.http_request.content_type or [] # or self.http_request.mimetype + json_input = 'json' in content_type + if not json_input: + try: + doc = lxml.etree.fromstring(self.http_request.get_data()) + except Exception as e: + raise NoApplicableCode(e.msg) + operation = doc.tag + version = get_version_from_ns(doc.nsmap[doc.prefix]) + self.set_version(version) + + language = doc.attrib.get('language') + self.check_and_set_language(language) + + request_parser = self._post_request_parser(operation) + request_parser(doc) + else: + try: + jdoc = json.loads(self.http_request.get_data()) + except Exception as e: + raise NoApplicableCode(e.msg) + if self.identifier is not None: + jdoc = {'inputs': jdoc} + else: + self.identifier = jdoc.get('identifier', None) + + self.operation = jdoc.get('operation', self.operation) + + preprocessor_tuple = self.preprocessors.get(self.identifier, None) + if preprocessor_tuple: + self.identifier = preprocessor_tuple[0] + self.preprocess_request = preprocessor_tuple[1] + self.preprocess_response = preprocessor_tuple[2] - operation = doc.tag - version = get_version_from_ns(doc.nsmap[doc.prefix]) - self.set_version(version) - request_parser = self._post_request_parser(operation) - request_parser(doc) + jdoc['operation'] = self.operation + jdoc['identifier'] = self.identifier + jdoc['api'] = self.api + jdoc['default_mimetype'] = self.default_mimetype + + if self.preprocess_request is not None: + jdoc = self.preprocess_request(jdoc) + self.json = jdoc + + version = jdoc.get('version') + self.set_version(version) + + language = jdoc.get('language') + self.check_and_set_language(language) + + request_parser = self._post_json_request_parser() + request_parser(jdoc) def _get_request_parser(self, operation): """Factory function returing propper parsing function @@ -108,20 +166,16 @@ def parse_get_getcapabilities(http_request): acceptedversions = _get_get_param(http_request, 'acceptversions') wpsrequest.check_accepted_versions(acceptedversions) - language = _get_get_param(http_request, 'language') - wpsrequest.check_and_set_language(language) - def parse_get_describeprocess(http_request): """Parse GET DescribeProcess request """ version = _get_get_param(http_request, 'version') wpsrequest.check_and_set_version(version) - language = _get_get_param(http_request, 'language') - wpsrequest.check_and_set_language(language) - wpsrequest.identifiers = _get_get_param( - http_request, 'identifier', aslist=True) + http_request, 'identifier', wpsrequest.identifiers, aslist=True) + if wpsrequest.identifiers is None and self.identifier is not None: + wpsrequest.identifiers = [wpsrequest.identifier] def parse_get_execute(http_request): """Parse GET Execute request @@ -129,10 +183,7 @@ def parse_get_execute(http_request): version = _get_get_param(http_request, 'version') wpsrequest.check_and_set_version(version) - language = _get_get_param(http_request, 'language') - wpsrequest.check_and_set_language(language) - - wpsrequest.identifier = _get_get_param(http_request, 'identifier') + wpsrequest.identifier = _get_get_param(http_request, 'identifier', wpsrequest.identifier) wpsrequest.store_execute = _get_get_param( http_request, 'storeExecuteResponse', 'false') wpsrequest.status = _get_get_param(http_request, 'status', 'false') @@ -143,28 +194,29 @@ def parse_get_execute(http_request): if self.inputs is None: self.inputs = {} - wpsrequest.outputs = {} - # take responseDocument preferably - resp_outputs = get_data_from_kvp( - _get_get_param(http_request, 'ResponseDocument')) - raw_outputs = get_data_from_kvp( - _get_get_param(http_request, 'RawDataOutput')) - wpsrequest.raw = False - if resp_outputs: - wpsrequest.outputs = resp_outputs - elif raw_outputs: - wpsrequest.outputs = raw_outputs - wpsrequest.raw = True + raw, output_ids = False, _get_get_param(http_request, 'ResponseDocument') + if output_ids is None: + raw, output_ids = True, _get_get_param(http_request, 'RawDataOutput') + if output_ids is not None: + wpsrequest.raw, wpsrequest.output_ids = raw, output_ids + elif wpsrequest.raw is None: + wpsrequest.raw = wpsrequest.output_ids is not None + + wpsrequest.default_mimetype = _get_get_param(http_request, 'f', wpsrequest.default_mimetype) + wpsrequest.outputs = get_data_from_kvp(wpsrequest.output_ids) or {} + if wpsrequest.raw: # executeResponse XML will not be stored and no updating of # status wpsrequest.store_execute = 'false' wpsrequest.status = 'false' - if not operation: - raise MissingParameterValue('Missing request value', 'request') - else: + if operation: self.operation = operation.lower() + else: + if wps_strict: + raise MissingParameterValue('Missing request value', 'request') + self.operation = 'execute' if self.operation == 'getcapabilities': return parse_get_getcapabilities @@ -177,7 +229,8 @@ def parse_get_execute(http_request): 'Unknown request {}'.format(self.operation), operation) def _post_request_parser(self, tagname): - """Factory function returing propper parsing function + """Factory function returning a proper parsing function + according to tagname and sets self.operation to the correct operation """ wpsrequest = self @@ -191,9 +244,6 @@ def parse_post_getcapabilities(doc): [v.text for v in acceptedversions]) wpsrequest.check_accepted_versions(acceptedversions) - language = doc.attrib.get('language') - wpsrequest.check_and_set_language(language) - def parse_post_describeprocess(doc): """Parse POST DescribeProcess request """ @@ -201,9 +251,6 @@ def parse_post_describeprocess(doc): version = doc.attrib.get('version') wpsrequest.check_and_set_version(version) - language = doc.attrib.get('language') - wpsrequest.check_and_set_language(language) - wpsrequest.operation = 'describeprocess' wpsrequest.identifiers = [identifier_el.text for identifier_el in self.xpath_ns(doc, './ows:Identifier')] @@ -214,9 +261,6 @@ def parse_post_execute(doc): version = doc.attrib.get('version') wpsrequest.check_and_set_version(version) - language = doc.attrib.get('language') - wpsrequest.check_and_set_language(language) - wpsrequest.operation = 'execute' identifier = self.xpath_ns(doc, './ows:Identifier') @@ -261,6 +305,69 @@ def parse_post_execute(doc): raise InvalidParameterValue( 'Unknown request {}'.format(tagname), 'request') + def _post_json_request_parser(self): + """ + Factory function returning a proper parsing function + according to self.operation. + self.operation is modified to be lowercase + or the default 'execute' operation if self.operation is None + """ + + wpsrequest = self + + def parse_json_post_getcapabilities(jdoc): + """Parse POST GetCapabilities request + """ + acceptedversions = jdoc.get('acceptedversions') + wpsrequest.check_accepted_versions(acceptedversions) + + def parse_json_post_describeprocess(jdoc): + """Parse POST DescribeProcess request + """ + + version = jdoc.get('version') + wpsrequest.check_and_set_version(version) + wpsrequest.identifiers = [identifier_el.text for identifier_el in + self.xpath_ns(jdoc, './ows:Identifier')] + + def parse_json_post_execute(jdoc): + """Parse POST Execute request + """ + version = jdoc.get('version') + wpsrequest.check_and_set_version(version) + + wpsrequest.identifier = jdoc.get('identifier') + if wpsrequest.identifier is None: + raise MissingParameterValue( + 'Process identifier not set', 'Identifier') + + wpsrequest.lineage = 'false' + wpsrequest.store_execute = 'false' + wpsrequest.status = 'false' + wpsrequest.inputs = get_inputs_from_json(jdoc) + + if wpsrequest.output_ids is None: + wpsrequest.output_ids = jdoc.get('outputs', {}) + wpsrequest.raw = jdoc.get('raw', False) + wpsrequest.raw, wpsrequest.outputs = get_output_from_dict(wpsrequest.output_ids, wpsrequest.raw) + + if wpsrequest.raw: + # executeResponse XML will not be stored + wpsrequest.store_execute = 'false' + + # todo: parse response_document like in the xml version? + + self.operation = 'execute' if self.operation is None else self.operation.lower() + if self.operation == 'getcapabilities': + return parse_json_post_getcapabilities + elif self.operation == 'describeprocess': + return parse_json_post_describeprocess + elif self.operation == 'execute': + return parse_json_post_execute + else: + raise InvalidParameterValue( + 'Unknown request {}'.format(self.operation), 'request') + def set_version(self, version): self.version = version self.xpath_ns = get_xpath_ns(version) @@ -287,13 +394,16 @@ def check_accepted_versions(self, acceptedversions): raise VersionNegotiationFailed( 'The requested version "{}" is not supported by this server'.format(acceptedversions), 'version') - def check_and_set_version(self, version): + def check_and_set_version(self, version, allow_default=True): """set this.version """ if not version: - raise MissingParameterValue('Missing version', 'version') - elif not _check_version(version): + if allow_default: + version = default_version + else: + raise MissingParameterValue('Missing version', 'version') + if not _check_version(version): raise VersionNegotiationFailed( 'The requested version "{}" is not supported by this server'.format(version), 'version') else: @@ -332,6 +442,8 @@ def default(self, obj): obj = { 'operation': self.operation, 'version': self.version, + 'api': self.api, + 'default_mimetype': self.default_mimetype, 'language': self.language, 'identifier': self.identifier, 'identifiers': self.identifiers, @@ -352,28 +464,32 @@ def json(self, value): :param value: the json (not string) representation """ - self.operation = value['operation'] - self.version = value['version'] - self.language = value['language'] - self.identifier = value['identifier'] - self.identifiers = value['identifiers'] - self.store_execute = value['store_execute'] - self.status = value['status'] - self.lineage = value['lineage'] - self.outputs = value['outputs'] - self.raw = value['raw'] + self.operation = value.get('operation') + self.version = value.get('version') + self.api = value.get('api') + self.default_mimetype = value.get('default_mimetype') + self.language = value.get('language') + self.identifier = value.get('identifier') + self.identifiers = value.get('identifiers') + self.store_execute = value.get('store_execute') + self.status = value.get('status', False) + self.lineage = value.get('lineage', False) + self.outputs = value.get('outputs') + self.raw = value.get('raw', False) self.inputs = {} - for identifier in value['inputs']: + for identifier in value.get('inputs', []): inpt_defs = value['inputs'][identifier] - + if not isinstance(inpt_defs, (list, tuple)): + inpt_defs = [inpt_defs] + self.inputs[identifier] = [] for inpt_def in inpt_defs: + if not isinstance(inpt_def, dict): + inpt_def = {"data": inpt_def} + if 'identifier' not in inpt_def: + inpt_def['identifier'] = identifier inpt = input_from_json(inpt_def) - - if identifier in self.inputs: - self.inputs[identifier].append(inpt) - else: - self.inputs[identifier] = [inpt] + self.inputs[identifier].append(inpt) def get_inputs_from_xml(doc): @@ -400,13 +516,11 @@ def get_inputs_from_xml(doc): complex_data = xpath_ns(input_el, './wps:Data/wps:ComplexData') if complex_data: - complex_data_el = complex_data[0] inpt = {} inpt['identifier'] = identifier_el.text - inpt['mimeType'] = complex_data_el.attrib.get('mimeType', '') - inpt['encoding'] = complex_data_el.attrib.get( - 'encoding', '').lower() + inpt['mimeType'] = complex_data_el.attrib.get('mimeType', None) + inpt['encoding'] = complex_data_el.attrib.get('encoding', '').lower() inpt['schema'] = complex_data_el.attrib.get('schema', '') inpt['method'] = complex_data_el.attrib.get('method', 'GET') if len(complex_data_el.getchildren()) > 0: @@ -426,7 +540,7 @@ def get_inputs_from_xml(doc): inpt[identifier_el.text] = reference_data_el.text inpt['href'] = reference_data_el.attrib.get( '{http://www.w3.org/1999/xlink}href', '') - inpt['mimeType'] = reference_data_el.attrib.get('mimeType', '') + inpt['mimeType'] = reference_data_el.attrib.get('mimeType', None) inpt['method'] = reference_data_el.attrib.get('method', 'GET') header_element = xpath_ns(reference_data_el, './wps:Header') if header_element: @@ -470,7 +584,7 @@ def get_output_from_xml(doc): [identifier_el] = xpath_ns(output_el, './ows:Identifier') outpt = {} outpt[identifier_el.text] = '' - outpt['mimetype'] = output_el.attrib.get('mimeType', '') + outpt['mimetype'] = output_el.attrib.get('mimeType', None) outpt['encoding'] = output_el.attrib.get('encoding', '') outpt['schema'] = output_el.attrib.get('schema', '') outpt['uom'] = output_el.attrib.get('uom', '') @@ -482,7 +596,7 @@ def get_output_from_xml(doc): [identifier_el] = xpath_ns(output_el, './ows:Identifier') outpt = {} outpt[identifier_el.text] = '' - outpt['mimetype'] = output_el.attrib.get('mimeType', '') + outpt['mimetype'] = output_el.attrib.get('mimeType', None) outpt['encoding'] = output_el.attrib.get('encoding', '') outpt['schema'] = output_el.attrib.get('schema', '') outpt['uom'] = output_el.attrib.get('uom', '') @@ -491,6 +605,92 @@ def get_output_from_xml(doc): return the_output +def get_inputs_from_json(jdoc): + the_inputs = {} + inputs_dict = jdoc.get('inputs', {}) + for identifier, inpt_defs in inputs_dict.items(): + if not isinstance(inpt_defs, (list, tuple)): + inpt_defs = [inpt_defs] + the_inputs[identifier] = [] + for inpt_def in inpt_defs: + if not isinstance(inpt_def, dict): + inpt_def = {"data": inpt_def} + data_type = inpt_def.get('type', 'literal') + if data_type == 'literal': + inpt = {} + inpt['identifier'] = identifier + inpt['data'] = inpt_def.get('data') + inpt['uom'] = inpt_def.get('uom', '') + inpt['datatype'] = inpt_def.get('datatype', '') + the_inputs[identifier].append(inpt) + continue + + if data_type == 'complex': + inpt = {} + inpt['identifier'] = identifier + inpt['mimeType'] = inpt_def.get('mimeType', None) + inpt['encoding'] = inpt_def.get('encoding', '').lower() + inpt['schema'] = inpt_def.get('schema', '') + inpt['method'] = inpt_def.get('method', 'GET') + # if len(complex_data_el.getchildren()) > 0: + # value_el = complex_data_el[0] + # inpt['data'] = _get_dataelement_value(value_el) + # else: + if True: + inpt['data'] = _get_rawvalue_value(inpt_def, inpt['encoding']) + the_inputs[identifier].append(inpt) + continue + + if data_type == 'reference': + inpt = {} + inpt['identifier'] = identifier + inpt[identifier] = inpt_def + inpt['href'] = inpt_def.get('href', '') + inpt['mimeType'] = inpt_def.get('mimeType', None) + inpt['method'] = inpt_def.get('method', 'GET') + inpt['header'] = inpt_def.get('header', '') + inpt['body'] = inpt_def.get('body', '') + inpt['bodyreference'] = inpt_def.get('bodyreference', '') + the_inputs[identifier].append(inpt) + continue + + if data_type == 'bbox': + # Using OWSlib BoundingBox + from owslib.ows import BoundingBox + bbox_datas = inpt_def + for bbox_data in bbox_datas: + bbox_data_el = bbox_data + bbox = BoundingBox(bbox_data_el) + the_inputs[identifier].append(bbox) + LOGGER.debug("parse bbox: {},{},{},{}".format(bbox.minx, bbox.miny, bbox.maxx, bbox.maxy)) + return the_inputs + + +def get_output_from_dict(output_ids, raw): + the_output = {} + if isinstance(output_ids, dict): + pass + elif isinstance(output_ids, (tuple, list)): + output_ids = {x: {} for x in output_ids} + else: + output_ids = {output_ids: {}} + raw = True # single non-dict output means raw output + for identifier, output_el in output_ids.items(): + if isinstance(output_el, list): + output_el = output_el[0] + outpt = {} + outpt[identifier] = '' + outpt['mimetype'] = output_el.get('mimeType', None) + outpt['encoding'] = output_el.get('encoding', '') + outpt['schema'] = output_el.get('schema', '') + outpt['uom'] = output_el.get('uom', '') + if not raw: + outpt['asReference'] = output_el.get('asReference', 'false') + the_output[identifier] = outpt + + return raw, the_output + + def get_data_from_kvp(data, part=None): """Get execute DataInputs and ResponseDocument from URL (key-value-pairs) encoding :param data: key:value pair list of the datainputs and responseDocument parameter diff --git a/pywps/app/basic.py b/pywps/app/basic.py index 33c02ef2a..f27175145 100644 --- a/pywps/app/basic.py +++ b/pywps/app/basic.py @@ -7,8 +7,10 @@ """ import logging +from typing import Optional, Tuple + from werkzeug.wrappers import Response -from pywps import __version__ +import pywps.configuration as config LOGGER = logging.getLogger('PYWPS') @@ -33,8 +35,113 @@ def xpath_ns(ele, path): return xpath_ns -def xml_response(doc): - """XML response serializer""" - response = Response(doc, content_type='text/xml') +def make_response(doc, content_type): + """response serializer""" + if not content_type: + content_type = get_default_response_mimetype() + response = Response(doc, content_type=content_type) response.status_percentage = 100 return response + + +def get_default_response_mimetype(): + default_mimetype = config.get_config_value('server', 'default_mimetype') + return default_mimetype + + +def get_json_indent(): + json_ident = int(config.get_config_value('server', 'json_indent')) + return json_ident if json_ident >= 0 else None + + +def get_response_type(accept_mimetypes, default_mimetype) -> Tuple[bool, str]: + """ + This function determinate if the response should be JSON or XML based on + the accepted mimetypes of the request and the default mimetype provided, + which will be used in case both are equally accepted. + + :param accept_mimetypes: determinate which mimetypes are accepted + :param default_mimetype: "text/xml", "application/json" + :return: Tuple[bool, str] - + bool - True: The response type is JSON, False: Otherwise - XML + str - The output mimetype + """ + accept_json = \ + accept_mimetypes.accept_json or \ + accept_mimetypes.best is None or \ + 'json' in accept_mimetypes.best.lower() + accept_xhtml = \ + accept_mimetypes.accept_xhtml or \ + accept_mimetypes.best is None or \ + 'xml' in accept_mimetypes.best.lower() + if not default_mimetype: + default_mimetype = get_default_response_mimetype() + json_is_default = 'json' in default_mimetype or '*' in default_mimetype + json_response = (accept_json and (not accept_xhtml or json_is_default)) or \ + (json_is_default and accept_json == accept_xhtml) + mimetype = 'application/json' if json_response else 'text/xml' if accept_xhtml else '' + return json_response, mimetype + + +def parse_http_url(http_request) -> dict: + """ + This function parses the request URL and extracts the following: + default operation, process identifier, output_ids, default mimetype + info that cannot be terminated from the URL will be None (default) + + The url is expected to be in the following format, all the levels are optional. + [base_url]/[identifier]/[output_ids] + + :param http_request: the request URL + :return: dict with the extracted info listed: + base_url - [wps|processes|jobs|api/api_level] + default_mimetype - determinate by the base_url part: + XML - if the base url == 'wps', + JSON - if the base URL in ['api'|'jobs'|'processes'] + operation - also determinate by the base_url part: + ['api'|'jobs'] -> 'execute' + processes -> 'describeprocess' or 'getcapabilities' + 'describeprocess' if identifier is present as the next item, 'getcapabilities' otherwise + api - api level, only expected if base_url=='api' + identifier - the process identifier + output_ids - if exist then it selects raw output with the name output_ids + """ + operation = api = identifier = output_ids = default_mimetype = base_url = None + if http_request: + parts = str(http_request.path[1:]).split('/') + i = 0 + if len(parts) > i: + base_url = parts[i].lower() + if base_url == 'wps': + default_mimetype = 'xml' + elif base_url in ['api', 'processes', 'jobs']: + default_mimetype = 'json' + i += 1 + if base_url == 'api': + api = parts[i] + i += 1 + if len(parts) > i: + identifier = parts[i] + i += 1 + if len(parts) > i: + output_ids = parts[i] + if not output_ids: + output_ids = None + if base_url in ['jobs', 'api']: + operation = 'execute' + elif base_url == 'processes': + operation = 'describeprocess' if identifier else 'getcapabilities' + d = {} + if operation: + d['operation'] = operation + if identifier: + d['identifier'] = identifier + if output_ids: + d['output_ids'] = output_ids + if default_mimetype: + d['default_mimetype'] = default_mimetype + if api: + d['api'] = api + if base_url: + d['base_url'] = base_url + return d diff --git a/pywps/configuration.py b/pywps/configuration.py index 71485c908..48ba7ccb2 100755 --- a/pywps/configuration.py +++ b/pywps/configuration.py @@ -24,8 +24,10 @@ CONFIG = None LOGGER = logging.getLogger("PYWPS") +wps_strict = True -def get_config_value(section, option): + +def get_config_value(section, option, default_value=''): """Get desired value from configuration files :param section: section in configuration files @@ -38,7 +40,7 @@ def get_config_value(section, option): if not CONFIG: load_configuration() - value = '' + value = default_value if CONFIG.has_section(section): if CONFIG.has_option(section, option): @@ -96,6 +98,13 @@ def load_configuration(cfgfiles=None): # Allowed functions: "copy", "move", "link" (default "copy") CONFIG.set('server', 'storage_copy_function', 'copy') + # handles the default mimetype for requests. + # available options: "text/xml", "application/json" + CONFIG.set("server", "default_mimetype", "text/xml") + + # default json indentation for responses. + CONFIG.set("server", "json_indent", "2") + CONFIG.add_section('processing') CONFIG.set('processing', 'mode', 'default') CONFIG.set('processing', 'path', os.path.dirname(os.path.realpath(sys.argv[0]))) diff --git a/pywps/exceptions.py b/pywps/exceptions.py index e730d73df..1eb7b2159 100644 --- a/pywps/exceptions.py +++ b/pywps/exceptions.py @@ -11,7 +11,10 @@ http://lists.opengeospatial.org/pipermail/wps-dev/2013-October/000335.html """ +import json +from werkzeug.datastructures import MIMEAccept +from werkzeug.http import parse_accept_header from werkzeug.wrappers import Response from werkzeug.exceptions import HTTPException from werkzeug.utils import escape @@ -19,6 +22,7 @@ import logging from pywps import __version__ +from pywps.app.basic import get_json_indent, get_response_type, parse_http_url __author__ = "Alex Morega & Calin Ciociu" @@ -53,7 +57,7 @@ def name(self): def get_description(self, environ=None): """Get the description.""" if self.description: - return '''{}'''.format(escape(self.description)) + return escape(self.description) else: return '' @@ -65,17 +69,26 @@ def get_response(self, environ=None): 'name': escape(self.name), 'description': self.get_description(environ) } - doc = str(( - '\n' - '\n' - '\n' # noqa - ' \n' - ' {description}\n' - ' \n' - '' - ).format(**args)) - - return Response(doc, self.code, mimetype='text/xml') + accept_mimetypes = parse_accept_header(environ.get("HTTP_ACCEPT"), MIMEAccept) + request = environ.get('werkzeug.request', None) + default_mimetype = None if not request else request.args.get('f', None) + if default_mimetype is None: + default_mimetype = parse_http_url(request).get('default_mimetype') + json_response, mimetype = get_response_type(accept_mimetypes, default_mimetype) + if json_response: + doc = json.dumps(args, indent=get_json_indent()) + else: + doc = str(( + '\n' + '\n' + '\n' # noqa + ' \n' + ' {description}\n' + ' \n' + '' + ).format(**args)) + + return Response(doc, self.code, mimetype=mimetype) class InvalidParameterValue(NoApplicableCode): diff --git a/pywps/inout/array_encode.py b/pywps/inout/array_encode.py new file mode 100644 index 000000000..d0ad676ed --- /dev/null +++ b/pywps/inout/array_encode.py @@ -0,0 +1,14 @@ +################################################################## +# Copyright 2018 Open Source Geospatial Foundation and others # +# licensed under MIT, Please consult LICENSE.txt for details # +################################################################## + +from json import JSONEncoder + + +class ArrayEncoder(JSONEncoder): + def default(self, obj): + if hasattr(obj, 'tolist'): + # this will work for array.array and numpy.ndarray + return obj.tolist() + return JSONEncoder.default(self, obj) diff --git a/pywps/inout/basic.py b/pywps/inout/basic.py index 40fdcd5f2..64c918a17 100644 --- a/pywps/inout/basic.py +++ b/pywps/inout/basic.py @@ -4,6 +4,8 @@ ################################################################## from pathlib import PurePath +from pywps.inout.formats import Supported_Formats +from pywps.inout.types import Translations from pywps.translations import lower_case_dict from io import StringIO import os @@ -203,11 +205,12 @@ def _check_valid(self): """ validate = self.validator - _valid = validate(self, self.valid_mode) - if not _valid: - self.data_set = False - raise InvalidParameterValue('Input data not valid using ' - 'mode {}'.format(self.valid_mode)) + if validate is not None: + _valid = validate(self, self.valid_mode) + if not _valid: + self.data_set = False + raise InvalidParameterValue('Input data not valid using ' + 'mode {}'.format(self.valid_mode)) self.data_set = True @property @@ -644,7 +647,7 @@ def __init__(self, identifier, title=None, abstract=None, keywords=None, self.abstract = abstract self.keywords = keywords self.min_occurs = int(min_occurs) - self.max_occurs = int(max_occurs) + self.max_occurs = int(max_occurs) if max_occurs is not None else None self.metadata = metadata self.translations = lower_case_dict(translations) @@ -715,7 +718,7 @@ def get_format(self, mime_type): def validator(self): """Return the proper validator for given data_format """ - return self.data_format.validate + return None if self.data_format is None else self.data_format.validate @property def supported_formats(self): @@ -1071,8 +1074,8 @@ class ComplexOutput(BasicIO, BasicComplex, IOHandler): """ def __init__(self, identifier, title=None, abstract=None, keywords=None, - workdir=None, data_format=None, supported_formats=None, - mode=MODE.NONE, translations=None): + workdir=None, data_format=None, supported_formats: Supported_Formats = None, + mode=MODE.NONE, translations: Translations = None): BasicIO.__init__(self, identifier, title, abstract, keywords, translations=translations) IOHandler.__init__(self, workdir=workdir, mode=mode) BasicComplex.__init__(self, data_format, supported_formats) diff --git a/pywps/inout/formats/__init__.py b/pywps/inout/formats/__init__.py index 4680cbf84..5e05276cc 100644 --- a/pywps/inout/formats/__init__.py +++ b/pywps/inout/formats/__init__.py @@ -13,7 +13,7 @@ from collections import namedtuple import mimetypes - +from typing import Optional, Sequence, Union _FORMATS = namedtuple('FORMATS', 'GEOJSON, JSON, SHP, GML, GPX, METALINK, META4, KML, KMZ, GEOTIFF,' 'WCS, WCS100, WCS110, WCS20, WFS, WFS100,' @@ -162,6 +162,9 @@ def json(self, jsonin): self.extension = jsonin['extension'] +Supported_Formats = Optional[Sequence[Union[str, Format]]] + + FORMATS = _FORMATS( Format('application/geo+json', extension='.geojson'), Format('application/json', extension='.json'), diff --git a/pywps/inout/inputs.py b/pywps/inout/inputs.py index 180fbf204..201356580 100644 --- a/pywps/inout/inputs.py +++ b/pywps/inout/inputs.py @@ -186,6 +186,14 @@ def json(self): @classmethod def from_json(cls, json_input): + data_format = json_input.get('data_format') + if data_format is not None: + data_format = Format( + schema=data_format.get('schema'), + extension=data_format.get('extension'), + mime_type=data_format.get('mime_type', ""), + encoding=data_format.get('encoding') + ) instance = cls( identifier=json_input['identifier'], title=json_input.get('title'), @@ -193,19 +201,14 @@ def from_json(cls, json_input): keywords=json_input.get('keywords', []), workdir=json_input.get('workdir'), metadata=[Metadata.from_json(data) for data in json_input.get('metadata', [])], - data_format=Format( - schema=json_input['data_format'].get('schema'), - extension=json_input['data_format'].get('extension'), - mime_type=json_input['data_format']['mime_type'], - encoding=json_input['data_format'].get('encoding') - ), + data_format=data_format, supported_formats=[ Format( schema=infrmt.get('schema'), extension=infrmt.get('extension'), - mime_type=infrmt['mime_type'], + mime_type=infrmt.get('mime_type'), encoding=infrmt.get('encoding') - ) for infrmt in json_input['supported_formats'] + ) for infrmt in json_input.get('supported_formats', []) ], mode=json_input.get('mode', MODE.NONE), translations=json_input.get('translations'), @@ -331,7 +334,7 @@ def json(self): @classmethod def from_json(cls, json_input): allowed_values = [] - for allowed_value in json_input['allowed_values']: + for allowed_value in json_input.get('allowed_values', []): if allowed_value['type'] == 'anyvalue': allowed_values.append(AnyValue()) elif allowed_value['type'] == 'novalue': @@ -351,7 +354,7 @@ def from_json(cls, json_input): data = json_input_copy.pop('data', None) uom = json_input_copy.pop('uom', None) metadata = json_input_copy.pop('metadata', []) - json_input_copy.pop('type') + json_input_copy.pop('type', None) json_input_copy.pop('any_value', None) json_input_copy.pop('values_reference', None) @@ -371,8 +374,8 @@ def clone(self): def input_from_json(json_data): - data_type = json_data['type'] - if data_type == 'complex': + data_type = json_data.get('type', 'literal') + if data_type in ['complex', 'reference']: inpt = ComplexInput.from_json(json_data) elif data_type == 'literal': inpt = LiteralInput.from_json(json_data) diff --git a/pywps/inout/literaltypes.py b/pywps/inout/literaltypes.py index 8a93a246c..c92295439 100644 --- a/pywps/inout/literaltypes.py +++ b/pywps/inout/literaltypes.py @@ -5,7 +5,6 @@ """Literaltypes are used for LiteralInputs, to make sure, input data are OK """ - from urllib.parse import urlparse from dateutil.parser import parse as date_parser import datetime @@ -19,7 +18,7 @@ LITERAL_DATA_TYPES = ('float', 'boolean', 'integer', 'string', 'positiveInteger', 'anyURI', 'time', 'date', 'dateTime', 'scale', 'angle', - 'nonNegativeInteger') + 'nonNegativeInteger', None) # currently we are supporting just ^^^ data types, feel free to add support for # more diff --git a/pywps/inout/outputs.py b/pywps/inout/outputs.py index 91c431ac2..ff0748b55 100644 --- a/pywps/inout/outputs.py +++ b/pywps/inout/outputs.py @@ -5,6 +5,7 @@ """ WPS Output classes """ +from typing import Optional, Sequence, Dict, Union import lxml.etree as etree import os @@ -13,9 +14,10 @@ from pywps.exceptions import InvalidParameterValue from pywps.inout import basic from pywps.inout.storage.file import FileStorageBuilder +from pywps.inout.types import Translations from pywps.validator.mode import MODE from pywps import configuration as config -from pywps.inout.formats import Format +from pywps.inout.formats import Format, Supported_Formats class BoundingBoxOutput(basic.BBoxOutput): @@ -109,9 +111,10 @@ class ComplexOutput(basic.ComplexOutput): e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}} """ - def __init__(self, identifier, title, supported_formats=None, - data_format=None, abstract='', keywords=[], workdir=None, metadata=None, - as_reference=False, mode=MODE.NONE, translations=None): + def __init__(self, identifier: str, title: str, supported_formats: Supported_Formats = None, + data_format=None, abstract: str = '', keywords=[], workdir=None, + metadata: Optional[Sequence[Metadata]] = None, + as_reference=False, mode: MODE = MODE.NONE, translations: Translations = None): if metadata is None: metadata = [] @@ -539,13 +542,14 @@ class MetaLink4(MetaLink): def output_from_json(json_data): - if json_data['type'] == 'complex': + data_type = json_data.get('type', 'literal') + if data_type == 'complex': output = ComplexOutput.from_json(json_data) - elif json_data['type'] == 'literal': + elif data_type == 'literal': output = LiteralOutput.from_json(json_data) - elif json_data['type'] == 'bbox': + elif data_type == 'bbox': output = BoundingBoxOutput.from_json(json_data) else: - raise InvalidParameterValue("Output type not recognized: {}".format(json_data['type'])) + raise InvalidParameterValue("Output type not recognized: {}".format(data_type)) return output diff --git a/pywps/inout/types.py b/pywps/inout/types.py new file mode 100644 index 000000000..2e05ae183 --- /dev/null +++ b/pywps/inout/types.py @@ -0,0 +1,8 @@ +################################################################## +# Copyright 2018 Open Source Geospatial Foundation and others # +# licensed under MIT, Please consult LICENSE.txt for details # +################################################################## + +from typing import Optional, Dict + +Translations = Optional[Dict[str, Dict[str, str]]] diff --git a/pywps/response/__init__.py b/pywps/response/__init__.py index d20122b08..f4c7cfc75 100644 --- a/pywps/response/__init__.py +++ b/pywps/response/__init__.py @@ -1,3 +1,8 @@ +from abc import abstractmethod +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pywps import WPSRequest + from pywps.dblog import store_status from pywps.response.status import WPS_STATUS from pywps.translations import get_translation @@ -27,7 +32,7 @@ def get_response(operation): class WPSResponse(object): - def __init__(self, wps_request, uuid=None, version="1.0.0"): + def __init__(self, wps_request: 'WPSRequest', uuid=None, version="1.0.0"): self.wps_request = wps_request self.uuid = uuid @@ -35,6 +40,7 @@ def __init__(self, wps_request, uuid=None, version="1.0.0"): self.status = WPS_STATUS.ACCEPTED self.status_percentage = 0 self.doc = None + self.content_type = None self.version = version self.template_env = RelEnvironment( loader=PackageLoader('pywps', 'templates'), @@ -57,9 +63,13 @@ def _update_status(self, status, message, status_percentage): self.status_percentage = status_percentage store_status(self.uuid, self.status, self.message, self.status_percentage) + @abstractmethod + def _construct_doc(self): + ... + def get_response_doc(self): try: - self.doc = self._construct_doc() + self.doc, self.content_type = self._construct_doc() except Exception as e: if hasattr(e, "description"): msg = e.description @@ -71,4 +81,4 @@ def get_response_doc(self): else: self._update_status(WPS_STATUS.SUCCEEDED, "Response generated", 100) - return self.doc + return self.doc, self.content_type diff --git a/pywps/response/capabilities.py b/pywps/response/capabilities.py index 718555339..d0cce2e26 100644 --- a/pywps/response/capabilities.py +++ b/pywps/response/capabilities.py @@ -1,6 +1,8 @@ +import json + from werkzeug.wrappers import Request import pywps.configuration as config -from pywps.app.basic import xml_response +from pywps.app.basic import make_response, get_response_type, get_json_indent from pywps.response import WPSResponse from pywps import __version__ from pywps.exceptions import NoApplicableCode @@ -60,20 +62,27 @@ def json(self): 'processes': processes } - def _construct_doc(self): - - template = self.template_env.get_template(self.version + '/capabilities/main.xml') + @staticmethod + def _render_json_response(jdoc): + return jdoc - doc = template.render(**self.json) - - return doc + def _construct_doc(self): + doc = self.json + json_response, mimetype = get_response_type( + self.wps_request.http_request.accept_mimetypes, self.wps_request.default_mimetype) + if json_response: + doc = json.dumps(self._render_json_response(doc), indent=get_json_indent()) + else: + template = self.template_env.get_template(self.version + '/capabilities/main.xml') + doc = template.render(**doc) + return doc, mimetype @Request.application def __call__(self, request): # This function must return a valid response. try: - doc = self.get_response_doc() - return xml_response(doc) + doc, content_type = self.get_response_doc() + return make_response(doc, content_type=content_type) except NoApplicableCode as e: return e except Exception as e: diff --git a/pywps/response/describe.py b/pywps/response/describe.py index e8144b5d4..c3e61b3c9 100644 --- a/pywps/response/describe.py +++ b/pywps/response/describe.py @@ -1,6 +1,8 @@ +import json + from werkzeug.wrappers import Request import pywps.configuration as config -from pywps.app.basic import xml_response +from pywps.app.basic import make_response, get_response_type, get_json_indent from pywps.exceptions import NoApplicableCode from pywps.exceptions import MissingParameterValue from pywps.exceptions import InvalidParameterValue @@ -41,23 +43,31 @@ def json(self): 'language': self.wps_request.language, } - def _construct_doc(self): + @staticmethod + def _render_json_response(jdoc): + return jdoc + def _construct_doc(self): if not self.identifiers: raise MissingParameterValue('Missing parameter value "identifier"', 'identifier') - template = self.template_env.get_template(self.version + '/describe/main.xml') - max_size = int(config.get_size_mb(config.get_config_value('server', 'maxsingleinputsize'))) - doc = template.render(max_size=max_size, **self.json) - - return doc + doc = self.json + json_response, mimetype = get_response_type( + self.wps_request.http_request.accept_mimetypes, self.wps_request.default_mimetype) + if json_response: + doc = json.dumps(self._render_json_response(doc), indent=get_json_indent()) + else: + template = self.template_env.get_template(self.version + '/describe/main.xml') + max_size = int(config.get_size_mb(config.get_config_value('server', 'maxsingleinputsize'))) + doc = template.render(max_size=max_size, **doc) + return doc, mimetype @Request.application def __call__(self, request): # This function must return a valid response. try: - doc = self.get_response_doc() - return xml_response(doc) + doc, content_type = self.get_response_doc() + return make_response(doc, content_type=content_type) except NoApplicableCode as e: return e except Exception as e: diff --git a/pywps/response/execute.py b/pywps/response/execute.py index b621954cd..f77403665 100755 --- a/pywps/response/execute.py +++ b/pywps/response/execute.py @@ -3,14 +3,17 @@ # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## +import json import logging import time from werkzeug.wrappers import Request from pywps import get_ElementMakerForVersion +from pywps.app.basic import get_response_type, get_json_indent, get_default_response_mimetype from pywps.exceptions import NoApplicableCode import pywps.configuration as config from werkzeug.wrappers import Response +from pywps.inout.array_encode import ArrayEncoder from pywps.response.status import WPS_STATUS from pywps.response import WPSResponse from pywps.inout.formats import FORMATS @@ -78,7 +81,7 @@ def update_status(self, message, status_percentage=None): def _update_status_doc(self): try: # rebuild the doc - self.doc = self._construct_doc() + self.doc, self.content_type = self._construct_doc() except Exception as e: raise NoApplicableCode('Building Response Document failed with : {}'.format(e)) @@ -188,30 +191,72 @@ def json(self): data["output_definitions"] = [self.outputs[o].json for o in self.outputs] return data + @staticmethod + def _render_json_response(jdoc): + response = dict() + response['status'] = jdoc['status'] + out = jdoc['process']['outputs'] + response['outputs'] = {val['identifier']: val['data'] for val in out if ('identifier' in val and 'data' in val)} + return response + def _construct_doc(self): - template = self.template_env.get_template(self.version + '/execute/main.xml') - doc = template.render(**self.json) - return doc + if self.status == WPS_STATUS.SUCCEEDED and \ + hasattr(self.wps_request, 'preprocess_response') and \ + self.wps_request.preprocess_response: + self.outputs = self.wps_request.preprocess_response(self.outputs) + doc = self.json + try: + json_response, mimetype = get_response_type( + self.wps_request.http_request.accept_mimetypes, self.wps_request.default_mimetype) + except Exception: + mimetype = get_default_response_mimetype() + json_response = 'json' in mimetype + if json_response: + doc = json.dumps(self._render_json_response(doc), cls=ArrayEncoder, indent=get_json_indent()) + else: + template = self.template_env.get_template(self.version + '/execute/main.xml') + doc = template.render(**doc) + return doc, mimetype @Request.application def __call__(self, request): + accept_json_response, accepted_mimetype = get_response_type( + self.wps_request.http_request.accept_mimetypes, self.wps_request.default_mimetype) if self.wps_request.raw: if self.status == WPS_STATUS.FAILED: return NoApplicableCode(self.message) else: wps_output_identifier = next(iter(self.wps_request.outputs)) # get the first key only wps_output_value = self.outputs[wps_output_identifier] - if wps_output_value.source_type is None: + response = wps_output_value.data + if response is None: return NoApplicableCode("Expected output was not generated") suffix = '' - if isinstance(wps_output_value, ComplexOutput): - if wps_output_value.data_format.extension is not None: - suffix = wps_output_value.data_format.extension - return Response(wps_output_value.data, - mimetype=wps_output_value.data_format.mime_type, + # if isinstance(wps_output_value, ComplexOutput): + if hasattr(wps_output_value, 'data_format'): + data_format = wps_output_value.data_format + mimetype = data_format.mime_type + if data_format.extension is not None: + suffix = data_format.extension + else: + # like LitearlOutput + mimetype = self.wps_request.outputs[wps_output_identifier].get('mimetype', None) + if not isinstance(response, (str, bytes, bytearray)): + if not mimetype: + mimetype = accepted_mimetype + json_response = mimetype and 'json' in mimetype + if json_response: + mimetype = 'application/json' + suffix = '.json' + response = json.dumps(response, cls=ArrayEncoder, indent=get_json_indent()) + else: + response = str(response) + if not mimetype: + mimetype = None + return Response(response, mimetype=mimetype, headers={'Content-Disposition': 'attachment; filename="{}"' .format(wps_output_identifier + suffix)}) else: if not self.doc: return NoApplicableCode("Output was not generated") - return Response(self.doc, mimetype='text/xml') + return Response(self.doc, mimetype=accepted_mimetype) diff --git a/pywps/tests.py b/pywps/tests.py index db9d5c8ad..4d0a945ce 100644 --- a/pywps/tests.py +++ b/pywps/tests.py @@ -2,6 +2,7 @@ # Copyright 2018 Open Source Geospatial Foundation and others # # licensed under MIT, Please consult LICENSE.txt for details # ################################################################## +import json import tempfile from pathlib import Path @@ -97,6 +98,16 @@ def post_xml(self, *args, **kwargs): kwargs['data'] = data return self.post(*args, **kwargs) + def post_json(self, *args, **kwargs): + doc = kwargs.pop('doc') + # data = json.dumps(doc, indent=2) + # kwargs['data'] = data + kwargs['json'] = doc + # kwargs['content_type'] = 'application/json' # input is json, redundant as it's deducted from the json kwarg + # kwargs['mimetype'] = 'application/json' # output is json + kwargs['environ_base'] = {'HTTP_ACCEPT': 'application/json'} # output is json + return self.post(*args, **kwargs) + class WpsTestResponse(BaseResponse): @@ -143,9 +154,30 @@ def assert_process_started(resp): assert success.split[0] == "processstarted" +def assert_response_success_json(resp, expected_data): + assert resp.status_code == 200 + + content_type = resp.headers['Content-Type'] + expected_contect_type = 'application/json' + re_content_type = rf'{expected_contect_type}(;\s*charset=.*)?' + assert re.match(re_content_type, content_type) + + data = json.loads(resp.data) + + success = data['status']['status'] + assert success == 'succeeded' + + if expected_data: + outputs = data['outputs'] + assert outputs == expected_data + + def assert_response_success(resp): assert resp.status_code == 200 - assert re.match(r'text/xml(;\s*charset=.*)?', resp.headers['Content-Type']) + content_type = resp.headers['Content-Type'] + expected_contect_type = 'text/xml' + re_content_type = rf'{expected_contect_type}(;\s*charset=.*)?' + assert re.match(re_content_type, content_type) success = resp.xpath('/wps:ExecuteResponse/wps:Status/wps:ProcessSucceeded') assert len(success) == 1 diff --git a/tests/test_execute.py b/tests/test_execute.py index 24a36d9ee..e054b6d78 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -18,7 +18,7 @@ from pywps import get_inputs_from_xml, get_output_from_xml from pywps import E, get_ElementMakerForVersion from pywps.app.basic import get_xpath_ns -from pywps.tests import client_for, assert_response_success +from pywps.tests import client_for, assert_response_success, assert_response_success_json from pywps import configuration from io import StringIO @@ -422,31 +422,52 @@ def test_get_with_no_inputs(self): assert get_output(resp.xml) == {'outvalue': '42'} def test_post_with_no_inputs(self): + request = { + 'identifier': 'ultimate_question', + 'version': '1.0.0' + } + result = {'outvalue': '42'} + client = client_for(Service(processes=[create_ultimate_question()])) request_doc = WPS.Execute( - OWS.Identifier('ultimate_question'), - version='1.0.0' + OWS.Identifier(request['identifier']), + version=request['version'] ) resp = client.post_xml(doc=request_doc) assert_response_success(resp) - assert get_output(resp.xml) == {'outvalue': '42'} + assert get_output(resp.xml) == result + + resp = client.post_json(doc=request) + assert_response_success_json(resp, result) def test_post_with_string_input(self): + request = { + 'identifier': 'greeter', + 'version': '1.0.0', + 'inputs': { + 'name': 'foo' + }, + } + result = {'message': "Hello foo!"} + client = client_for(Service(processes=[create_greeter()])) request_doc = WPS.Execute( - OWS.Identifier('greeter'), + OWS.Identifier(request['identifier']), WPS.DataInputs( WPS.Input( OWS.Identifier('name'), - WPS.Data(WPS.LiteralData('foo')) + WPS.Data(WPS.LiteralData(request['inputs']['name'])) ) ), - version='1.0.0' + version=request['version'] ) resp = client.post_xml(doc=request_doc) assert_response_success(resp) - assert get_output(resp.xml) == {'message': "Hello foo!"} + assert get_output(resp.xml) == result + + resp = client.post_json(doc=request) + assert_response_success_json(resp, result) def test_bbox(self): client = client_for(Service(processes=[create_bbox_process()])) diff --git a/tests/test_processing.py b/tests/test_processing.py index f6a8c2572..2211403d3 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -15,7 +15,6 @@ import pywps.processing from pywps.processing.job import Job from pywps.processing.basic import MultiProcessing -from pywps import Process from pywps.app import WPSRequest from pywps.response.execute import ExecuteResponse