Source code for ows.xml

# -------------------------------------------------------------------------------
#
# Project: pyows <http://eoxserver.org>
# Authors: Fabian Schindler <fabian.schindler@eox.at>
#
# -------------------------------------------------------------------------------
# Copyright (C) 2019 EOX IT Services GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies of this Software or works derived from this Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# -------------------------------------------------------------------------------

""" This module contains facilities to help decoding XML structures.
"""

from typing import List, Dict, Optional

from lxml import etree
from lxml.builder import ElementMaker as _ElementMaker

from .decoder import BaseParameter, BaseDecoder, NO_DEFAULT


# type alias
Element = etree._Element
ElementTree = etree._ElementTree
Comment = etree.Comment


[docs]class ElementMaker(_ElementMaker): ''' Subclass of the original ElementMaker that automatically filters out None values in sub-elements and attributes. ''' def __call__(self, tag: str, *args: List[Optional[Element]], **kwargs: Dict[str, Optional[str]]) -> Element: return super().__call__(tag, *[ arg for arg in args if arg is not None ], **{ key: value for key, value in kwargs.items() if value is not None })
[docs]class NameSpace(object): ''' Helper object to ease the dealing with namespaces in both encoding and decoding. :param uri: the namespace URI :param prefix: the namespace prefix :param schema_location: the schema location of this namespace ''' def __init__(self, uri: str, prefix=None, schema_location=None): self._uri = uri self._lxml_uri = "{%s}" % uri self._prefix = prefix self._schema_location = schema_location @property def uri(self): return self._uri @property def prefix(self): return self._prefix @property def schema_location(self): return self._schema_location def __eq__(self, other): if isinstance(other, NameSpace): return self.uri == other.uri elif isinstance(other, str): return self.uri == other raise TypeError def __call__(self, tag): return self._lxml_uri + tag
[docs]class NameSpaceMap(dict): """ Helper object to ease the setup and management of namespace collections in both encoding and decoding. Can (and should) be passed as ``namespaces`` attribute in :class:`ows.xml.Decoder` subclasses. :param namespaces: a list of :class:`NameSpace` objects. """ def __init__(self, *namespaces): self._schema_location_dict = {} for namespace in namespaces: self.add(namespace) self._namespaces = namespaces
[docs] def add(self, namespace): self[namespace.prefix] = namespace.uri if namespace.schema_location: self._schema_location_dict[namespace.uri] = ( namespace.schema_location )
def __copy__(self): return type(self)(*self._namespaces) @property def schema_locations(self): return self._schema_location_dict
ns_xsi = NameSpace("http://www.w3.org/2001/XMLSchema-instance", "xsi")
[docs]class Parameter(BaseParameter): """ Parameter for XML values. :param selector: the node selector; if a string is passed it is interpreted as an XPath expression, a callable will be called with the root of the element tree and shall yield any number of node :param type: the type to parse the raw value; by default the raw string is returned :param num: defines how many times the key can be present; use any numeric value to set it to a fixed count, "*" for any number, "?" for zero or one time or "+" for one or more times :param default: the default value :param namespaces: any namespace necessary for the XPath expression; defaults to the :class:`Decoder` namespaces. :param locator: override the locator in case of exceptions """ def __init__(self, selector, type=None, num=1, default=NO_DEFAULT, default_factory=None, namespaces=None, locator=None): super(Parameter, self).__init__(type, num, default, default_factory) self.selector = selector self.namespaces = namespaces self._locator = locator
[docs] def select(self, decoder): # prepare the XPath selector if necessary if isinstance(self.selector, str): namespaces = self.namespaces or decoder.namespaces self.selector = etree.XPath(self.selector, namespaces=namespaces) results = self.selector(decoder._tree) if isinstance(results, (str, float, int)): results = [results] return results
@property def locator(self): return self._locator or str(self.selector)
[docs]class Decoder(BaseDecoder): """ Base class for XML Decoders. :param params: an instance of either :class:`lxml.etree.ElementTree`, or :class:`basestring` (which will be parsed using :func:`lxml.etree.fromstring`) Decoders should be used as such: :: from ows import xml, typelist class ExampleDecoder(xml.Decoder): namespaces = {"myns": "http://myns.org"} single = xml.Parameter("myns:single/text()", num=1) items = xml.Parameter("myns:collection/myns:item/text()", num="+") attr_a = xml.Parameter("myns:object/@attrA", num="?") attr_b = xml.Parameter("myns:object/@attrB", num="?", default="x") decoder = ExampleDecoder(''' <myns:root xmlns:myns="http://myns.org"> <myns:single>value</myns:single> <myns:collection> <myns:item>a</myns:item> <myns:item>b</myns:item> <myns:item>c</myns:item> </myns:collection> <myns:object attrA="value"/> </myns:root> ''') print(decoder.single) print(decoder.items) print(decoder.attr_a) print(decoder.attr_b) """ # must be overriden if the XPath expressions use # namespaces namespaces = {} def __init__(self, tree): if isinstance(tree, (str, bytes)): try: tree = etree.fromstring(tree) except etree.XMLSyntaxError as exc: raise ValueError( "Malformed XML document. Error was %s" % exc ) from exc elif isinstance(tree, etree._Element): pass else: raise ValueError(f'Unsupported type {type(tree)}') self._tree = tree