[Git][debian-gis-team/pywps][experimental] 5 commits: New upstream version 4.5.0
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Thu Aug 12 15:36:19 BST 2021
Bas Couwenberg pushed to branch experimental at Debian GIS Project / pywps
Commits:
25ad6aa4 by Bas Couwenberg at 2021-08-12T16:26:32+02:00
New upstream version 4.5.0
- - - - -
c8b55df9 by Bas Couwenberg at 2021-08-12T16:26:34+02:00
Update upstream source from tag 'upstream/4.5.0'
Update to upstream version '4.5.0'
with Debian dir 42f972e7cb1c783cd697c473f06736c0b353c45d
- - - - -
87d521d6 by Bas Couwenberg at 2021-08-12T16:26:55+02:00
New upstream release.
- - - - -
6e24f102 by Bas Couwenberg at 2021-08-12T16:28:12+02:00
Refresh patches.
- - - - -
32fed6c2 by Bas Couwenberg at 2021-08-12T16:28:29+02:00
Set distribution to experimental.
- - - - -
27 changed files:
- VERSION.txt
- debian/changelog
- debian/patches/offline-tests.patch
- + docs/api_rest.rst
- docs/index.rst
- pywps/__init__.py
- pywps/app/Process.py
- pywps/app/Service.py
- pywps/app/WPSRequest.py
- pywps/app/basic.py
- pywps/configuration.py
- pywps/exceptions.py
- + pywps/inout/array_encode.py
- pywps/inout/basic.py
- pywps/inout/formats/__init__.py
- pywps/inout/inputs.py
- pywps/inout/literaltypes.py
- pywps/inout/outputs.py
- + pywps/inout/types.py
- pywps/response/__init__.py
- pywps/response/capabilities.py
- pywps/response/describe.py
- pywps/response/execute.py
- pywps/tests.py
- setup.cfg
- tests/test_execute.py
- tests/test_processing.py
Changes:
=====================================
VERSION.txt
=====================================
@@ -1 +1 @@
-4.4.5
+4.5.0
=====================================
debian/changelog
=====================================
@@ -1,3 +1,11 @@
+pywps (4.5.0-1~exp1) experimental; urgency=medium
+
+ * Team upload.
+ * New upstream release.
+ * Refresh patches.
+
+ -- Bas Couwenberg <sebastic at debian.org> Thu, 12 Aug 2021 16:28:15 +0200
+
pywps (4.4.5-1~exp1) experimental; urgency=medium
* Team upload.
=====================================
debian/patches/offline-tests.patch
=====================================
@@ -43,7 +43,7 @@ Forwarded: not-needed
opendap_input = ComplexInput('dods', 'opendap test', [FORMATS.DODS,])
--- a/tests/test_execute.py
+++ b/tests/test_execute.py
-@@ -229,6 +229,8 @@ def get_output(doc):
+@@ -234,6 +234,8 @@ def get_output(doc):
class ExecuteTest(unittest.TestCase):
"""Test for Exeucte request KVP request"""
=====================================
docs/api_rest.rst
=====================================
@@ -0,0 +1,407 @@
+.. _api_rest:
+
+###################
+PyWPS Rest API Doc
+###################
+
+Since version 4.5, PyWPS includes an experimental implementation of the novel OGC API.
+This standard defines the OGC API - Processes API standard.
+This standard builds on the OGC Web Processing Service (WPS) 2.0 Standard
+and defines the processing interface to communicate over a RESTful protocol using JSON encodings.
+
+For more details about the standard please refer to https://github.com/opengeospatial/ogcapi-processes
+
+Defining the input/output format (JSON or XML)
+================================================
+
+WPS 1.0 standard defines input and outputs in XML format.
+OGC API - Processes: rest-api, json.
+PyWPS >= 4.5 allows inputs and outputs to be in both XML and JSON formats.
+
+The default format (mimetype) of the input/output is determinate by the URL:
+
+* Default XML - if the url starts with `/wps`
+* Default JSON - if the url starts with `/jobs` or `/processes`
+
+Please refer to `app.basic.parse_http_url` for full details about those defaults.
+
+
+GET request:
+-------------
+
+The default mimetype (output format) can be set by adding `&f=json` or `&f=xml` parameter.
+
+GET GetCapabilities Request URL:
+
+.. code-block::
+
+ http://localhost:5000/processes/?service=WPS
+ http://localhost:5000/wps/?request=GetCapabilities&service=WPS&f=json
+
+GET GetCapabilities Response:
+
+.. code-block::
+
+ {
+ "pywps_version": "4.5.0",
+ "version": "1.0.0",
+ "title": "PyWPS WPS server",
+ "abstract": "PyWPS WPS server server.",
+ "keywords": [
+ "WPS",
+ "PyWPS",
+ ],
+ "provider": {
+ "name": "PyWPS Development team",
+ "site": "https://github.com/geopython/pywps-flask",
+ },
+ "serviceurl": "http://localhost:5000/wps",
+ "languages": [
+ "en-US"
+ ],
+ "language": "en-US",
+ "processes": [
+ {
+ "class": "processes.sayhello:SayHello",
+ "uuid": "None",
+ "workdir": null,
+ "version": "1.3.3.8",
+ "identifier": "say_hello",
+ "title": "Process Say Hello",
+ "abstract": "Returns a literal string output with Hello plus the inputed name",
+ "keywords": [],
+ "metadata": [],
+ "inputs": [
+ {
+ "identifier": "name",
+ "title": "Input name",
+ "abstract": "",
+ "keywords": [],
+ "metadata": [],
+ "type": "literal",
+ "data_type": "string",
+ "workdir": null,
+ "allowed_values": [],
+ "any_value": false,
+ "mode": 1,
+ "min_occurs": 1,
+ "max_occurs": 1,
+ "translations": null,
+ "data": "World"
+ }
+ ],
+ "outputs": [
+ {
+ "identifier": "output",
+ "title": "Output response",
+ "abstract": "",
+ "keywords": [],
+ "data": null,
+ "data_type": "string",
+ "type": "literal",
+ "uoms": [],
+ "translations": null
+ }
+ ],
+ "store_supported": "true",
+ "status_supported": "true",
+ "profile": [],
+ "translations": null
+ }
+ ]
+ }
+
+GET DescribeProcess Request URL:
+
+.. code-block::
+
+ http://localhost:5000/processes/say_hello?service=WPS
+ http://localhost:5000/wps/?request=DescribeProcess&service=WPS&identifier=say_hello&version=1.0.0&f=json
+
+GET DescribeProcess Response:
+
+.. code-block::
+
+ {
+ "pywps_version": "4.5.0",
+ "processes": [
+ {
+ "class": "processes.sayhello:SayHello",
+ "uuid": "None",
+ "workdir": null,
+ "version": "1.3.3.8",
+ "identifier": "say_hello",
+ "title": "Process Say Hello",
+ "abstract": "Returns a literal string output with Hello plus the inputed name",
+ "keywords": [],
+ "metadata": [],
+ "inputs": [
+ {
+ "identifier": "name",
+ "title": "Input name",
+ "abstract": "",
+ "keywords": [],
+ "metadata": [],
+ "type": "literal",
+ "data_type": "string",
+ "workdir": null,
+ "allowed_values": [],
+ "any_value": false,
+ "mode": 1,
+ "min_occurs": 1,
+ "max_occurs": 1,
+ "translations": null,
+ "data": "World"
+ }
+ ],
+ "outputs": [
+ {
+ "identifier": "output",
+ "title": "Output response",
+ "abstract": "",
+ "keywords": [],
+ "data": null,
+ "data_type": "string",
+ "type": "literal",
+ "uoms": [],
+ "translations": null
+ }
+ ],
+ "store_supported": "true",
+ "status_supported": "true",
+ "profile": [],
+ "translations": null
+ }
+ ],
+ "language": "en-US"
+ }
+
+GET Execute Request URL:
+
+.. code-block::
+
+ http://localhost:5000/wps?/service=wps&version=1.0.0&request=execute&Identifier=say_hello&storeExecuteResponse=true&DataInputs=name=Dude&f=json
+
+GET Execute Response:
+
+.. code-block::
+
+ {
+ "status": {
+ "status": "succeeded",
+ "time": "2021-06-15T14:19:28Z",
+ "percent_done": "100",
+ "message": "PyWPS Process Process Say Hello finished"
+ },
+ "outputs": {
+ "output": "Hello Dude"
+ }
+ }
+
+GET Execute Request URL (Raw output):
+
+.. code-block::
+
+ http://localhost:5000/wps?/service=wps&version=1.0.0&request=execute&Identifier=say_hello&storeExecuteResponse=true&DataInputs=name=Dude&RawDataOutput=output
+
+GET Execute Response:
+
+.. code-block::
+
+ Hello Dude
+
+
+POST request:
+---------------
+
+The default mimetype (input and output formats) can be changed by setting the following headers
+of a POST request to one following values `text/xml` or `application/json`:
+
+ * `Content-Type` (format of the input)
+ * `Accept` (format of the output)
+
+Example of a `Say Hello` POST request:
+
+POST Execute Request URL:
+
+.. code-block::
+
+ http://localhost:5000/jobs
+
+POST Execute Request Body:
+
+.. code-block::
+
+ {
+ "identifier": "say_hello",
+ "inputs": {
+ "name": "Dude"
+ }
+ }
+
+POST Execute Response:
+
+.. code-block::
+
+ {
+ "status": {
+ "status": "succeeded",
+ "time": "2021-06-15T14:19:28Z",
+ "percent_done": "100",
+ "message": "PyWPS Process Process Say Hello finished"
+ },
+ "outputs": {
+ "output": "Hello Dude"
+ }
+ }
+
+
+Example of a `Say Hello` POST request with raw output:
+
+POST Execute Request Body:
+
+.. code-block::
+
+ {
+ "identifier": "say_hello",
+ "outputs": "output",
+ "inputs": {
+ "name": "Dude"
+ }
+ }
+
+
+POST Execute Response:
+
+.. code-block::
+
+ Hello Dude
+
+Alternatively, the `identifier` and optionally the raw output name can be encoded in the Request URL:
+
+POST Execute Request URL (with `identifier`):
+
+.. code-block::
+
+ http://localhost:5000/jobs/say_hello
+
+POST Execute Request Body:
+
+.. code-block::
+
+ {
+ "name": "Dude"
+ }
+
+POST Execute Response:
+
+.. code-block::
+
+ {
+ "status": {
+ "status": "succeeded",
+ "time": "2021-06-15T14:19:28Z",
+ "percent_done": "100",
+ "message": "PyWPS Process Process Say Hello finished"
+ },
+ "outputs": {
+ "output": "Hello Dude"
+ }
+ }
+
+POST Execute Request URL (with `identifier` and output name):
+
+.. code-block::
+
+ http://localhost:5000/jobs/say_hello/output
+
+POST Execute Request Body:
+
+.. code-block::
+
+ {
+ "name": "Dude"
+ }
+
+POST Execute Response:
+
+.. code-block::
+
+ Hello Dude
+
+
+Example for a reference input:
+
+.. code-block::
+
+ "raster": {
+ "type": "reference",
+ "href": "file:./path/to/data/data.tif"
+ }
+
+Example for a BoundingBox input:
+(bbox default axis order is yx (EPSG:4326), i.e. miny, minx, maxy, maxx)
+
+.. code-block::
+
+ "extent": {
+ "type": "bbox",
+ "bbox": [32, 34.7, 32.1, 34.8]
+ }
+
+
+Example for a ComplexInput input:
+(the data is a standard GeoJSON)
+
+.. code-block::
+
+ "cutline": {
+ "type": "complex",
+ "data": {
+ "type": "FeatureCollection",
+ "name": "Center",
+ "crs": {
+ "type": "name",
+ "properties": {
+ "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+ }
+ },
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 34.76844787397541,
+ 32.07247233606565
+ ],
+ [
+ 34.78658619364754,
+ 32.07260143442631
+ ],
+ [
+ 34.77780750512295,
+ 32.09532274590172
+ ],
+ [
+ 34.76844787397541,
+ 32.07247233606565
+ ]
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ }
+
+
+The examples above show some `Literal`, 'Complex', `BoundingBox` inputs.
+Internally, PyWPS always keeps the inputs in JSON formats (also in previous versions)
+So potentially all input types that are supported in XML should also be supported in JSON,
+though only a small subset of them were tested in this preliminary implementation.
+
+Multiple inputs for the same parameter can be passed by using a list as the parameter value.
=====================================
docs/index.rst
=====================================
@@ -38,6 +38,7 @@ Contents:
external-tools
extensions
api
+ api_rest
contributing
exceptions
=====================================
pywps/__init__.py
=====================================
@@ -9,7 +9,7 @@ import os
from lxml.builder import ElementMaker
-__version__ = "4.4.5"
+__version__ = "4.5.0"
LOGGER = logging.getLogger('PYWPS')
LOGGER.debug('setting core variables')
=====================================
pywps/app/Process.py
=====================================
@@ -252,7 +252,7 @@ class Process(object):
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')
@@ -287,7 +287,7 @@ class Process(object):
wps_response._update_status(WPS_STATUS.FAILED, msg, 100)
finally:
- # The run of the next pending request if finished here, weather or not it successfull
+ # The run of the next pending request if finished here, weather or not it successful
self.launch_next_process()
return wps_response
=====================================
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 @@ class Service(object):
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 @@ class Service(object):
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',
=====================================
pywps/app/WPSRequest.py
=====================================
@@ -4,32 +4,37 @@
##################################################################
import logging
+
import lxml
from pywps import xml_util as etree
from werkzeug.exceptions import MethodNotAllowed
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 +42,23 @@ class WPSRequest(object):
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 +76,7 @@ class WPSRequest(object):
"""
# 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 +84,12 @@ class WPSRequest(object):
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 +102,57 @@ class WPSRequest(object):
raise FileSizeExceeded('File size for input exceeded.'
' Maximum request size allowed: {} megabytes'.format(maxsize / 1024 / 1024))
- try:
- doc = 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 = 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)
- 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)
+ 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]
+
+ 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, http_request=self.http_request)
+ 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
@@ -107,9 +166,7 @@ class WPSRequest(object):
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)
+ wpsrequest.default_mimetype = _get_get_param(http_request, 'f', wpsrequest.default_mimetype)
def parse_get_describeprocess(http_request):
"""Parse GET DescribeProcess request
@@ -117,11 +174,11 @@ class WPSRequest(object):
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]
+ wpsrequest.default_mimetype = _get_get_param(http_request, 'f', wpsrequest.default_mimetype)
def parse_get_execute(http_request):
"""Parse GET Execute request
@@ -129,10 +186,7 @@ class WPSRequest(object):
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 +197,29 @@ class WPSRequest(object):
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 +232,8 @@ class WPSRequest(object):
'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 +247,6 @@ class WPSRequest(object):
[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 +254,6 @@ class WPSRequest(object):
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 +264,6 @@ class WPSRequest(object):
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 +308,69 @@ class WPSRequest(object):
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 +397,16 @@ class WPSRequest(object):
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 +445,8 @@ class WPSRequest(object):
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 +467,32 @@ class WPSRequest(object):
: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 +519,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 +543,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 +587,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 +599,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 +608,72 @@ 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')
+ inpt = {'identifier': identifier}
+ if data_type == 'literal':
+ inpt['data'] = inpt_def.get('data')
+ inpt['uom'] = inpt_def.get('uom', '')
+ inpt['datatype'] = inpt_def.get('datatype', '')
+ the_inputs[identifier].append(inpt)
+ elif data_type == 'complex':
+ 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')
+ inpt['data'] = _get_rawvalue_value(inpt_def.get('data', ''), inpt['encoding'])
+ the_inputs[identifier].append(inpt)
+ elif data_type == 'reference':
+ 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)
+ elif data_type == 'bbox':
+ inpt['data'] = inpt_def['bbox']
+ inpt['crs'] = inpt_def.get('crs', 'urn:ogc:def:crs:EPSG::4326')
+ inpt['dimensions'] = inpt_def.get('dimensions', 2)
+ the_inputs[identifier].append(inpt)
+ 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
=====================================
pywps/app/basic.py
=====================================
@@ -7,8 +7,10 @@ XML tools
"""
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 get_xpath_ns(version):
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
=====================================
pywps/configuration.py
=====================================
@@ -24,8 +24,10 @@ RAW_OPTIONS = [('logging', 'format'), ]
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])))
=====================================
pywps/exceptions.py
=====================================
@@ -11,7 +11,10 @@ Based on OGC OWS, WPS and
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 markupsafe import escape
@@ -19,6 +22,7 @@ from markupsafe import escape
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 @@ class NoApplicableCode(HTTPException):
def get_description(self, environ=None):
"""Get the description."""
if self.description:
- return '''<ows:ExceptionText>{}</ows:ExceptionText>'''.format(escape(self.description))
+ return escape(self.description)
else:
return ''
@@ -65,17 +69,26 @@ class NoApplicableCode(HTTPException):
'name': escape(self.name),
'description': self.get_description(environ)
}
- doc = str((
- '<?xml version="1.0" encoding="UTF-8"?>\n'
- '<!-- PyWPS {version} -->\n'
- '<ows:ExceptionReport xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/ows/1.1 http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd" version="1.0.0">\n' # noqa
- ' <ows:Exception exceptionCode="{name}" locator="{locator}" >\n'
- ' {description}\n'
- ' </ows:Exception>\n'
- '</ows:ExceptionReport>'
- ).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((
+ '<?xml version="1.0" encoding="UTF-8"?>\n'
+ '<!-- PyWPS {version} -->\n'
+ '<ows:ExceptionReport xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/ows/1.1 http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd" version="1.0.0">\n' # noqa
+ ' <ows:Exception exceptionCode="{name}" locator="{locator}" >\n'
+ ' <ows:ExceptionText>{description}</ows:ExceptionText>\n'
+ ' </ows:Exception>\n'
+ '</ows:ExceptionReport>'
+ ).format(**args))
+
+ return Response(doc, self.code, mimetype=mimetype)
class InvalidParameterValue(NoApplicableCode):
=====================================
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)
=====================================
pywps/inout/basic.py
=====================================
@@ -2,8 +2,11 @@
# Copyright 2018 Open Source Geospatial Foundation and others #
# licensed under MIT, Please consult LICENSE.txt for details #
##################################################################
+import json
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 +206,12 @@ class IOHandler(object):
"""
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
@@ -316,6 +320,14 @@ class IOHandler(object):
WARNING: may be bytes or str"""
return self._iohandler.data
+ def data_as_json(self):
+ # applies json.loads if needed
+ data = self._iohandler.data
+ if data and not isinstance(self._iohandler, DataHandler) and self.extension in ['.geojson', 'json']:
+ data = json.loads(data)
+ self.data = data # switch to a DataHandler
+ return data
+
@data.setter
def data(self, value):
self._iohandler = DataHandler(value, self)
@@ -460,7 +472,10 @@ class DataHandler(FileHandler):
openmode = self._openmode(self.data)
kwargs = {} if 'b' in openmode else {'encoding': 'utf8'}
with open(self._file, openmode, **kwargs) as fh:
- fh.write(self.data)
+ if isinstance(self.data, (bytes, str)):
+ fh.write(self.data)
+ else:
+ json.dump(self.data, fh)
return self._file
@@ -643,8 +658,8 @@ class BasicIO:
self.title = title
self.abstract = abstract
self.keywords = keywords
- self.min_occurs = int(min_occurs)
- self.max_occurs = int(max_occurs)
+ self.min_occurs = int(min_occurs) if min_occurs is not None else 0
+ 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 +730,7 @@ class BasicComplex(object):
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 +1086,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)
=====================================
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 @@ class Format(object):
self.extension = jsonin['extension']
+Supported_Formats = Optional[Sequence[Union[str, Format]]]
+
+
FORMATS = _FORMATS(
Format('application/geo+json', extension='.geojson'),
Format('application/json', extension='.json'),
=====================================
pywps/inout/inputs.py
=====================================
@@ -83,16 +83,16 @@ class BoundingBoxInput(basic.BBoxInput):
def from_json(cls, json_input):
instance = cls(
identifier=json_input['identifier'],
- title=json_input['title'],
- abstract=json_input['abstract'],
- crss=json_input['crss'],
- keywords=json_input['keywords'],
+ title=json_input.get('title'),
+ abstract=json_input.get('abstract'),
+ crss=json_input.get('crss'),
+ keywords=json_input.get('keywords'),
metadata=[Metadata.from_json(data) for data in json_input.get('metadata', [])],
- dimensions=json_input['dimensions'],
- workdir=json_input['workdir'],
- mode=json_input['mode'],
- min_occurs=json_input['min_occurs'],
- max_occurs=json_input['max_occurs'],
+ dimensions=json_input.get('dimensions'),
+ workdir=json_input.get('workdir'),
+ mode=json_input.get('mode'),
+ min_occurs=json_input.get('min_occurs'),
+ max_occurs=json_input.get('max_occurs'),
translations=json_input.get('translations'),
)
instance.data = json_input['bbox']
@@ -186,6 +186,14 @@ class ComplexInput(basic.ComplexInput):
@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 @@ class ComplexInput(basic.ComplexInput):
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'),
@@ -218,9 +221,10 @@ class ComplexInput(basic.ComplexInput):
elif json_input.get('data'):
data = json_input['data']
# remove cdata tag if it exists (issue #553)
- match = CDATA_PATTERN.match(data)
- if match:
- data = match.group(1)
+ if isinstance(data, str):
+ match = CDATA_PATTERN.match(data)
+ if match:
+ data = match.group(1)
instance.data = data
return instance
@@ -331,7 +335,7 @@ class LiteralInput(basic.LiteralInput):
@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 +355,7 @@ class LiteralInput(basic.LiteralInput):
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 +375,8 @@ class LiteralInput(basic.LiteralInput):
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)
=====================================
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 @@ LOGGER = logging.getLogger('PYWPS')
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
=====================================
pywps/inout/outputs.py
=====================================
@@ -5,6 +5,7 @@
"""
WPS Output classes
"""
+from typing import Optional, Sequence, Dict, Union
from pywps import xml_util as etree
import os
@@ -13,9 +14,10 @@ from pywps.app.Common import Metadata
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, 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 = []
@@ -210,14 +213,12 @@ class ComplexOutput(basic.ComplexOutput):
def _json_data(self, data):
"""Return Data node
"""
- # Match only data that are safe CDATA pattern.
- CDATA_PATTERN = re.compile(r'^<!\[CDATA\[((?!\]\]>).)*\]\]>$')
-
data["type"] = "complex"
if self.data:
-
- if self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
+ if self.data_format.mime_type in [FORMATS.GEOJSON.mime_type, FORMATS.JSON.mime_type]:
+ data["data"] = self.data
+ elif self.data_format.mime_type in ["application/xml", "application/gml+xml", "text/xml"]:
# Note that in a client-server round trip, the original and returned file will not be identical.
data_doc = etree.parse(self.file)
data["data"] = etree.tostring(data_doc, pretty_print=True).decode('utf-8')
@@ -226,6 +227,9 @@ class ComplexOutput(basic.ComplexOutput):
if self.data_format.encoding == 'base64':
data["data"] = self.base64.decode('utf-8')
else:
+ # Match only data that are safe CDATA pattern.
+ CDATA_PATTERN = re.compile(r'^<!\[CDATA\[((?!\]\]>).)*\]\]>$')
+
# Otherwise we assume all other formats are unsafe and need to be enclosed in a CDATA tag.
if isinstance(self.data, bytes):
# Try to inline data as text but if fail encode is in base64
@@ -539,13 +543,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
=====================================
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]]]
=====================================
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 @@ class WPSResponse(object):
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 @@ class WPSResponse(object):
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 @@ class WPSResponse(object):
else:
self._update_status(WPS_STATUS.SUCCEEDED, "Response generated", 100)
- return self.doc
+ return self.doc, self.content_type
=====================================
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 @@ class CapabilitiesResponse(WPSResponse):
'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:
=====================================
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 @@ class DescribeResponse(WPSResponse):
'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:
=====================================
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 @@ class ExecuteResponse(WPSResponse):
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,89 @@ class ExecuteResponse(WPSResponse):
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']
+ d = {}
+ for val in out:
+ id = val.get('identifier')
+ if id is None:
+ continue
+ type = val.get('type')
+ key = 'bbox' if type == 'bbox' else 'data'
+ if key in val:
+ d[id] = val[key]
+ response['outputs'] = d
+ 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,
+ request=self.wps_request,
+ http_request=self.wps_request.http_request)
+ 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):
+ data_format = None
+ if hasattr(wps_output_value, 'output_format'):
+ # this is set in the response, thus should be more precise
+ data_format = wps_output_value.output_format
+ elif hasattr(wps_output_value, 'data_format'):
+ # this is set in the process' response _handler function, thus could have a few supported formats
+ data_format = wps_output_value.data_format
+ if data_format is not None:
+ 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)
=====================================
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
@@ -98,6 +99,16 @@ class WpsClient(Client):
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(Response):
@@ -144,9 +155,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
=====================================
setup.cfg
=====================================
@@ -1,9 +1,9 @@
[bumpversion]
-current_version = 4.4.5
+current_version = 4.5.0
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+).(?P<patch>\d+)
-serialize =
+serialize =
{major}.{minor}.{patch}
[flake8]
=====================================
tests/test_execute.py
=====================================
@@ -17,7 +17,7 @@ from pywps.exceptions import InvalidParameterValue
from pywps import get_inputs_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
@@ -112,12 +112,17 @@ def create_bbox_process():
outputs=[BoundingBoxOutput('outbbox', 'Output message', ["EPSG:4326"])])
-def create_complex_proces():
+def create_complex_proces(mime_type: str = 'gml'):
def complex_proces(request, response):
- response.outputs['complex'].data = request.inputs['complex'][0].data
+ response.outputs['complex'].data = request.inputs['complex'][0].data_as_json()
return response
- frmt = Format(mime_type='application/gml', extension=".gml") # this is unknown mimetype
+ if mime_type == 'gml':
+ frmt = Format(mime_type='application/gml', extension=".gml") # this is unknown mimetype
+ elif mime_type == 'geojson':
+ frmt = FORMATS.GEOJSON
+ else:
+ raise Exception(f'Unknown mime type {mime_type}')
return Process(handler=complex_proces,
identifier='my_complex_process',
@@ -420,31 +425,52 @@ class ExecuteTest(unittest.TestCase):
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()]))
@@ -478,6 +504,47 @@ class ExecuteTest(unittest.TestCase):
upper_corner = upper_corner.strip().replace(' ', ' ')
self.assertEqual('16.0 51.0', upper_corner)
+ def test_bbox_rest(self):
+ client = client_for(Service(processes=[create_bbox_process()]))
+ bbox = [15.0, 50.0, 16.0, 51.0]
+ request = dict(
+ identifier='my_bbox_process',
+ version='1.0.0',
+ inputs={
+ 'mybbox': {
+ "type": "bbox",
+ 'bbox': bbox,
+ }
+ },
+ )
+ result = {'outbbox': bbox}
+
+ resp = client.post_json(doc=request)
+ assert_response_success_json(resp, result)
+
+ def test_geojson_input_rest(self):
+ geojson = os.path.join(DATA_DIR, 'json', 'point.geojson')
+ with open(geojson) as f:
+ p = json.load(f)
+
+ my_process = create_complex_proces('geojson')
+ client = client_for(Service(processes=[my_process]))
+
+ request = dict(
+ identifier='my_complex_process',
+ version='1.0.0',
+ inputs={
+ 'complex': {
+ "type": "complex",
+ 'data': p
+ }
+ },
+ )
+ result = {'complex': p}
+
+ resp = client.post_json(doc=request)
+ assert_response_success_json(resp, result)
+
def test_output_response_dataType(self):
client = client_for(Service(processes=[create_greeter()]))
request_doc = WPS.Execute(
@@ -495,6 +562,39 @@ class ExecuteTest(unittest.TestCase):
assert el.attrib['dataType'] == 'string'
+class AllowedInputReferenceExecuteTest(unittest.TestCase):
+
+ def setUp(self):
+ self.allowedinputpaths = configuration.get_config_value('server', 'allowedinputpaths')
+ configuration.CONFIG.set('server', 'allowedinputpaths', DATA_DIR)
+
+ def tearDown(self):
+ configuration.CONFIG.set('server', 'allowedinputpaths', self.allowedinputpaths)
+
+ def test_geojson_input_reference_rest(self):
+ geojson = os.path.join(DATA_DIR, 'json', 'point.geojson')
+ with open(geojson) as f:
+ p = json.load(f)
+
+ my_process = create_complex_proces('geojson')
+ client = client_for(Service(processes=[my_process]))
+
+ request = dict(
+ identifier='my_complex_process',
+ version='1.0.0',
+ inputs={
+ 'complex': {
+ "type": "reference",
+ "href": f"file:{geojson}"
+ }
+ },
+ )
+ result = {'complex': p}
+
+ resp = client.post_json(doc=request)
+ assert_response_success_json(resp, result)
+
+
class ExecuteTranslationsTest(unittest.TestCase):
def setUp(self):
=====================================
tests/test_processing.py
=====================================
@@ -15,7 +15,6 @@ from pywps import configuration
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
View it on GitLab: https://salsa.debian.org/debian-gis-team/pywps/-/compare/ca747b100bb3c92551dbff5358b1798b14d4859e...32fed6c2cbeedf03a46541b63638673d6d62d607
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pywps/-/compare/ca747b100bb3c92551dbff5358b1798b14d4859e...32fed6c2cbeedf03a46541b63638673d6d62d607
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210812/9b01364d/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list