Skip to content
Commits on Source (6)
......@@ -12,7 +12,11 @@ docs/_build
.DS_Store
.*.un~
# project
.idea
# git
*.orig
.coverage
.pytest_cache
......@@ -4,7 +4,6 @@ sudo: required
dist: trusty
python:
- "2.7"
- "3.6"
git:
......
pywps (4.2.3-2) UNRELEASED; urgency=medium
pywps (4.2.4-1) unstable; urgency=medium
* New upstream release.
* Drop Name field from upstream metadata.
* Bump Standards-Version to 4.5.0, no changes.
* Update copyright years for OSGeo.
* Drop version.patch, fixed upstream. Refresh remaining patches.
-- Bas Couwenberg <sebastic@debian.org> Mon, 09 Dec 2019 09:41:10 +0100
-- Bas Couwenberg <sebastic@debian.org> Mon, 10 Feb 2020 05:46:51 +0100
pywps (4.2.3-1) unstable; urgency=medium
......
......@@ -4,7 +4,7 @@ Upstream-Contact: PyWPS Development Team <pywps-dev@lists.osgeo.org>
Source: https://github.com/geopython/pywps/releases
Files: *
Copyright: 2018, Open Source Geospatial Foundation and others
Copyright: 2018-2019, Open Source Geospatial Foundation and others
2014-2016, PyWPS Development Team, represented by PyWPS Project Steering Committee
License: Expat
......
......@@ -41,7 +41,7 @@ Author: Bas Couwenberg <sebastic@debian.org>
opendap_input.url = "http://test.opendap.org:80/opendap/netcdf/examples/sresa1b_ncar_ccsm3_0_run1_200001.nc"
--- a/tests/test_execute.py
+++ b/tests/test_execute.py
@@ -199,6 +199,7 @@ def get_output(doc):
@@ -233,6 +233,7 @@ def get_output(doc):
class ExecuteTest(unittest.TestCase):
"""Test for Exeucte request KVP request"""
......@@ -59,7 +59,7 @@ Author: Bas Couwenberg <sebastic@debian.org>
def test_url(self):
if not service_ok('https://demo.mapserver.org'):
self.skipTest("mapserver is unreachable")
@@ -534,6 +535,7 @@ class ComplexOutputTest(unittest.TestCas
@@ -538,6 +539,7 @@ class ComplexOutputTest(unittest.TestCas
else:
self.assertEqual(base64.b64decode(b).decode(), self.data)
......
offline-tests.patch
use-mathjax-package.patch
privacy-breach.patch
version.patch
Description: Fix version.
Author: Bas Couwenberg <sebastic@debian.org>
Bug: https://github.com/geopython/pywps/issues/500
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-4.3
+4.2.3
......@@ -31,6 +31,7 @@ outputpath=outputs
workdir=workdir
maxprocesses=10
parallelprocesses=2
storagetype=file
[processing]
mode=default
......@@ -44,3 +45,11 @@ format=%(asctime)s] [%(levelname)s] file=%(pathname)s line=%(lineno)s module=%(m
[grass]
gisbase=/usr/local/grass-7.3.svn/
[s3]
bucket=my-org-wps
region=us-east-1
prefix=appname/coolapp/
public=true
encrypt=false
......@@ -20,6 +20,7 @@ The configuration file has several sections:
* `logging` for logging configuration
* `grass` for *optional* configuration to support `GRASS GIS
<https://grass.osgeo.org>`_
* `s3` for *optional* configuration to support AWS S3 storage
PyWPS ships with a sample configuration file (``default-sample.cfg``).
A similar file is also available in the `flask` service as
......@@ -85,6 +86,8 @@ configuration file <https://docs.pycsw.org/en/latest/configuration.html>`_.
``pointOfContact``, ``distributor``, ``user``, ``resourceProvider``,
``originator``, ``owner``, ``principalInvestigator``
.. _server-configuration:
[server]
--------
......@@ -92,7 +95,8 @@ configuration file <https://docs.pycsw.org/en/latest/configuration.html>`_.
the URL of the WPS service endpoint
:language:
the ISO 639-1 language and ISO 3166-1 alpha2 country code of the service
a comma-separated list of ISO 639-1 language and ISO 3166-1 alpha2 country
code of the service
(e.g. ``en-CA``, ``fr-CA``, ``en-US``)
:encoding:
......@@ -146,7 +150,8 @@ configuration file <https://docs.pycsw.org/en/latest/configuration.html>`_.
Example: `outputpath=/var/www/wps/outputs` shall correspond with
`outputurl=http://foo.bar/wps/outputs`
:storagetype:
The type of storage to use when storing status and results. Possible values are: ``file``, ``s3``. Defaults to ``file``.
[processing]
------------
......@@ -193,6 +198,25 @@ configuration file <https://docs.pycsw.org/en/latest/configuration.html>`_.
directory of the GRASS GIS instalation, refered as `GISBASE
<https://grass.osgeo.org/grass73/manuals/variables.html>`_
[s3]
----
:bucket:
Name of the bucket to store files in. e.g. ``my-wps-results``
:region:
Region in which the bucket refered to above exists. e.g. ``us-east-1``
:public:
Set this to ``true`` if public access to status and result files is desired. Defaults to ``false``.
:prefix:
Prefix to prepend to all file paths written to the S3 bucket by PyWPS. e.g. ``wps/results``
:encrypt:
Set this to ``true`` if encryption at rest is desired. Defaults to ``false``
-----------
Sample file
-----------
......@@ -212,6 +236,7 @@ Sample file
outputpath=/tmp/outputs/
workdir=
allowedinputpaths=/tmp
storagetype=file
[metadata:main]
identification_title=PyWPS Processing Service
......@@ -248,3 +273,11 @@ Sample file
[grass]
gisbase=/usr/local/grass-7.3.svn/
[s3]
bucket=my-org-wps
region=us-east-1
prefix=appname/coolapp/
public=true
encrypt=false
......@@ -474,6 +474,64 @@ Use the `run` method to start the server::
To make the server visible from another computer, replace ``localhost`` with ``0.0.0.0``.
Supporting multiple languages
=============================
Supporting multiple languages requires:
- Setting the `language` property in the server configuration (see :ref:`server-configuration`)
- Adding translations to :class:`Process`, inputs and outputs objects
The expected translations format is always the same. The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property::
from pywps import Process, LiteralInput, LiteralOutput
class SayHello(Process):
def __init__(self):
inputs = [
LiteralInput(
'name',
title='Input name',
abstract='The name to say hello to.',
translations={"fr-CA": {"abstract": "Le nom à saluer."}}
)
],
outputs=[
LiteralOutput(
'response',
title='Output response',
abstract='The complete output message.',
translations={"fr-CA": {
"title": "La réponse",
"abstract": "Le message complet."
}}
)
],
super().__init__(
self._handler,
identifier='say_hello',
title='Process Say Hello',
abstract='Returns a literal string output with Hello plus the inputed name',
version='1.0',
inputs=inputs,
outputs=outputs,
store_supported=True,
status_supported=True,
translations={"fr-CA": {
"title": "Processus Dire Bonjour",
"abstract": "Retourne une chaine de caractères qui dit bonjour au nom fournit en entrée."
}},
)
def _handler(self, request, response):
...
The translation will default to the untranslated attribute of the base object if
the key is not provided in the `translations` dictionnary.
Automated process documentation
===============================
......
.. currentmodule:: pywps
.. _storage:
Storage
#######
.. todo::
* Local file storage
In PyWPS, storage covers the storage of both the results that we want to return to the user and the storage of the execution status of each process.
AWS S3
-------
Amazon Web Services Simple Storage Service (AWS S3) can be used to store both process execution status XML documents and process result files. By using S3 we can allow easy public read access to process status and results on S3 using a variety of tools including the web browser, the AWS SDK and the AWS CLI.
For more information about AWS S3 please see https://aws.amazon.com/s3/ and for information about working with an S3 bucket see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html
Requirements
=============
In order to work with S3 storage, you must first create an S3 bucket. https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html#create-bucket-intro
PyWPS uses the boto3 library to send requests to AWS. In order to make requests boto3 requires credentials which grant read and write access to the S3 bucket. Please see the boto3 guide on credentials for options on how to configure the credentials for your application. https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html
An example of an IAM policy that will allow PyWPS to read and write to the S3 Bucket is described here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html
``{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListObjectsInBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": ["arn:aws:s3:::bucket-name"]
},
{
"Sid": "AllObjectActions",
"Effect": "Allow",
"Action": "s3:*Object",
"Resource": ["arn:aws:s3:::bucket-name/*"]
}
]
}``
......@@ -5,15 +5,13 @@
import logging
import os
from pywps.translations import lower_case_dict
import sys
import traceback
import json
import shutil
import time
import tempfile
import importlib
from pywps import get_ElementMakerForVersion, E, dblog
from pywps import dblog
from pywps.response import get_response
from pywps.response.status import WPS_STATUS
from pywps.response.execute import ExecuteResponse
......@@ -25,6 +23,8 @@ from pywps._compat import PY2
from pywps.exceptions import (StorageNotSupported, OperationNotSupported,
ServerBusy, NoApplicableCode)
from pywps.app.exceptions import ProcessError
from pywps.inout.storage.builder import StorageBuilder
import importlib
LOGGER = logging.getLogger("PYWPS")
......@@ -50,10 +50,14 @@ class Process(object):
objects.
:param metadata: List of metadata advertised by this process. They
should be :class:`pywps.app.Common.Metadata` objects.
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}}
"""
def __init__(self, handler, identifier, title, abstract='', keywords=[], profile=[], metadata=[], inputs=[],
outputs=[], version='None', store_supported=False, status_supported=False, grass_location=None):
def __init__(self, handler, identifier, title, abstract='', keywords=[], profile=[],
metadata=[], inputs=[], outputs=[], version='None', store_supported=False,
status_supported=False, grass_location=None, translations=None):
self.identifier = identifier
self.handler = handler
self.title = title
......@@ -65,12 +69,14 @@ class Process(object):
self.inputs = inputs
self.outputs = outputs
self.uuid = None
self.status_location = ''
self.status_url = ''
self.status_store = None
# self.status_location = ''
# self.status_url = ''
self.workdir = None
self._grass_mapset = None
self.grass_location = grass_location
self.service = None
self.translations = lower_case_dict(translations)
if store_supported:
self.store_supported = 'true'
......@@ -100,6 +106,7 @@ class Process(object):
'store_supported': self.store_supported,
'status_supported': self.status_supported,
'profile': [p for p in self.profile],
'translations': self.translations,
}
@classmethod
......@@ -117,6 +124,7 @@ class Process(object):
def execute(self, wps_request, uuid):
self._set_uuid(uuid)
self._setup_status_storage()
self.async_ = False
response_cls = get_response("execute")
wps_response = response_cls(wps_request, process=self, uuid=self.uuid)
......@@ -152,12 +160,20 @@ class Process(object):
for outpt in self.outputs:
outpt.uuid = uuid
file_path = config.get_config_value('server', 'outputpath')
def _setup_status_storage(self):
self.status_store = StorageBuilder.buildStorage()
@property
def status_location(self):
return self.status_store.location(self.status_filename)
file_url = config.get_config_value('server', 'outputurl')
@property
def status_filename(self):
return str(self.uuid) + '.xml'
self.status_location = os.path.join(file_path, str(self.uuid)) + '.xml'
self.status_url = os.path.join(file_url, str(self.uuid)) + '.xml'
@property
def status_url(self):
return self.status_store.url(self.status_filename)
def _execute_process(self, async_, wps_request, wps_response):
"""Uses :module:`pywps.processing` module for sending process to
......@@ -289,6 +305,7 @@ class Process(object):
process_identifier = new_wps_request.identifier
process = self.service.prepare_process_for_execution(process_identifier)
process._set_uuid(uuid)
process._setup_status_storage()
process.async_ = True
new_wps_response = ExecuteResponse(new_wps_request, process=process, uuid=uuid)
new_wps_response.store_status_file = True
......
......@@ -111,6 +111,9 @@ 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)
def parse_get_describeprocess(http_request):
"""Parse GET DescribeProcess request
"""
......@@ -191,6 +194,9 @@ class WPSRequest(object):
map(lambda v: v.text, 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
"""
......@@ -208,7 +214,6 @@ class WPSRequest(object):
def parse_post_execute(doc):
"""Parse POST Execute request
"""
version = doc.attrib.get('version')
wpsrequest.check_and_set_version(version)
......@@ -300,13 +305,19 @@ class WPSRequest(object):
def check_and_set_language(self, language):
"""set this.language
"""
supported_languages = configuration.get_config_value('server', 'language').split(',')
supported_languages = [lang.strip() for lang in supported_languages]
if not language:
language = 'None'
elif language != 'en-US':
# default to the first supported language
language = supported_languages[0]
if language not in supported_languages:
raise InvalidParameterValue(
'The requested language "{}" is not supported by this server'.format(language), 'language')
else:
'The requested language "{}" is not supported by this server'.format(language),
'language',
)
self.language = language
@property
......
......@@ -95,6 +95,7 @@ def load_configuration(cfgfiles=None):
# If this flag is enabled PyWPS will remove the process temporary workdir
# after process has finished.
CONFIG.set('server', 'cleantempdir', 'true')
CONFIG.set('server', 'storagetype', 'file')
CONFIG.add_section('processing')
CONFIG.set('processing', 'mode', 'default')
......@@ -134,6 +135,13 @@ def load_configuration(cfgfiles=None):
CONFIG.add_section('grass')
CONFIG.set('grass', 'gisbase', '')
CONFIG.add_section('s3')
CONFIG.set('s3', 'bucket', '')
CONFIG.set('s3', 'prefix', '')
CONFIG.set('s3', 'public', 'false')
CONFIG.set('s3', 'encrypt', 'false')
CONFIG.set('s3', 'region', '')
if not cfgfiles:
cfgfiles = _get_default_config_files_location()
......
......@@ -3,6 +3,7 @@
# licensed under MIT, Please consult LICENSE.txt for details #
##################################################################
from pywps.translations import lower_case_dict
from pywps._compat import text_type, StringIO
import os
from io import open
......@@ -543,8 +544,9 @@ class SimpleHandler(DataHandler):
class BasicIO:
"""Basic Input/Output class
"""
def __init__(self, identifier, title=None, abstract=None, keywords=None,
min_occurs=1, max_occurs=1, metadata=[]):
min_occurs=1, max_occurs=1, metadata=[], translations=None):
self.identifier = identifier
self.title = title
self.abstract = abstract
......@@ -552,6 +554,7 @@ class BasicIO:
self.min_occurs = int(min_occurs)
self.max_occurs = int(max_occurs)
self.metadata = metadata
self.translations = lower_case_dict(translations)
class BasicLiteral:
......@@ -701,9 +704,17 @@ class LiteralInput(BasicIO, BasicLiteral, SimpleHandler):
data_type="integer", workdir=None, allowed_values=None,
uoms=None, mode=MODE.NONE,
min_occurs=1, max_occurs=1, metadata=[],
default=None, default_type=SOURCE_TYPE.DATA):
BasicIO.__init__(self, identifier, title, abstract, keywords,
min_occurs, max_occurs, metadata)
default=None, default_type=SOURCE_TYPE.DATA, translations=None):
BasicIO.__init__(self,
identifier=identifier,
title=title,
abstract=abstract,
keywords=keywords,
min_occurs=min_occurs,
max_occurs=max_occurs,
metadata=metadata,
translations=translations,
)
BasicLiteral.__init__(self, data_type, uoms)
SimpleHandler.__init__(self, workdir, data_type, mode=mode)
......@@ -752,8 +763,8 @@ class LiteralOutput(BasicIO, BasicLiteral, SimpleHandler):
def __init__(self, identifier, title=None, abstract=None, keywords=None,
data_type=None, workdir=None, uoms=None, validate=None,
mode=MODE.NONE):
BasicIO.__init__(self, identifier, title, abstract, keywords)
mode=MODE.NONE, translations=None):
BasicIO.__init__(self, identifier, title, abstract, keywords, translations=translations)
BasicLiteral.__init__(self, data_type, uoms)
SimpleHandler.__init__(self, workdir=None, data_type=data_type,
mode=mode)
......@@ -784,9 +795,17 @@ class BBoxInput(BasicIO, BasicBoundingBox, DataHandler):
dimensions=None, workdir=None,
mode=MODE.SIMPLE,
min_occurs=1, max_occurs=1, metadata=[],
default=None, default_type=SOURCE_TYPE.DATA):
BasicIO.__init__(self, identifier, title, abstract, keywords,
min_occurs, max_occurs, metadata)
default=None, default_type=SOURCE_TYPE.DATA, translations=None):
BasicIO.__init__(self,
identifier=identifier,
title=title,
abstract=abstract,
keywords=keywords,
min_occurs=min_occurs,
max_occurs=max_occurs,
metadata=metadata,
translations=translations,
)
BasicBoundingBox.__init__(self, crss, dimensions)
DataHandler.__init__(self, workdir=workdir, mode=mode)
......@@ -804,8 +823,8 @@ class BBoxOutput(BasicIO, BasicBoundingBox, DataHandler):
"""
def __init__(self, identifier, title=None, abstract=None, keywords=None, crss=None,
dimensions=None, workdir=None, mode=MODE.NONE):
BasicIO.__init__(self, identifier, title, abstract, keywords)
dimensions=None, workdir=None, mode=MODE.NONE, translations=None):
BasicIO.__init__(self, identifier, title, abstract, keywords, translations=translations)
BasicBoundingBox.__init__(self, crss, dimensions)
DataHandler.__init__(self, workdir=workdir, mode=mode)
self._storage = None
......@@ -832,10 +851,17 @@ class ComplexInput(BasicIO, BasicComplex, IOHandler):
workdir=None, data_format=None, supported_formats=None,
mode=MODE.NONE,
min_occurs=1, max_occurs=1, metadata=[],
default=None, default_type=SOURCE_TYPE.DATA):
BasicIO.__init__(self, identifier, title, abstract, keywords,
min_occurs, max_occurs, metadata)
default=None, default_type=SOURCE_TYPE.DATA, translations=None):
BasicIO.__init__(self,
identifier=identifier,
title=title,
abstract=abstract,
keywords=keywords,
min_occurs=min_occurs,
max_occurs=max_occurs,
metadata=metadata,
translations=translations,
)
IOHandler.__init__(self, workdir=workdir, mode=mode)
BasicComplex.__init__(self, data_format, supported_formats)
......@@ -944,8 +970,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):
BasicIO.__init__(self, identifier, title, abstract, keywords)
mode=MODE.NONE, 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)
......@@ -965,7 +991,9 @@ class ComplexOutput(BasicIO, BasicComplex, IOHandler):
def get_url(self):
"""Return URL pointing to data
"""
(outtype, storage, url) = self.storage.store(self)
# TODO: it is not obvious that storing happens here
(_, _, url) = self.storage.store(self)
# url = self.storage.url(self)
return url
......
......@@ -18,7 +18,8 @@ import mimetypes
_FORMATS = namedtuple('FORMATS', 'GEOJSON, JSON, SHP, GML, METALINK, META4, KML, KMZ, GEOTIFF,'
'WCS, WCS100, WCS110, WCS20, WFS, WFS100,'
'WFS110, WFS20, WMS, WMS130, WMS110,'
'WMS100, TEXT, DODS, NETCDF, LAZ, LAS, ZIP')
'WMS100, TEXT, DODS, NETCDF, LAZ, LAS, ZIP,'
'XML')
class Format(object):
......@@ -128,6 +129,8 @@ class Format(object):
def same_as(self, frmt):
"""Check input frmt, if it seems to be the same as self
"""
if not isinstance(frmt, Format):
return False
return all([frmt.mime_type == self.mime_type,
frmt.encoding == self.encoding,
frmt.schema == self.schema])
......@@ -186,7 +189,8 @@ FORMATS = _FORMATS(
Format('application/x-netcdf', extension='.nc', encoding='base64'),
Format('application/octet-stream', extension='.laz'),
Format('application/octet-stream', extension='.las'),
Format('application/zip', extension='.zip', encoding='base64')
Format('application/zip', extension='.zip', encoding='base64'),
Format('application/xml', extension='.xml'),
)
......
......@@ -31,20 +31,25 @@ class BoundingBoxInput(basic.BBoxInput):
:param int max_occurs: how many times this input occurs
:param metadata: List of metadata advertised by this process. They
should be :class:`pywps.app.Common.Metadata` objects.
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}}
"""
def __init__(self, identifier, title, crss=None, abstract='', keywords=[],
dimensions=2, workdir=None, metadata=[], min_occurs=1,
max_occurs=1,
mode=MODE.NONE,
default=None, default_type=basic.SOURCE_TYPE.DATA):
default=None, default_type=basic.SOURCE_TYPE.DATA,
translations=None):
basic.BBoxInput.__init__(self, identifier, title=title, crss=crss,
abstract=abstract, keywords=keywords,
dimensions=dimensions, workdir=workdir, metadata=metadata,
min_occurs=min_occurs, max_occurs=max_occurs,
mode=mode, default=default,
default_type=default_type)
default_type=default_type,
translations=translations)
self.as_reference = False
......@@ -68,7 +73,8 @@ class BoundingBoxInput(basic.BBoxInput):
'workdir': self.workdir,
'mode': self.valid_mode,
'min_occurs': self.min_occurs,
'max_occurs': self.max_occurs
'max_occurs': self.max_occurs,
'translations': self.translations,
}
@classmethod
......@@ -85,6 +91,7 @@ class BoundingBoxInput(basic.BBoxInput):
mode=json_input['mode'],
min_occurs=json_input['min_occurs'],
max_occurs=json_input['max_occurs'],
translations=json_input.get('translations'),
)
instance.data = json_input['bbox']
......@@ -112,12 +119,15 @@ class ComplexInput(basic.ComplexInput):
:param int min_occurs: minimum occurrence
:param int max_occurs: maximum occurrence
:param pywps.validator.mode.MODE mode: validation mode (none to strict)
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}}
"""
def __init__(self, identifier, title, supported_formats,
data_format=None, abstract='', keywords=[], workdir=None, metadata=[], min_occurs=1,
max_occurs=1, mode=MODE.NONE,
default=None, default_type=basic.SOURCE_TYPE.DATA):
default=None, default_type=basic.SOURCE_TYPE.DATA, translations=None):
"""constructor"""
basic.ComplexInput.__init__(self, identifier, title=title,
......@@ -126,7 +136,7 @@ class ComplexInput(basic.ComplexInput):
keywords=keywords, workdir=workdir, metadata=metadata,
min_occurs=min_occurs,
max_occurs=max_occurs, mode=mode,
default=default, default_type=default_type)
default=default, default_type=default_type, translations=translations)
self.as_reference = False
self.method = ''
......@@ -148,7 +158,8 @@ class ComplexInput(basic.ComplexInput):
'workdir': self.workdir,
'mode': self.valid_mode,
'min_occurs': self.min_occurs,
'max_occurs': self.max_occurs
'max_occurs': self.max_occurs,
'translations': self.translations,
}
if self.prop == 'file':
......@@ -194,7 +205,8 @@ class ComplexInput(basic.ComplexInput):
encoding=infrmt.get('encoding')
) for infrmt in json_input['supported_formats']
],
mode=json_input.get('mode', MODE.NONE)
mode=json_input.get('mode', MODE.NONE),
translations=json_input.get('translations'),
)
instance.as_reference = json_input.get('asreference', False)
if json_input.get('file'):
......@@ -254,14 +266,16 @@ class LiteralInput(basic.LiteralInput):
:param pywps.inout.literaltypes.AnyValue allowed_values: or :py:class:`pywps.inout.literaltypes.AllowedValue` object
:param metadata: List of metadata advertised by this process. They
should be :class:`pywps.app.Common.Metadata` objects.
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}}
"""
def __init__(self, identifier, title=None, data_type=None, workdir=None, abstract='', keywords=[],
metadata=[], uoms=None,
min_occurs=1, max_occurs=1,
mode=MODE.SIMPLE, allowed_values=None,
default=None, default_type=basic.SOURCE_TYPE.DATA):
default=None, default_type=basic.SOURCE_TYPE.DATA, translations=None):
"""Constructor
"""
data_type = data_type or 'string'
......@@ -271,7 +285,8 @@ class LiteralInput(basic.LiteralInput):
uoms=uoms, min_occurs=min_occurs,
max_occurs=max_occurs, mode=mode,
allowed_values=allowed_values,
default=default, default_type=default_type)
default=default, default_type=default_type,
translations=translations)
self.as_reference = False
......@@ -293,6 +308,7 @@ class LiteralInput(basic.LiteralInput):
'mode': self.valid_mode,
'min_occurs': self.min_occurs,
'max_occurs': self.max_occurs,
'translations': self.translations,
# other values not set in the constructor
}
if self.values_reference:
......
......@@ -11,10 +11,10 @@ import os
from pywps.app.Common import Metadata
from pywps.exceptions import InvalidParameterValue
from pywps.inout import basic
from pywps.inout.formats import Format
from pywps.inout.storage import FileStorage
from pywps.inout.storage.file import FileStorageBuilder
from pywps.validator.mode import MODE
from pywps import configuration as config
from pywps.inout.formats import Format
class BoundingBoxOutput(basic.BBoxOutput):
......@@ -29,15 +29,18 @@ class BoundingBoxOutput(basic.BBoxOutput):
:param pywps.validator.mode.MODE mode: validation mode (none to strict)
:param metadata: List of metadata advertised by this process. They
should be :class:`pywps.app.Common.Metadata` objects.
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}}
"""
def __init__(self, identifier, title, crss, abstract='', keywords=[],
dimensions=2, metadata=[], min_occurs='1',
max_occurs='1', as_reference=False,
mode=MODE.NONE):
mode=MODE.NONE, translations=None):
basic.BBoxOutput.__init__(self, identifier, title=title,
abstract=abstract, keywords=keywords, crss=crss,
dimensions=dimensions, mode=mode)
dimensions=dimensions, mode=mode, translations=translations)
self.metadata = metadata
self.min_occurs = min_occurs
......@@ -65,6 +68,7 @@ class BoundingBoxOutput(basic.BBoxOutput):
'ur': self.ur,
'workdir': self.workdir,
'mode': self.valid_mode,
'translations': self.translations,
}
@classmethod
......@@ -80,6 +84,7 @@ class BoundingBoxOutput(basic.BBoxOutput):
crss=json_output['crss'],
dimensions=json_output['dimensions'],
mode=json_output['mode'],
translations=json_output.get('translations'),
)
instance.data = json_output['bbox']
instance.workdir = json_output['workdir']
......@@ -98,11 +103,14 @@ class ComplexOutput(basic.ComplexOutput):
:param pywps.validator.mode.MODE mode: validation mode (none to strict)
:param metadata: List of metadata advertised by this process. They
should be :class:`pywps.app.Common.Metadata` objects.
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
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):
as_reference=False, mode=MODE.NONE, translations=None):
if metadata is None:
metadata = []
......@@ -110,7 +118,7 @@ class ComplexOutput(basic.ComplexOutput):
data_format=data_format, abstract=abstract, keywords=keywords,
workdir=workdir,
supported_formats=supported_formats,
mode=mode)
mode=mode, translations=translations)
self.metadata = metadata
self.as_reference = as_reference
......@@ -133,7 +141,8 @@ class ComplexOutput(basic.ComplexOutput):
'workdir': self.workdir,
'mode': self.valid_mode,
'min_occurs': self.min_occurs,
'max_occurs': self.max_occurs
'max_occurs': self.max_occurs,
'translations': self.translations,
}
if self.as_reference:
......@@ -174,7 +183,8 @@ class ComplexOutput(basic.ComplexOutput):
encoding=infrmt.get('encoding')
) for infrmt in json_output['supported_formats']
],
mode=json_output.get('mode', MODE.NONE)
mode=json_output.get('mode', MODE.NONE),
translations=json_output.get('translations'),
)
instance.as_reference = json_output.get('asreference', False)
if json_output.get('file'):
......@@ -191,7 +201,7 @@ class ComplexOutput(basic.ComplexOutput):
if self.prop == 'url':
data["href"] = self.url
elif self.prop is not None:
self.storage = FileStorage()
self.storage = FileStorageBuilder().build()
data["href"] = self.get_url()
return data
......@@ -235,14 +245,17 @@ class LiteralOutput(basic.LiteralOutput):
:param pywps.validator.mode.MODE mode: validation mode (none to strict)
:param metadata: List of metadata advertised by this process. They
should be :class:`pywps.app.Common.Metadata` objects.
:param dict[str,dict[str,str]] translations: The first key is the RFC 4646 language code,
and the nested mapping contains translated strings accessible by a string property.
e.g. {"fr-CA": {"title": "Mon titre", "abstract": "Une description"}}
"""
def __init__(self, identifier, title, data_type='string', abstract='', keywords=[],
metadata=[], uoms=None, mode=MODE.SIMPLE):
metadata=[], uoms=None, mode=MODE.SIMPLE, translations=None):
if uoms is None:
uoms = []
basic.LiteralOutput.__init__(self, identifier, title=title, abstract=abstract, keywords=keywords,
data_type=data_type, uoms=uoms, mode=mode)
data_type=data_type, uoms=uoms, mode=mode, translations=translations)
self.metadata = metadata
@property
......@@ -257,7 +270,8 @@ class LiteralOutput(basic.LiteralOutput):
"data": self.data,
"data_type": self.data_type,
"type": "literal",
"uoms": [u.json for u in self.uoms]
"uoms": [u.json for u in self.uoms],
"translations": self.translations,
}
if self.uom:
......@@ -277,6 +291,7 @@ class LiteralOutput(basic.LiteralOutput):
abstract=json_output['abstract'],
keywords=json_output['keywords'],
uoms=uoms,
translations=json_output.get('translations'),
)
instance.data = json_output.get('data')
......@@ -288,6 +303,7 @@ class LiteralOutput(basic.LiteralOutput):
class MetaFile:
"""MetaFile object."""
def __init__(self, identity=None, description=None, fmt=None):
"""Create a `MetaFile` object.
......
##################################################################
# Copyright 2018 Open Source Geospatial Foundation and others #
# licensed under MIT, Please consult LICENSE.txt for details #
##################################################################
import logging
import os
from abc import ABCMeta, abstractmethod
LOGGER = logging.getLogger('PYWPS')
class STORE_TYPE:
PATH = 0
S3 = 1
# TODO: cover with tests
class StorageAbstract(object):
"""Data storage abstract class
"""
__metaclass__ = ABCMeta
@abstractmethod
def store(self, output):
"""
:param output: of type IOHandler
:returns: (type, store, url) where
type - is type of STORE_TYPE - number
store - string describing storage - file name, database connection
url - url, where the data can be downloaded
"""
raise NotImplementedError
@abstractmethod
def write(self, data, destination, data_format=None):
"""
:param data: data to write to storage
:param destination: identifies the destination to write to storage
generally a file name which can be interpreted
by the implemented Storage class in a manner of
its choosing
:param data_format: Optional parameter of type pywps.inout.formats.FORMAT
describing the format of the data to write.
:returns: url where the data can be downloaded
"""
raise NotImplementedError
@abstractmethod
def url(self, destination):
"""
:param destination: the name of the output to calculate
the url for
:returns: URL where file_name can be reached
"""
raise NotImplementedError
@abstractmethod
def location(self, destination):
"""
Provides a location for the specified destination.
This may be any path, pathlike object, db connection string, URL, etc
and it is not guaranteed to be accessible on the local file system
:param destination: the name of the output to calculate
the location for
:returns: location where file_name can be found
"""
raise NotImplementedError
class CachedStorage(StorageAbstract):
def __init__(self):
self._cache = {}
def store(self, output):
if output.identifier not in self._cache:
self._cache[output.identifier] = self._do_store(output)
return self._cache[output.identifier]
def _do_store(self, output):
raise NotImplementedError
class DummyStorage(StorageAbstract):
"""Dummy empty storage implementation, does nothing
Default instance, for non-reference output request
>>> store = DummyStorage()
>>> assert store.store
"""
def __init__(self):
"""
"""
def store(self, output):
pass