601 lines
20 KiB
Python
601 lines
20 KiB
Python
import copy
|
|
import logging
|
|
from collections import OrderedDict, deque
|
|
from itertools import chain
|
|
|
|
import six
|
|
from cached_property import threaded_cached_property
|
|
|
|
from zeep.exceptions import XMLParseError, UnexpectedElementError
|
|
from zeep.xsd.const import xsi_ns
|
|
from zeep.xsd.elements import Any, AnyAttribute, AttributeGroup, Element
|
|
from zeep.xsd.indicators import Group, OrderIndicator, Sequence
|
|
from zeep.xsd.utils import NamePrefixGenerator
|
|
from zeep.utils import get_base_class
|
|
from zeep.xsd.valueobjects import CompoundValue
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Type(object):
|
|
|
|
def __init__(self, qname=None, is_global=False):
|
|
self.qname = qname
|
|
self.name = qname.localname if qname else None
|
|
self._resolved = False
|
|
self.is_global = is_global
|
|
|
|
def accept(self, value):
|
|
raise NotImplementedError
|
|
|
|
def parse_kwargs(self, kwargs, name, available_kwargs):
|
|
value = None
|
|
name = name or self.name
|
|
|
|
if name in available_kwargs:
|
|
value = kwargs[name]
|
|
available_kwargs.remove(name)
|
|
return {name: value}
|
|
return {}
|
|
|
|
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
|
|
context=None):
|
|
raise NotImplementedError(
|
|
'%s.parse_xmlelement() is not implemented' % self.__class__.__name__)
|
|
|
|
def parsexml(self, xml, schema=None):
|
|
raise NotImplementedError
|
|
|
|
def render(self, parent, value):
|
|
raise NotImplementedError(
|
|
'%s.render() is not implemented' % self.__class__.__name__)
|
|
|
|
def resolve(self):
|
|
raise NotImplementedError(
|
|
'%s.resolve() is not implemented' % self.__class__.__name__)
|
|
|
|
def extend(self, child):
|
|
raise NotImplementedError(
|
|
'%s.extend() is not implemented' % self.__class__.__name__)
|
|
|
|
def restrict(self, child):
|
|
raise NotImplementedError(
|
|
'%s.restrict() is not implemented' % self.__class__.__name__)
|
|
|
|
@property
|
|
def attributes(self):
|
|
return []
|
|
|
|
@classmethod
|
|
def signature(cls, depth=()):
|
|
return ''
|
|
|
|
|
|
class UnresolvedType(Type):
|
|
def __init__(self, qname, schema):
|
|
self.qname = qname
|
|
assert self.qname.text != 'None'
|
|
self.schema = schema
|
|
|
|
def __repr__(self):
|
|
return '<%s(qname=%r)>' % (self.__class__.__name__, self.qname)
|
|
|
|
def render(self, parent, value):
|
|
raise RuntimeError(
|
|
"Unable to render unresolved type %s. This is probably a bug." % (
|
|
self.qname))
|
|
|
|
def resolve(self):
|
|
retval = self.schema.get_type(self.qname)
|
|
return retval.resolve()
|
|
|
|
|
|
class UnresolvedCustomType(Type):
|
|
|
|
def __init__(self, qname, base_type, schema):
|
|
assert qname is not None
|
|
self.qname = qname
|
|
self.name = str(qname.localname)
|
|
self.schema = schema
|
|
self.base_type = base_type
|
|
|
|
def __repr__(self):
|
|
return '<%s(qname=%r, base_type=%r)>' % (
|
|
self.__class__.__name__, self.qname.text, self.base_type)
|
|
|
|
def resolve(self):
|
|
base = self.base_type
|
|
base = base.resolve()
|
|
|
|
cls_attributes = {
|
|
'__module__': 'zeep.xsd.dynamic_types',
|
|
}
|
|
|
|
if issubclass(base.__class__, UnionType):
|
|
xsd_type = type(self.name, (base.__class__,), cls_attributes)
|
|
return xsd_type(base.item_types)
|
|
|
|
elif issubclass(base.__class__, SimpleType):
|
|
xsd_type = type(self.name, (base.__class__,), cls_attributes)
|
|
return xsd_type(self.qname)
|
|
|
|
else:
|
|
xsd_type = type(self.name, (base.base_class,), cls_attributes)
|
|
return xsd_type(self.qname)
|
|
|
|
|
|
@six.python_2_unicode_compatible
|
|
class SimpleType(Type):
|
|
accepted_types = six.string_types
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
"""Return the xmlvalue for the given value.
|
|
|
|
Expects only one argument 'value'. The args, kwargs handling is done
|
|
here manually so that we can return readable error messages instead of
|
|
only '__call__ takes x arguments'
|
|
|
|
"""
|
|
num_args = len(args) + len(kwargs)
|
|
if num_args != 1:
|
|
raise TypeError((
|
|
'%s() takes exactly 1 argument (%d given). ' +
|
|
'Simple types expect only a single value argument'
|
|
) % (self.__class__.__name__, num_args))
|
|
|
|
if kwargs and 'value' not in kwargs:
|
|
raise TypeError((
|
|
'%s() got an unexpected keyword argument %r. ' +
|
|
'Simple types expect only a single value argument'
|
|
) % (self.__class__.__name__, next(six.iterkeys(kwargs))))
|
|
|
|
value = args[0] if args else kwargs['value']
|
|
return self.xmlvalue(value)
|
|
|
|
def __eq__(self, other):
|
|
return (
|
|
other is not None and
|
|
self.__class__ == other.__class__ and
|
|
self.__dict__ == other.__dict__)
|
|
|
|
def __str__(self):
|
|
return '%s(value)' % (self.__class__.__name__)
|
|
|
|
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
|
|
context=None):
|
|
if xmlelement.text is None:
|
|
return
|
|
try:
|
|
return self.pythonvalue(xmlelement.text)
|
|
except (TypeError, ValueError):
|
|
logger.exception("Error during xml -> python translation")
|
|
return None
|
|
|
|
def pythonvalue(self, xmlvalue):
|
|
raise NotImplementedError(
|
|
'%s.pytonvalue() not implemented' % self.__class__.__name__)
|
|
|
|
def render(self, parent, value):
|
|
parent.text = self.xmlvalue(value)
|
|
|
|
def resolve(self):
|
|
return self
|
|
|
|
def signature(self, depth=()):
|
|
return self.name
|
|
|
|
def xmlvalue(self, value):
|
|
raise NotImplementedError(
|
|
'%s.xmlvalue() not implemented' % self.__class__.__name__)
|
|
|
|
|
|
class ComplexType(Type):
|
|
_xsd_name = None
|
|
|
|
def __init__(self, element=None, attributes=None,
|
|
restriction=None, extension=None, qname=None, is_global=False):
|
|
if element and type(element) == list:
|
|
element = Sequence(element)
|
|
|
|
self.name = self.__class__.__name__ if qname else None
|
|
self._element = element
|
|
self._attributes = attributes or []
|
|
self._restriction = restriction
|
|
self._extension = extension
|
|
super(ComplexType, self).__init__(qname=qname, is_global=is_global)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
return self._value_class(*args, **kwargs)
|
|
|
|
@property
|
|
def accepted_types(self):
|
|
return (self._value_class,)
|
|
|
|
@threaded_cached_property
|
|
def _value_class(self):
|
|
return type(
|
|
self.__class__.__name__, (CompoundValue,),
|
|
{'_xsd_type': self, '__module__': 'zeep.objects'})
|
|
|
|
def __str__(self):
|
|
return '%s(%s)' % (self.__class__.__name__, self.signature())
|
|
|
|
@threaded_cached_property
|
|
def attributes(self):
|
|
generator = NamePrefixGenerator(prefix='_attr_')
|
|
result = []
|
|
elm_names = {name for name, elm in self.elements if name is not None}
|
|
for attr in self._attributes_unwrapped:
|
|
if attr.name is None:
|
|
name = generator.get_name()
|
|
elif attr.name in elm_names:
|
|
name = 'attr__%s' % attr.name
|
|
else:
|
|
name = attr.name
|
|
result.append((name, attr))
|
|
return result
|
|
|
|
@threaded_cached_property
|
|
def _attributes_unwrapped(self):
|
|
attributes = []
|
|
for attr in self._attributes:
|
|
if isinstance(attr, AttributeGroup):
|
|
attributes.extend(attr.attributes)
|
|
else:
|
|
attributes.append(attr)
|
|
return attributes
|
|
|
|
@threaded_cached_property
|
|
def elements(self):
|
|
"""List of tuples containing the element name and the element"""
|
|
result = []
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, Element):
|
|
result.append((element.attr_name, element))
|
|
else:
|
|
result.extend(element.elements)
|
|
return result
|
|
|
|
@threaded_cached_property
|
|
def elements_nested(self):
|
|
"""List of tuples containing the element name and the element"""
|
|
result = []
|
|
generator = NamePrefixGenerator()
|
|
|
|
# Handle wsdl:arrayType objects
|
|
attrs = {attr.qname.text: attr for attr in self._attributes if attr.qname}
|
|
array_type = attrs.get('{http://schemas.xmlsoap.org/soap/encoding/}arrayType')
|
|
if array_type:
|
|
name = generator.get_name()
|
|
if isinstance(self._element, Group):
|
|
return [(name, Sequence([
|
|
Any(max_occurs='unbounded', restrict=array_type.array_type)
|
|
]))]
|
|
else:
|
|
return [(name, self._element)]
|
|
|
|
# _element is one of All, Choice, Group, Sequence
|
|
if self._element:
|
|
result.append((generator.get_name(), self._element))
|
|
return result
|
|
|
|
def parse_xmlelement(self, xmlelement, schema, allow_none=True,
|
|
context=None):
|
|
"""Consume matching xmlelements and call parse() on each"""
|
|
# If this is an empty complexType (<xsd:complexType name="x"/>)
|
|
if not self.attributes and not self.elements:
|
|
return None
|
|
|
|
attributes = xmlelement.attrib
|
|
init_kwargs = OrderedDict()
|
|
|
|
# If this complexType extends a simpleType then we have no nested
|
|
# elements. Parse it directly via the type object. This is the case
|
|
# for xsd:simpleContent
|
|
if isinstance(self._element, Element) and isinstance(self._element.type, SimpleType):
|
|
name, element = self.elements_nested[0]
|
|
init_kwargs[name] = element.type.parse_xmlelement(
|
|
xmlelement, schema, name, context=context)
|
|
else:
|
|
elements = deque(xmlelement.iterchildren())
|
|
if allow_none and len(elements) == 0 and len(attributes) == 0:
|
|
return
|
|
|
|
# Parse elements. These are always indicator elements (all, choice,
|
|
# group, sequence)
|
|
for name, element in self.elements_nested:
|
|
try:
|
|
result = element.parse_xmlelements(
|
|
elements, schema, name, context=context)
|
|
if result:
|
|
init_kwargs.update(result)
|
|
except UnexpectedElementError as exc:
|
|
raise XMLParseError(exc.message)
|
|
|
|
# Check if all children are consumed (parsed)
|
|
if elements:
|
|
raise XMLParseError("Unexpected element %r" % elements[0].tag)
|
|
|
|
# Parse attributes
|
|
if attributes:
|
|
attributes = copy.copy(attributes)
|
|
for name, attribute in self.attributes:
|
|
if attribute.name:
|
|
if attribute.qname.text in attributes:
|
|
value = attributes.pop(attribute.qname.text)
|
|
init_kwargs[name] = attribute.parse(value)
|
|
else:
|
|
init_kwargs[name] = attribute.parse(attributes)
|
|
|
|
return self(**init_kwargs)
|
|
|
|
def render(self, parent, value, xsd_type=None):
|
|
"""Serialize the given value lxml.Element subelements on the parent
|
|
element.
|
|
|
|
"""
|
|
if not self.elements_nested and not self.attributes:
|
|
return
|
|
|
|
# Render attributes
|
|
for name, attribute in self.attributes:
|
|
attr_value = getattr(value, name, None)
|
|
attribute.render(parent, attr_value)
|
|
|
|
# Render sub elements
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, Element) or element.accepts_multiple:
|
|
element_value = getattr(value, name, None)
|
|
else:
|
|
element_value = value
|
|
|
|
if isinstance(element, Element):
|
|
element.type.render(parent, element_value)
|
|
else:
|
|
element.render(parent, element_value)
|
|
|
|
if xsd_type:
|
|
if xsd_type._xsd_name:
|
|
parent.set(xsi_ns('type'), xsd_type._xsd_name)
|
|
if xsd_type.qname:
|
|
parent.set(xsi_ns('type'), xsd_type.qname)
|
|
|
|
def parse_kwargs(self, kwargs, name, available_kwargs):
|
|
value = None
|
|
name = name or self.name
|
|
|
|
if name in available_kwargs:
|
|
value = kwargs[name]
|
|
available_kwargs.remove(name)
|
|
|
|
value = self._create_object(value, name)
|
|
return {name: value}
|
|
return {}
|
|
|
|
def _create_object(self, value, name):
|
|
"""Return the value as a CompoundValue object"""
|
|
if value is None:
|
|
return None
|
|
|
|
if isinstance(value, list):
|
|
return [self._create_object(val, name) for val in value]
|
|
|
|
if isinstance(value, CompoundValue):
|
|
return value
|
|
|
|
if isinstance(value, dict):
|
|
return self(**value)
|
|
|
|
# Check if the valueclass only expects one value, in that case
|
|
# we can try to automatically create an object for it.
|
|
if len(self.attributes) + len(self.elements) == 1:
|
|
return self(value)
|
|
|
|
raise ValueError((
|
|
"Error while create XML for complexType '%s': "
|
|
"Expected instance of type %s, received %r instead."
|
|
) % (self.qname or name, self._value_class, type(value)))
|
|
|
|
def resolve(self):
|
|
"""Resolve all sub elements and types"""
|
|
if self._resolved:
|
|
return self._resolved
|
|
self._resolved = self
|
|
|
|
if self._element:
|
|
self._element = self._element.resolve()
|
|
|
|
resolved = []
|
|
for attribute in self._attributes:
|
|
value = attribute.resolve()
|
|
assert value is not None
|
|
if isinstance(value, list):
|
|
resolved.extend(value)
|
|
else:
|
|
resolved.append(value)
|
|
self._attributes = resolved
|
|
|
|
if self._extension:
|
|
self._extension = self._extension.resolve()
|
|
self._resolved = self.extend(self._extension)
|
|
return self._resolved
|
|
|
|
elif self._restriction:
|
|
self._restriction = self._restriction.resolve()
|
|
self._resolved = self.restrict(self._restriction)
|
|
return self._resolved
|
|
|
|
else:
|
|
return self._resolved
|
|
|
|
def extend(self, base):
|
|
"""Create a new complextype instance which is the current type
|
|
extending the given base type.
|
|
|
|
Used for handling xsd:extension tags
|
|
|
|
"""
|
|
if isinstance(base, ComplexType):
|
|
base_attributes = base._attributes_unwrapped
|
|
base_element = base._element
|
|
else:
|
|
base_attributes = []
|
|
base_element = None
|
|
attributes = base_attributes + self._attributes_unwrapped
|
|
|
|
# Make sure we don't have duplicate (child is leading)
|
|
if base_attributes and self._attributes_unwrapped:
|
|
new_attributes = OrderedDict()
|
|
for attr in attributes:
|
|
if isinstance(attr, AnyAttribute):
|
|
new_attributes['##any'] = attr
|
|
else:
|
|
new_attributes[attr.qname.text] = attr
|
|
attributes = new_attributes.values()
|
|
|
|
# If the base and the current type both have an element defined then
|
|
# these need to be merged. The base_element might be empty (or just
|
|
# container a placeholder element).
|
|
element = []
|
|
if self._element and base_element:
|
|
element = self._element.clone(self._element.name)
|
|
if isinstance(element, OrderIndicator) and isinstance(base_element, OrderIndicator):
|
|
for item in reversed(base_element):
|
|
element.insert(0, item)
|
|
|
|
elif isinstance(self._element, Group):
|
|
raise NotImplementedError('TODO')
|
|
else:
|
|
pass # Element (ignore for now)
|
|
|
|
elif self._element or base_element:
|
|
element = self._element or base_element
|
|
else:
|
|
element = Element('_value_1', base)
|
|
|
|
new = self.__class__(
|
|
element=element,
|
|
attributes=attributes,
|
|
qname=self.qname)
|
|
return new
|
|
|
|
def restrict(self, base):
|
|
"""Create a new complextype instance which is the current type
|
|
restricted by the base type.
|
|
|
|
Used for handling xsd:restriction
|
|
|
|
"""
|
|
attributes = list(
|
|
chain(base._attributes_unwrapped, self._attributes_unwrapped))
|
|
|
|
# Make sure we don't have duplicate (self is leading)
|
|
if base._attributes_unwrapped and self._attributes_unwrapped:
|
|
new_attributes = OrderedDict()
|
|
for attr in attributes:
|
|
if isinstance(attr, AnyAttribute):
|
|
new_attributes['##any'] = attr
|
|
else:
|
|
new_attributes[attr.qname.text] = attr
|
|
attributes = new_attributes.values()
|
|
|
|
new = self.__class__(
|
|
element=self._element or base._element,
|
|
attributes=attributes,
|
|
qname=self.qname)
|
|
return new.resolve()
|
|
|
|
def signature(self, depth=()):
|
|
if len(depth) > 0 and self.is_global:
|
|
return self.name
|
|
|
|
parts = []
|
|
depth += (self.name,)
|
|
for name, element in self.elements_nested:
|
|
# http://schemas.xmlsoap.org/soap/encoding/ contains cyclic type
|
|
if isinstance(element, Element) and element.type == self:
|
|
continue
|
|
|
|
part = element.signature(depth)
|
|
parts.append(part)
|
|
|
|
for name, attribute in self.attributes:
|
|
part = '%s: %s' % (name, attribute.signature(depth))
|
|
parts.append(part)
|
|
|
|
value = ', '.join(parts)
|
|
if len(depth) > 1:
|
|
value = '{%s}' % value
|
|
return value
|
|
|
|
|
|
class ListType(SimpleType):
|
|
"""Space separated list of simpleType values"""
|
|
|
|
def __init__(self, item_type):
|
|
self.item_type = item_type
|
|
super(ListType, self).__init__()
|
|
|
|
def __call__(self, value):
|
|
return value
|
|
|
|
def render(self, parent, value):
|
|
parent.text = self.xmlvalue(value)
|
|
|
|
def resolve(self):
|
|
self.item_type = self.item_type.resolve()
|
|
self.base_class = self.item_type.__class__
|
|
return self
|
|
|
|
def xmlvalue(self, value):
|
|
item_type = self.item_type
|
|
return ' '.join(item_type.xmlvalue(v) for v in value)
|
|
|
|
def pythonvalue(self, value):
|
|
if not value:
|
|
return []
|
|
item_type = self.item_type
|
|
return [item_type.pythonvalue(v) for v in value.split()]
|
|
|
|
def signature(self, depth=()):
|
|
return self.item_type.signature(depth) + '[]'
|
|
|
|
|
|
class UnionType(SimpleType):
|
|
|
|
def __init__(self, item_types):
|
|
self.item_types = item_types
|
|
self.item_class = None
|
|
assert item_types
|
|
super(UnionType, self).__init__(None)
|
|
|
|
def resolve(self):
|
|
from zeep.xsd.builtins import _BuiltinType
|
|
|
|
self.item_types = [item.resolve() for item in self.item_types]
|
|
base_class = get_base_class(self.item_types)
|
|
if issubclass(base_class, _BuiltinType) and base_class != _BuiltinType:
|
|
self.item_class = base_class
|
|
return self
|
|
|
|
def signature(self, depth=()):
|
|
return ''
|
|
|
|
def parse_xmlelement(self, xmlelement, schema=None, allow_none=True,
|
|
context=None):
|
|
if self.item_class:
|
|
return self.item_class().parse_xmlelement(
|
|
xmlelement, schema, allow_none, context)
|
|
return xmlelement.text
|
|
|
|
def pythonvalue(self, value):
|
|
if self.item_class:
|
|
return self.item_class().pythonvalue(value)
|
|
return value
|
|
|
|
def xmlvalue(self, value):
|
|
if self.item_class:
|
|
return self.item_class().xmlvalue(value)
|
|
return value
|