Source code for pywps.inout.outputs

##################################################################
# Copyright 2018 Open Source Geospatial Foundation and others    #
# licensed under MIT, Please consult LICENSE.txt for details     #
##################################################################
"""
WPS Output classes
"""

import lxml.etree as etree
import os
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.validator.mode import MODE
from pywps import configuration as config
from pywps.inout.formats import Format


[docs]class BoundingBoxOutput(basic.BBoxOutput): """ :param identifier: The name of this input. :param str title: Title of the input :param str abstract: Input abstract :param crss: List of supported coordinate reference system (e.g. ['EPSG:4326']) :param int dimensions: number of dimensions (2 or 3) :param int min_occurs: minimum occurence :param int max_occurs: maximum occurence :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, translations=None): basic.BBoxOutput.__init__(self, identifier, title=title, abstract=abstract, keywords=keywords, crss=crss, dimensions=dimensions, mode=mode, translations=translations) self.metadata = metadata self.min_occurs = min_occurs self.max_occurs = max_occurs self.as_reference = as_reference @property def json(self): """Get JSON representation of the output """ return { 'identifier': self.identifier, 'title': self.title, 'abstract': self.abstract, 'keywords': self.keywords, 'min_occurs': self.min_occurs, 'max_occurs': self.max_occurs, 'metadata': self.metadata, 'type': 'bbox', 'crs': self.crs, 'crss': self.crss, 'dimensions': self.dimensions, 'bbox': self.data, 'll': self.ll, 'ur': self.ur, 'workdir': self.workdir, 'mode': self.valid_mode, 'translations': self.translations, } @classmethod def from_json(cls, json_output): instance = cls( identifier=json_output['identifier'], title=json_output['title'], abstract=json_output['abstract'], keywords=json_output['keywords'], min_occurs=json_output['min_occurs'], max_occurs=json_output['max_occurs'], metadata=[Metadata.from_json(data) for data in json_output.get('metadata', [])], 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'] return instance
[docs]class ComplexOutput(basic.ComplexOutput): """ :param identifier: The name of this output. :param title: Readable form of the output name. :param supported_formats: List of supported formats. The first format in the list will be used as the default. :type supported_formats: (pywps.inout.formats.Format, ) :param str abstract: Description of the output :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, translations=None): if metadata is None: metadata = [] basic.ComplexOutput.__init__(self, identifier, title=title, data_format=data_format, abstract=abstract, keywords=keywords, workdir=workdir, supported_formats=supported_formats, mode=mode, translations=translations) self.metadata = metadata self.as_reference = as_reference self.storage = None @property def json(self): """Get JSON representation of the output """ data = { "identifier": self.identifier, "title": self.title, "abstract": self.abstract, 'keywords': self.keywords, 'type': 'complex', 'supported_formats': [frmt.json for frmt in self.supported_formats], 'asreference': self.as_reference, 'data_format': self.data_format.json if self.data_format else None, 'file': self.file if self.prop == 'file' else None, 'workdir': self.workdir, 'mode': self.valid_mode, 'min_occurs': self.min_occurs, 'max_occurs': self.max_occurs, 'translations': self.translations, } if self.as_reference: data = self._json_reference(data) else: data = self._json_data(data) if self.data_format: if self.data_format.mime_type: data['mimetype'] = self.data_format.mime_type if self.data_format.encoding: data['encoding'] = self.data_format.encoding if self.data_format.schema: data['schema'] = self.data_format.schema return data @classmethod def from_json(cls, json_output): instance = cls( identifier=json_output['identifier'], title=json_output.get('title'), abstract=json_output.get('abstract'), keywords=json_output.get('keywords', []), workdir=json_output.get('workdir'), metadata=[Metadata.from_json(data) for data in json_output.get('metadata', [])], data_format=Format( schema=json_output['data_format'].get('schema'), extension=json_output['data_format'].get('extension'), mime_type=json_output['data_format']['mime_type'], encoding=json_output['data_format'].get('encoding') ), supported_formats=[ Format( schema=infrmt.get('schema'), extension=infrmt.get('extension'), mime_type=infrmt['mime_type'], encoding=infrmt.get('encoding') ) for infrmt in json_output['supported_formats'] ], 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'): instance.file = json_output['file'] return instance def _json_reference(self, data): """Return Reference node """ data["type"] = "reference" # get_url will create the file and return the url for it if self.prop == 'url': data["href"] = self.url elif self.prop is not None: self.storage = FileStorageBuilder().build() data["href"] = self.get_url() return data def _json_data(self, data): """Return Data node """ data["type"] = "complex" if self.data: if 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') else: if self.data_format.encoding == 'base64': data["data"] = self.base64.decode('utf-8') else: # Otherwise we assume all other formats are unsafe and need to be enclosed in a CDATA tag. if isinstance(self.data, bytes): out = self.data.encode(self.data_format.encoding or 'utf-8') else: out = self.data data["data"] = u'<![CDATA[{}]]>'.format(out) return data
[docs]class LiteralOutput(basic.LiteralOutput): """ :param identifier: The name of this output. :param str title: Title of the input :param pywps.inout.literaltypes.LITERAL_DATA_TYPES data_type: data type :param str abstract: Input abstract :param str uoms: units :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, 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, translations=translations) self.metadata = metadata @property def json(self): """Get JSON representation of the output """ data = { "identifier": self.identifier, "title": self.title, "abstract": self.abstract, "keywords": self.keywords, "data": self.data, "data_type": self.data_type, "type": "literal", "uoms": [u.json for u in self.uoms], "translations": self.translations, } if self.uom: data["uom"] = self.uom.json return data @classmethod def from_json(cls, json_output): uoms = [basic.UOM(uom.get('uom')) for uom in json_output.get('uoms', [])] uom = json_output.get('uom') instance = cls( identifier=json_output['identifier'], title=json_output['title'], data_type=json_output['data_type'], abstract=json_output['abstract'], keywords=json_output['keywords'], uoms=uoms, translations=json_output.get('translations'), ) instance.data = json_output.get('data') if uom: instance.uom = basic.UOM(uom['uom']) return instance
class MetaFile: """MetaFile object.""" def __init__(self, identity=None, description=None, fmt=None): """Create a `MetaFile` object. :param str identity: human readable identity. :param str description: human readable file description. :param pywps.FORMAT fmt: file mime type. The content of each metafile is set like `ComplexOutputs`, ie using either the `data`, `file`, `stream` or `url` properties. The metalink document is created by a `MetaLink` instance, which holds a number of `MetaFile` instances. """ self._output = ComplexOutput( identifier=identity or '', title=description or '', as_reference=True, supported_formats=[fmt, ], ) def _set_workdir(self, workdir): self._output.workdir = workdir @property def hash(self): """Text construct that conveys a cryptographic hash for a file. All hashes are encoded in lowercase hexadecimal format. Hashes are used to verify the integrity of a complete file or portion of a file to determine if the file has been transferred without any errors. """ import hashlib m = hashlib.sha256() with open(self.file, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): m.update(chunk) return m.hexdigest() @property def identity(self): """Human-readable identity.""" return self._output.identifier @property def name(self): """Indicate a specific file in a document describing multiple files.""" js = self._output.json (_, name) = os.path.split(js.get('href', 'http:///')) return name @property def size(self): """Length of the linked content in octets.""" return os.stat(self.file).st_size @property def urls(self): js = self._output.json return [js.get('href', ''), ] @property def mediatype(self): """Multipurpose Internet Mail Extensions (MIME) media type [RFC4288] of the metadata file available at the IRI.""" return self._output.data_format.mime_type @property def data(self): return self._output.data @data.setter def data(self, value): self._output.data = value @property def file(self): return self._output.file @file.setter def file(self, value): self._output.file = value @property def url(self): return self._output.url @url.setter def url(self, value): self._output.url = value @property def stream(self): return self._output.stream @stream.setter def stream(self, value): self._output.stream = value def __str__(self): out = "MetaFile {}:".format(self.name) for url in self.urls: out += "\n\t{}".format(url) return out def __repr__(self): return "<pywps.inout.outputs.MetaFile {}>".format(self.name) class MetaLink: _xml_template = 'metalink/3.0/main.xml' # Specs: https://www.metalinker.org/Metalink_3.0_Spec.pdf def __init__(self, identity=None, description=None, publisher=None, files=(), workdir=None, checksums=False): """Create a MetaLink v3.0 instance. :param str identity: human readable identity. :param str description: human readable file description. :param str publisher: The name of the file's publisher. :param tuple files: Sequence of files to include in Metalink. Can also be added using `append`. :param str workdir: Work directory to store temporary files. :param bool checksums: Whether to compute checksums on files. To use, first append `MetaFile` instances, then write the metalink using the `xml` property. Methods: - `append`: add a `MetaFile` instance """ self.identity = identity self.description = description self.workdir = workdir self.publisher = publisher self.files = [] self.checksums = checksums for file in files: self.append(file) self._load_template() def append(self, file): """Append a `MetaFile` instance.""" if not isinstance(file, MetaFile): raise ValueError("file must be a MetaFile instance.") file._set_workdir(self.workdir) self.files.append(file) @property def xml(self): return self._template.render(meta=self) @property def origin(self): """IRI where the Metalink Document was originally published. If the dynamic attribute of metalink:origin is "true", then updated versions of the Metalink can be found at this IRI. """ return "" @property def published(self): """Date construct indicating an instant in time associated with an event early in the life cycle of the entry.""" import datetime return datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") @property def generator(self): """Generating agent name and version used to generate a Metalink Document, for debugging and other purposes.""" import pywps return "PyWPS/{}".format(pywps.__version__) @property def url(self): """Return the server URL.""" return config.get_config_value('server', 'url') def _load_template(self): from pywps.response import RelEnvironment from jinja2 import PackageLoader template_env = RelEnvironment( loader=PackageLoader('pywps', 'templates'), trim_blocks=True, lstrip_blocks=True, autoescape=True, ) self._template = template_env.get_template(self._xml_template) class MetaLink4(MetaLink): _xml_template = 'metalink/4.0/main.xml' # Specs: https://tools.ietf.org/html/rfc5854 def output_from_json(json_data): if json_data['type'] == 'complex': output = ComplexOutput.from_json(json_data) elif json_data['type'] == 'literal': output = LiteralOutput.from_json(json_data) elif json_data['type'] == 'bbox': output = BoundingBoxOutput.from_json(json_data) else: raise InvalidParameterValue("Output type not recognized: {}".format(json_data['type'])) return output