[Git][debian-gis-team/pywps][experimental] 5 commits: New upstream version 4.4.1
Bas Couwenberg
gitlab at salsa.debian.org
Mon Mar 22 04:49:23 GMT 2021
Bas Couwenberg pushed to branch experimental at Debian GIS Project / pywps
Commits:
f6a9f37c by Bas Couwenberg at 2021-03-22T05:38:08+01:00
New upstream version 4.4.1
- - - - -
4c591354 by Bas Couwenberg at 2021-03-22T05:38:11+01:00
Update upstream source from tag 'upstream/4.4.1'
Update to upstream version '4.4.1'
with Debian dir bffd7e2bc517e28179ffa849218b5cd0474b0b39
- - - - -
df735917 by Bas Couwenberg at 2021-03-22T05:38:25+01:00
New upstream release.
- - - - -
d705dc96 by Bas Couwenberg at 2021-03-22T05:40:11+01:00
Add python3-humanize to (build) dependencies.
- - - - -
ae4fc8bc by Bas Couwenberg at 2021-03-22T05:40:39+01:00
Set distribution to experimental.
- - - - -
15 changed files:
- .github/workflows/main.yml
- VERSION.txt
- debian/changelog
- debian/control
- docs/configuration.rst
- pywps/app/Process.py
- pywps/app/Service.py
- pywps/configuration.py
- pywps/dependencies.py
- pywps/exceptions.py
- pywps/inout/basic.py
- pywps/inout/outputs.py
- pywps/inout/storage/file.py
- requirements.txt
- tests/test_inout.py
Changes:
=====================================
.github/workflows/main.yml
=====================================
@@ -4,7 +4,7 @@ on: [ push, pull_request ]
jobs:
main:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
=====================================
VERSION.txt
=====================================
@@ -1 +1 @@
-4.4.0
+4.4.1
=====================================
debian/changelog
=====================================
@@ -1,8 +1,10 @@
-pywps (4.4.0-1~exp2) UNRELEASED; urgency=medium
+pywps (4.4.1-1~exp1) experimental; urgency=medium
+ * New upstream release.
* Update watch file for GitHub URL changes.
+ * Add python3-humanize to (build) dependencies.
- -- Bas Couwenberg <sebastic at debian.org> Sat, 20 Mar 2021 09:13:33 +0100
+ -- Bas Couwenberg <sebastic at debian.org> Mon, 22 Mar 2021 05:40:28 +0100
pywps (4.4.0-1~exp1) experimental; urgency=medium
=====================================
debian/control
=====================================
@@ -12,6 +12,7 @@ Build-Depends: debhelper (>= 10~),
python3-all,
python3-dateutil,
python3-gdal,
+ python3-humanize,
python3-jinja2,
python3-jsonschema,
python3-lxml,
@@ -31,6 +32,7 @@ Package: python3-pywps
Architecture: all
Depends: python3-dateutil,
python3-gdal,
+ python3-humanize,
python3-jinja2,
python3-jsonschema,
python3-lxml,
=====================================
docs/configuration.rst
=====================================
@@ -113,7 +113,10 @@ configuration file <https://docs.pycsw.org/en/latest/configuration.html>`_.
number of processor cores. -1 for no limit.
:maxrequestsize:
- maximal request size. 0 for no limit
+ maximal request size. 0 for no limit.
+
+:maxsingleinputsize:
+ maximal request size for a single input. 0 for no limit.
:maxprocesses:
maximal number of requests being stored in queue, waiting till they can be
@@ -153,6 +156,15 @@ configuration file <https://docs.pycsw.org/en/latest/configuration.html>`_.
:storagetype:
The type of storage to use when storing status and results. Possible values are: ``file``, ``s3``. Defaults to ``file``.
+:storage_copy_function:
+ When using file storage you can choose the copy function. Possible values are:
+
+ * ``copy``: using ``shutil.copy2``,
+ * ``move``: using ``shutil.move``,
+ * ``link``: using ``os.link`` (hardlink).
+
+ Default: ``copy``.
+
[processing]
------------
=====================================
pywps/app/Process.py
=====================================
@@ -20,9 +20,11 @@ from pywps.inout.inputs import input_from_json
from pywps.inout.outputs import output_from_json
import pywps.configuration as config
from pywps.exceptions import (StorageNotSupported, OperationNotSupported,
- ServerBusy, NoApplicableCode)
+ ServerBusy, NoApplicableCode,
+ InvalidParameterValue)
from pywps.app.exceptions import ProcessError
from pywps.inout.storage.builder import StorageBuilder
+from pywps.inout.outputs import ComplexOutput
import importlib
@@ -311,6 +313,7 @@ class Process(object):
process._set_uuid(uuid)
process._setup_status_storage()
process.async_ = True
+ process.setup_outputs_from_wps_request(new_wps_request)
new_wps_response = ExecuteResponse(new_wps_request, process=process, uuid=uuid)
new_wps_response.store_status_file = True
process._run_async(new_wps_request, new_wps_response)
@@ -446,3 +449,31 @@ class Process(object):
LOGGER.debug('GISRC {}, GISBASE {}, GISDBASE {}, LOCATION {}, MAPSET {}'.format(
os.environ.get('GISRC'), os.environ.get('GISBASE'),
dbase, location, os.path.basename(mapset_name)))
+
+ def setup_outputs_from_wps_request(self, wps_request):
+ # set as_reference to True for all the outputs specified as reference
+ # if the output is not required to be raw
+ if not wps_request.raw:
+ for wps_outpt in wps_request.outputs:
+
+ is_reference = wps_request.outputs[wps_outpt].get('asReference', 'false')
+ mimetype = wps_request.outputs[wps_outpt].get('mimetype', '')
+ if is_reference.lower() == 'true':
+ # check if store is supported
+ if self.store_supported == 'false':
+ raise StorageNotSupported(
+ 'The storage of data is not supported for this process.')
+
+ is_reference = True
+ else:
+ is_reference = False
+
+ for outpt in self.outputs:
+ if outpt.identifier == wps_outpt:
+ outpt.as_reference = is_reference
+ if isinstance(outpt, ComplexOutput) and mimetype != '':
+ data_format = [f for f in outpt.supported_formats if f.mime_type == mimetype]
+ if len(data_format) == 0:
+ raise InvalidParameterValue(
+ 'MimeType ' + mimetype + ' not valid')
+ outpt.data_format = data_format[0]
=====================================
pywps/app/Service.py
=====================================
@@ -13,7 +13,6 @@ import pywps.configuration as config
from pywps.exceptions import MissingParameterValue, NoApplicableCode, InvalidParameterValue, FileSizeExceeded, \
StorageNotSupported, FileURLNotSupported
from pywps.inout.inputs import ComplexInput, LiteralInput, BoundingBoxInput
-from pywps.inout.outputs import ComplexOutput
from pywps.dblog import log_request, store_status
from pywps import response
from pywps.response.status import WPS_STATUS
@@ -138,32 +137,7 @@ class Service(object):
wps_request.inputs = data_inputs
- # set as_reference to True for all the outputs specified as reference
- # if the output is not required to be raw
- if not wps_request.raw:
- for wps_outpt in wps_request.outputs:
-
- is_reference = wps_request.outputs[wps_outpt].get('asReference', 'false')
- mimetype = wps_request.outputs[wps_outpt].get('mimetype', '')
- if is_reference.lower() == 'true':
- # check if store is supported
- if process.store_supported == 'false':
- raise StorageNotSupported(
- 'The storage of data is not supported for this process.')
-
- is_reference = True
- else:
- is_reference = False
-
- for outpt in process.outputs:
- if outpt.identifier == wps_outpt:
- outpt.as_reference = is_reference
- if isinstance(outpt, ComplexOutput) and mimetype != '':
- data_format = [f for f in outpt.supported_formats if f.mime_type == mimetype]
- if len(data_format) == 0:
- raise InvalidParameterValue(
- 'MimeType ' + mimetype + ' not valid')
- outpt.data_format = data_format[0]
+ process.setup_outputs_from_wps_request(wps_request)
wps_response = process.execute(wps_request, uuid)
return wps_response
=====================================
pywps/configuration.py
=====================================
@@ -89,6 +89,10 @@ def load_configuration(cfgfiles=None):
# after process has finished.
CONFIG.set('server', 'cleantempdir', 'true')
CONFIG.set('server', 'storagetype', 'file')
+ # File storage outputs can be copied, moved or linked
+ # from the workdir to the output folder.
+ # Allowed functions: "copy", "move", "link" (default "copy")
+ CONFIG.set('server', 'storage_copy_function', 'copy')
CONFIG.add_section('processing')
CONFIG.set('processing', 'mode', 'default')
=====================================
pywps/dependencies.py
=====================================
@@ -10,6 +10,7 @@ try:
from osgeo import gdal, ogr
except ImportError:
warnings.warn('Complex validation requires GDAL/OGR support.')
+ ogr = None
try:
import netCDF4
=====================================
pywps/exceptions.py
=====================================
@@ -115,7 +115,13 @@ class StorageNotSupported(NoApplicableCode):
class NotEnoughStorage(NoApplicableCode):
- """Storage not supported exception implementation
+ """Not enough storage exception implementation
+ """
+ code = 400
+
+
+class FileStorageError(NoApplicableCode):
+ """File storage exception implementation
"""
code = 400
=====================================
pywps/inout/basic.py
=====================================
@@ -30,6 +30,7 @@ import base64
from collections import namedtuple
from copy import deepcopy
from io import BytesIO
+import humanize
_SOURCE_TYPE = namedtuple('SOURCE_TYPE', 'MEMORY, FILE, STREAM, DATA, URL')
@@ -336,6 +337,11 @@ class FileHandler(IOHandler):
import pathlib
return pathlib.PurePosixPath(self.file).as_uri()
+ @property
+ def size(self):
+ """Length of the linked content in octets."""
+ return os.stat(self.file).st_size
+
def _openmode(self, data=None):
openmode = 'r'
# in Python 3 we need to open binary files in binary mode.
@@ -441,12 +447,15 @@ class UrlHandler(FileHandler):
@property
def file(self):
+ """Downloads URL and return file pointer.
+ Checks if size is allowed before download.
+ """
if self._file is not None:
return self._file
self._file = self._build_file_name(href=self.url)
- max_byte_size = self.max_input_size()
+ max_byte_size = self.max_size()
# Create request
try:
@@ -455,21 +464,24 @@ class UrlHandler(FileHandler):
except Exception as e:
raise NoApplicableCode('File reference error: {}'.format(e))
- error_message = 'File size for input "{}" exceeded. Maximum allowed: {} megabytes'.format(
- self.inpt.get('identifier', '?'), max_byte_size)
+ error_message = 'File size for input "{}" exceeded. Maximum allowed: {}'.format(
+ self.inpt.get('identifier', '?'), humanize.naturalsize(max_byte_size))
- if int(data_size) > int(max_byte_size):
- raise FileSizeExceeded(error_message)
+ if int(max_byte_size) > 0:
+ if int(data_size) > int(max_byte_size):
+ raise FileSizeExceeded(error_message)
try:
with open(self._file, 'wb') as f:
data_size = 0
for chunk in reference_file.iter_content(chunk_size=1024):
data_size += len(chunk)
- if int(data_size) > int(max_byte_size):
- raise FileSizeExceeded(error_message)
+ if int(max_byte_size) > 0:
+ if int(data_size) > int(max_byte_size):
+ raise FileSizeExceeded(error_message)
f.write(chunk)
-
+ except FileSizeExceeded:
+ raise
except Exception as e:
raise NoApplicableCode(e)
@@ -483,6 +495,16 @@ class UrlHandler(FileHandler):
def post_data(self, value):
self._post_data = value
+ @property
+ def size(self):
+ """Get content-length of URL without download"""
+ req = self._openurl(self.url)
+ if req.ok:
+ size = int(req.headers.get('content-length', '0'))
+ else:
+ size = 0
+ return size
+
@staticmethod
def _openurl(href, data=None):
"""Open given href.
@@ -496,14 +518,15 @@ class UrlHandler(FileHandler):
return req
@staticmethod
- def max_input_size():
+ def max_size():
"""Calculates maximal size for input file based on configuration
and units.
:return: maximum file size in bytes
"""
ms = config.get_config_value('server', 'maxsingleinputsize')
- return config.get_size_mb(ms) * 1024**2
+ byte_size = config.get_size_mb(ms) * 1024**2
+ return byte_size
class SimpleHandler(DataHandler):
=====================================
pywps/inout/outputs.py
=====================================
@@ -320,6 +320,7 @@ class MetaFile:
The metalink document is created by a `MetaLink` instance, which
holds a number of `MetaFile` instances.
"""
+ self._size = None
self._output = ComplexOutput(
identifier=identity or '',
title=description or '',
@@ -360,8 +361,15 @@ class MetaFile:
@property
def size(self):
- """Length of the linked content in octets."""
- return os.stat(self.file).st_size
+ """Size of the linked content in bytes."""
+ if self._size is None:
+ self._size = self._output.size
+ return self._size
+
+ @size.setter
+ def size(self, value):
+ """Set size to avoid size calculation."""
+ self._size = int(value)
@property
def urls(self):
=====================================
pywps/inout/storage/file.py
=====================================
@@ -6,7 +6,7 @@
import logging
import os
from urllib.parse import urljoin
-from pywps.exceptions import NotEnoughStorage
+from pywps.exceptions import NotEnoughStorage, FileStorageError
from pywps import configuration as config
from pywps.inout.basic import IOHandler
@@ -22,7 +22,8 @@ class FileStorageBuilder(StorageImplementationBuilder):
def build(self):
file_path = config.get_config_value('server', 'outputpath')
base_url = config.get_config_value('server', 'outputurl')
- return FileStorage(file_path, base_url)
+ copy_function = config.get_config_value('server', 'storage_copy_function')
+ return FileStorage(file_path, base_url, copy_function=copy_function)
def _build_output_name(output):
@@ -59,17 +60,17 @@ class FileStorage(CachedStorage):
True
"""
- def __init__(self, output_path, output_url):
+ def __init__(self, output_path, output_url, copy_function=None):
"""
"""
CachedStorage.__init__(self)
self.target = output_path
self.output_url = output_url
+ self.copy_function = copy_function
def _do_store(self, output):
import platform
import math
- import shutil
import tempfile
import uuid
@@ -103,8 +104,12 @@ class FileStorage(CachedStorage):
dir=target)[1]
full_output_name = os.path.join(target, output_name)
- LOGGER.info('Storing file output to {}'.format(full_output_name))
- shutil.copy2(output.file, full_output_name)
+ LOGGER.info(f'Storing file output to {full_output_name} ({self.copy_function}).')
+ try:
+ self.copy(output.file, full_output_name, self.copy_function)
+ except Exception:
+ LOGGER.exception(f"Could not copy {output_name}.")
+ raise FileStorageError("Could not copy output file.")
just_file_name = os.path.basename(output_name)
@@ -113,6 +118,27 @@ class FileStorage(CachedStorage):
return (STORE_TYPE.PATH, output_name, url)
+ @staticmethod
+ def copy(src, dst, copy_function=None):
+ """Copy file from source to destination using `copy_function`.
+
+ Values of `copy_function` (default=`copy`):
+ * copy: using `shutil.copy2`
+ * move: using `shutil.move`
+ * link: using `os.link` (hardlink)
+ """
+ import shutil
+ if copy_function == 'move':
+ shutil.move(src, dst)
+ elif copy_function == 'link':
+ try:
+ os.link(src, dst)
+ except Exception:
+ LOGGER.warn("Could not create hardlink. Fallback to copy.")
+ FileStorage.copy(src, dst)
+ else:
+ shutil.copy2(src, dst)
+
def write(self, data, destination, data_format=None):
"""
Write data to self.target
=====================================
requirements.txt
=====================================
@@ -6,3 +6,4 @@ python-dateutil
requests
SQLAlchemy
werkzeug
+humanize
=====================================
tests/test_inout.py
=====================================
@@ -772,6 +772,12 @@ class TestMetaLink(unittest.TestCase):
mf._set_workdir(self.tmp_dir)
return mf
+ def metafile_with_url(self):
+ mf = MetaFile('identifier', 'title', fmt=FORMATS.JSON)
+ mf.url = "https://pywps.org/"
+ mf._set_workdir(self.tmp_dir)
+ return mf
+
def test_metafile(self):
mf = self.metafile()
self.assertEqual('identifier', mf.identity)
@@ -821,6 +827,25 @@ class TestMetaLink(unittest.TestCase):
ml4.checksums = True
assert 'hash' in ml4.xml
+ def test_size(self):
+ ml4 = self.metalink4()
+ ml4.append(self.metafile_with_url())
+ assert 'size' in ml4.xml
+
+ def test_no_size(self):
+ ml4 = self.metalink4()
+ mf = self.metafile_with_url()
+ mf.size = 0
+ ml4.files = [mf]
+ assert 'size' not in ml4.xml
+
+ def test_set_size(self):
+ ml4 = self.metalink4()
+ mf = self.metafile_with_url()
+ mf.size = 100
+ ml4.files = [mf]
+ assert '<size>100</size>' in ml4.xml
+
def load_tests(loader=None, tests=None, pattern=None):
if not loader:
View it on GitLab: https://salsa.debian.org/debian-gis-team/pywps/-/compare/cc8d9d9ac18014e301bcdc2d295b8567ed21665c...ae4fc8bc19bd51c5d12fca49aa4902c553e12640
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pywps/-/compare/cc8d9d9ac18014e301bcdc2d295b8567ed21665c...ae4fc8bc19bd51c5d12fca49aa4902c553e12640
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/20210322/faebe2bb/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list