742 lines
25 KiB
Python
742 lines
25 KiB
Python
"""
|
|
zeep.xsd.elements.indicators
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Indicators are a collection of elements. There are four available, these are
|
|
All, Choice, Group and Sequence.
|
|
|
|
Indicator -> OrderIndicator -> All
|
|
-> Choice
|
|
-> Sequence
|
|
-> Group
|
|
|
|
"""
|
|
import copy
|
|
import operator
|
|
from collections import OrderedDict, defaultdict, deque
|
|
|
|
from cached_property import threaded_cached_property
|
|
|
|
from zeep.exceptions import UnexpectedElementError, ValidationError
|
|
from zeep.xsd.const import NotSet, SkipValue
|
|
from zeep.xsd.elements import Any, Element
|
|
from zeep.xsd.elements.base import Base
|
|
from zeep.xsd.utils import (
|
|
NamePrefixGenerator, UniqueNameGenerator, create_prefixed_name,
|
|
max_occurs_iter)
|
|
|
|
__all__ = ['All', 'Choice', 'Group', 'Sequence']
|
|
|
|
|
|
class Indicator(Base):
|
|
"""Base class for the other indicators"""
|
|
|
|
def __repr__(self):
|
|
return '<%s(%s)>' % (
|
|
self.__class__.__name__, super(Indicator, self).__repr__())
|
|
|
|
@property
|
|
def default_value(self):
|
|
values = OrderedDict([
|
|
(name, element.default_value) for name, element in self.elements
|
|
])
|
|
|
|
if self.accepts_multiple:
|
|
return {'_value_1': values}
|
|
return values
|
|
|
|
def clone(self, name, min_occurs=1, max_occurs=1):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class OrderIndicator(Indicator, list):
|
|
"""Base class for All, Choice and Sequence classes."""
|
|
name = None
|
|
|
|
def __init__(self, elements=None, min_occurs=1, max_occurs=1):
|
|
self.min_occurs = min_occurs
|
|
self.max_occurs = max_occurs
|
|
super(OrderIndicator, self).__init__()
|
|
if elements is not None:
|
|
self.extend(elements)
|
|
|
|
def clone(self, name, min_occurs=1, max_occurs=1):
|
|
return self.__class__(
|
|
elements=list(self),
|
|
min_occurs=min_occurs,
|
|
max_occurs=max_occurs)
|
|
|
|
@threaded_cached_property
|
|
def elements(self):
|
|
"""List of tuples containing the element name and the element"""
|
|
result = []
|
|
for name, elm in self.elements_nested:
|
|
if name is None:
|
|
result.extend(elm.elements)
|
|
else:
|
|
result.append((name, elm))
|
|
return result
|
|
|
|
@threaded_cached_property
|
|
def elements_nested(self):
|
|
"""List of tuples containing the element name and the element"""
|
|
result = []
|
|
generator = NamePrefixGenerator()
|
|
generator_2 = UniqueNameGenerator()
|
|
|
|
for elm in self:
|
|
if isinstance(elm, (All, Choice, Group, Sequence)):
|
|
if elm.accepts_multiple:
|
|
result.append((generator.get_name(), elm))
|
|
else:
|
|
for sub_name, sub_elm in elm.elements:
|
|
sub_name = generator_2.create_name(sub_name)
|
|
result.append((None, elm))
|
|
elif isinstance(elm, (Any, Choice)):
|
|
result.append((generator.get_name(), elm))
|
|
else:
|
|
name = generator_2.create_name(elm.attr_name)
|
|
result.append((name, elm))
|
|
return result
|
|
|
|
def accept(self, values):
|
|
"""Return the number of values which are accepted by this choice.
|
|
|
|
If not all required elements are available then 0 is returned.
|
|
|
|
"""
|
|
if not self.accepts_multiple:
|
|
values = [values]
|
|
|
|
results = set()
|
|
for value in values:
|
|
num = 0
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, Element):
|
|
if element.name in value and value[element.name] is not None:
|
|
num += 1
|
|
else:
|
|
num += element.accept(value)
|
|
results.add(num)
|
|
return max(results)
|
|
|
|
def parse_args(self, args, index=0):
|
|
|
|
# If the sequence contains an choice element then we can't convert
|
|
# the args to kwargs since Choice elements don't work with position
|
|
# arguments
|
|
for name, elm in self.elements_nested:
|
|
if isinstance(elm, Choice):
|
|
raise TypeError("Choice elements only work with keyword arguments")
|
|
|
|
result = {}
|
|
for name, element in self.elements:
|
|
if index >= len(args):
|
|
break
|
|
result[name] = args[index]
|
|
index += 1
|
|
|
|
return result, args, index
|
|
|
|
def parse_kwargs(self, kwargs, name, available_kwargs):
|
|
"""Apply the given kwarg to the element.
|
|
|
|
The available_kwargs is modified in-place. Returns a dict with the
|
|
result.
|
|
|
|
:param kwargs: The kwargs
|
|
:type kwargs: dict
|
|
:param name: The name as which this type is registered in the parent
|
|
:type name: str
|
|
:param available_kwargs: The kwargs keys which are still available,
|
|
modified in place
|
|
:type available_kwargs: set
|
|
:rtype: dict
|
|
|
|
"""
|
|
if self.accepts_multiple:
|
|
assert name
|
|
|
|
if name:
|
|
if name not in available_kwargs:
|
|
return {}
|
|
|
|
assert self.accepts_multiple
|
|
|
|
# Make sure we have a list, lame lame
|
|
item_kwargs = kwargs.get(name)
|
|
if not isinstance(item_kwargs, list):
|
|
item_kwargs = [item_kwargs]
|
|
|
|
result = []
|
|
for item_value in max_occurs_iter(self.max_occurs, item_kwargs):
|
|
try:
|
|
item_kwargs = set(item_value.keys())
|
|
except AttributeError:
|
|
raise TypeError(
|
|
"A list of dicts is expected for unbounded Sequences")
|
|
|
|
subresult = OrderedDict()
|
|
for item_name, element in self.elements:
|
|
value = element.parse_kwargs(item_value, item_name, item_kwargs)
|
|
if value is not None:
|
|
subresult.update(value)
|
|
|
|
if item_kwargs:
|
|
raise TypeError((
|
|
"%s() got an unexpected keyword argument %r."
|
|
) % (self, list(item_kwargs)[0]))
|
|
|
|
result.append(subresult)
|
|
|
|
result = {name: result}
|
|
|
|
# All items consumed
|
|
if not any(filter(None, item_kwargs)):
|
|
available_kwargs.remove(name)
|
|
|
|
return result
|
|
|
|
else:
|
|
assert not self.accepts_multiple
|
|
result = OrderedDict()
|
|
for elm_name, element in self.elements_nested:
|
|
sub_result = element.parse_kwargs(kwargs, elm_name, available_kwargs)
|
|
if sub_result:
|
|
result.update(sub_result)
|
|
|
|
return result
|
|
|
|
def resolve(self):
|
|
for i, elm in enumerate(self):
|
|
self[i] = elm.resolve()
|
|
return self
|
|
|
|
def render(self, parent, value, render_path):
|
|
"""Create subelements in the given parent object."""
|
|
if not isinstance(value, list):
|
|
values = [value]
|
|
else:
|
|
values = value
|
|
|
|
self.validate(values, render_path)
|
|
|
|
for value in max_occurs_iter(self.max_occurs, values):
|
|
for name, element in self.elements_nested:
|
|
if name:
|
|
if name in value:
|
|
element_value = value[name]
|
|
child_path = render_path + [name]
|
|
else:
|
|
element_value = NotSet
|
|
child_path = render_path
|
|
else:
|
|
element_value = value
|
|
child_path = render_path
|
|
|
|
if element_value is SkipValue:
|
|
continue
|
|
|
|
if element_value is not None or not element.is_optional:
|
|
element.render(parent, element_value, child_path)
|
|
|
|
def validate(self, value, render_path):
|
|
for item in value:
|
|
if item is NotSet:
|
|
raise ValidationError("No value set", path=render_path)
|
|
|
|
def signature(self, schema=None, standalone=True):
|
|
parts = []
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, Indicator):
|
|
parts.append(element.signature(schema, standalone=False))
|
|
else:
|
|
value = element.signature(schema, standalone=False)
|
|
parts.append('%s: %s' % (name, value))
|
|
|
|
part = ', '.join(parts)
|
|
|
|
if self.accepts_multiple:
|
|
return '[%s]' % (part,)
|
|
return part
|
|
|
|
|
|
class All(OrderIndicator):
|
|
"""Allows the elements in the group to appear (or not appear) in any order
|
|
in the containing element.
|
|
|
|
"""
|
|
|
|
def __init__(self, elements=None, min_occurs=1, max_occurs=1,
|
|
consume_other=False):
|
|
super(All, self).__init__(elements, min_occurs, max_occurs)
|
|
self._consume_other = consume_other
|
|
|
|
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
|
|
"""Consume matching xmlelements
|
|
|
|
:param xmlelements: Dequeue of XML element objects
|
|
:type xmlelements: collections.deque of lxml.etree._Element
|
|
:param schema: The parent XML schema
|
|
:type schema: zeep.xsd.Schema
|
|
:param name: The name of the parent element
|
|
:type name: str
|
|
:param context: Optional parsing context (for inline schemas)
|
|
:type context: zeep.xsd.context.XmlParserContext
|
|
:rtype: dict or None
|
|
|
|
"""
|
|
result = OrderedDict()
|
|
expected_tags = {element.qname for __, element in self.elements}
|
|
consumed_tags = set()
|
|
|
|
values = defaultdict(deque)
|
|
for i, elm in enumerate(xmlelements):
|
|
if elm.tag in expected_tags:
|
|
consumed_tags.add(i)
|
|
values[elm.tag].append(elm)
|
|
|
|
# Remove the consumed tags from the xmlelements
|
|
for i in sorted(consumed_tags, reverse=True):
|
|
del xmlelements[i]
|
|
|
|
for name, element in self.elements:
|
|
sub_elements = values.get(element.qname)
|
|
if sub_elements:
|
|
result[name] = element.parse_xmlelements(
|
|
sub_elements, schema, context=context)
|
|
|
|
if self._consume_other and xmlelements:
|
|
result['_raw_elements'] = list(xmlelements)
|
|
xmlelements.clear()
|
|
return result
|
|
|
|
|
|
class Choice(OrderIndicator):
|
|
"""Permits one and only one of the elements contained in the group."""
|
|
|
|
def parse_args(self, args, index=0):
|
|
if args:
|
|
raise TypeError("Choice elements only work with keyword arguments")
|
|
|
|
@property
|
|
def is_optional(self):
|
|
return True
|
|
|
|
@property
|
|
def default_value(self):
|
|
return OrderedDict()
|
|
|
|
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
|
|
"""Consume matching xmlelements
|
|
|
|
:param xmlelements: Dequeue of XML element objects
|
|
:type xmlelements: collections.deque of lxml.etree._Element
|
|
:param schema: The parent XML schema
|
|
:type schema: zeep.xsd.Schema
|
|
:param name: The name of the parent element
|
|
:type name: str
|
|
:param context: Optional parsing context (for inline schemas)
|
|
:type context: zeep.xsd.context.XmlParserContext
|
|
:rtype: dict or None
|
|
|
|
"""
|
|
result = []
|
|
|
|
for _unused in max_occurs_iter(self.max_occurs):
|
|
if not xmlelements:
|
|
break
|
|
|
|
# Choose out of multiple
|
|
options = []
|
|
for element_name, element in self.elements_nested:
|
|
|
|
local_xmlelements = copy.copy(xmlelements)
|
|
|
|
try:
|
|
sub_result = element.parse_xmlelements(
|
|
xmlelements=local_xmlelements,
|
|
schema=schema,
|
|
name=element_name,
|
|
context=context)
|
|
except UnexpectedElementError:
|
|
continue
|
|
|
|
if isinstance(element, Element):
|
|
sub_result = {element_name: sub_result}
|
|
|
|
num_consumed = len(xmlelements) - len(local_xmlelements)
|
|
if num_consumed:
|
|
options.append((num_consumed, sub_result))
|
|
|
|
if not options:
|
|
xmlelements = []
|
|
break
|
|
|
|
# Sort on least left
|
|
options = sorted(options, key=operator.itemgetter(0), reverse=True)
|
|
if options:
|
|
result.append(options[0][1])
|
|
for i in range(options[0][0]):
|
|
xmlelements.popleft()
|
|
else:
|
|
break
|
|
|
|
if self.accepts_multiple:
|
|
result = {name: result}
|
|
else:
|
|
result = result[0] if result else {}
|
|
return result
|
|
|
|
def parse_kwargs(self, kwargs, name, available_kwargs):
|
|
"""Processes the kwargs for this choice element.
|
|
|
|
Returns a dict containing the values found.
|
|
|
|
This handles two distinct initialization methods:
|
|
|
|
1. Passing the choice elements directly to the kwargs (unnested)
|
|
2. Passing the choice elements into the `name` kwarg (_value_1) (nested).
|
|
This case is required when multiple choice elements are given.
|
|
|
|
:param name: Name of the choice element (_value_1)
|
|
:type name: str
|
|
:param element: Choice element object
|
|
:type element: zeep.xsd.Choice
|
|
:param kwargs: dict (or list of dicts) of kwargs for initialization
|
|
:type kwargs: list / dict
|
|
|
|
"""
|
|
if name and name in available_kwargs:
|
|
assert self.accepts_multiple
|
|
|
|
values = kwargs[name] or []
|
|
available_kwargs.remove(name)
|
|
result = []
|
|
|
|
if isinstance(values, dict):
|
|
values = [values]
|
|
|
|
# TODO: Use most greedy choice instead of first matching
|
|
for value in values:
|
|
for element in self:
|
|
if isinstance(element, OrderIndicator):
|
|
choice_value = value[name] if name in value else value
|
|
if element.accept(choice_value):
|
|
result.append(choice_value)
|
|
break
|
|
else:
|
|
if element.name in value:
|
|
choice_value = value.get(element.name)
|
|
result.append({element.name: choice_value})
|
|
break
|
|
else:
|
|
raise TypeError(
|
|
"No complete xsd:Sequence found for the xsd:Choice %r.\n"
|
|
"The signature is: %s" % (name, self.signature()))
|
|
|
|
if not self.accepts_multiple:
|
|
result = result[0] if result else None
|
|
else:
|
|
# Direct use-case isn't supported when maxOccurs > 1
|
|
if self.accepts_multiple:
|
|
return {}
|
|
|
|
result = {}
|
|
|
|
# When choice elements are specified directly in the kwargs
|
|
found = False
|
|
for name, choice in self.elements_nested:
|
|
temp_kwargs = copy.copy(available_kwargs)
|
|
subresult = choice.parse_kwargs(kwargs, name, temp_kwargs)
|
|
|
|
if subresult:
|
|
if not any(subresult.values()):
|
|
available_kwargs.intersection_update(temp_kwargs)
|
|
result.update(subresult)
|
|
elif not found:
|
|
available_kwargs.intersection_update(temp_kwargs)
|
|
result.update(subresult)
|
|
found = True
|
|
if found:
|
|
for choice_name, choice in self.elements:
|
|
result.setdefault(choice_name, None)
|
|
else:
|
|
result = {}
|
|
|
|
if name and self.accepts_multiple:
|
|
result = {name: result}
|
|
return result
|
|
|
|
def render(self, parent, value, render_path):
|
|
"""Render the value to the parent element tree node.
|
|
|
|
This is a bit more complex then the order render methods since we need
|
|
to search for the best matching choice element.
|
|
|
|
"""
|
|
if not self.accepts_multiple:
|
|
value = [value]
|
|
|
|
self.validate(value, render_path)
|
|
|
|
for item in value:
|
|
result = self._find_element_to_render(item)
|
|
if result:
|
|
element, choice_value = result
|
|
element.render(parent, choice_value, render_path)
|
|
|
|
def validate(self, value, render_path):
|
|
found = 0
|
|
for item in value:
|
|
result = self._find_element_to_render(item)
|
|
if result:
|
|
found += 1
|
|
|
|
if not found and not self.is_optional:
|
|
raise ValidationError("Missing choice values", path=render_path)
|
|
|
|
def accept(self, values):
|
|
"""Return the number of values which are accepted by this choice.
|
|
|
|
If not all required elements are available then 0 is returned.
|
|
|
|
"""
|
|
nums = set()
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, Element):
|
|
if self.accepts_multiple:
|
|
if all(name in item and item[name] for item in values):
|
|
nums.add(1)
|
|
else:
|
|
if name in values and values[name]:
|
|
nums.add(1)
|
|
else:
|
|
num = element.accept(values)
|
|
nums.add(num)
|
|
return max(nums) if nums else 0
|
|
|
|
def _find_element_to_render(self, value):
|
|
"""Return a tuple (element, value) for the best matching choice.
|
|
|
|
This is used to decide which choice child is best suitable for
|
|
rendering the available data.
|
|
|
|
"""
|
|
matches = []
|
|
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, Element):
|
|
if element.name in value:
|
|
try:
|
|
choice_value = value[element.name]
|
|
except KeyError:
|
|
choice_value = value
|
|
|
|
if choice_value is not None:
|
|
matches.append((1, element, choice_value))
|
|
else:
|
|
if name is not None:
|
|
try:
|
|
choice_value = value[name]
|
|
except KeyError:
|
|
choice_value = value
|
|
else:
|
|
choice_value = value
|
|
|
|
score = element.accept(choice_value)
|
|
if score:
|
|
matches.append((score, element, choice_value))
|
|
|
|
if matches:
|
|
matches = sorted(matches, key=operator.itemgetter(0), reverse=True)
|
|
return matches[0][1:]
|
|
|
|
def signature(self, schema=None, standalone=True):
|
|
parts = []
|
|
for name, element in self.elements_nested:
|
|
if isinstance(element, OrderIndicator):
|
|
parts.append('{%s}' % (element.signature(schema, standalone=False)))
|
|
else:
|
|
parts.append('{%s: %s}' % (name, element.signature(schema, standalone=False)))
|
|
part = '(%s)' % ' | '.join(parts)
|
|
if self.accepts_multiple:
|
|
return '%s[]' % (part,)
|
|
return part
|
|
|
|
|
|
class Sequence(OrderIndicator):
|
|
"""Requires the elements in the group to appear in the specified sequence
|
|
within the containing element.
|
|
|
|
"""
|
|
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
|
|
"""Consume matching xmlelements
|
|
|
|
:param xmlelements: Dequeue of XML element objects
|
|
:type xmlelements: collections.deque of lxml.etree._Element
|
|
:param schema: The parent XML schema
|
|
:type schema: zeep.xsd.Schema
|
|
:param name: The name of the parent element
|
|
:type name: str
|
|
:param context: Optional parsing context (for inline schemas)
|
|
:type context: zeep.xsd.context.XmlParserContext
|
|
:rtype: dict or None
|
|
|
|
"""
|
|
result = []
|
|
|
|
if self.accepts_multiple:
|
|
assert name
|
|
|
|
for _unused in max_occurs_iter(self.max_occurs):
|
|
if not xmlelements:
|
|
break
|
|
|
|
item_result = OrderedDict()
|
|
for elm_name, element in self.elements:
|
|
try:
|
|
item_subresult = element.parse_xmlelements(
|
|
xmlelements, schema, name, context=context)
|
|
except UnexpectedElementError:
|
|
if schema.strict:
|
|
raise
|
|
item_subresult = None
|
|
|
|
# Unwrap if allowed
|
|
if isinstance(element, OrderIndicator):
|
|
item_result.update(item_subresult)
|
|
else:
|
|
item_result[elm_name] = item_subresult
|
|
|
|
if not xmlelements:
|
|
break
|
|
if item_result:
|
|
result.append(item_result)
|
|
|
|
if not self.accepts_multiple:
|
|
return result[0] if result else None
|
|
return {name: result}
|
|
|
|
|
|
class Group(Indicator):
|
|
"""Groups a set of element declarations so that they can be incorporated as
|
|
a group into complex type definitions.
|
|
|
|
"""
|
|
|
|
def __init__(self, name, child, max_occurs=1, min_occurs=1):
|
|
super(Group, self).__init__()
|
|
self.child = child
|
|
self.qname = name
|
|
self.name = name.localname if name else None
|
|
self.max_occurs = max_occurs
|
|
self.min_occurs = min_occurs
|
|
|
|
def __str__(self):
|
|
return self.signature()
|
|
|
|
def __iter__(self, *args, **kwargs):
|
|
for item in self.child:
|
|
yield item
|
|
|
|
@threaded_cached_property
|
|
def elements(self):
|
|
if self.accepts_multiple:
|
|
return [('_value_1', self.child)]
|
|
return self.child.elements
|
|
|
|
def clone(self, name, min_occurs=1, max_occurs=1):
|
|
return self.__class__(
|
|
name=None,
|
|
child=self.child,
|
|
min_occurs=min_occurs,
|
|
max_occurs=max_occurs)
|
|
|
|
def accept(self, values):
|
|
"""Return the number of values which are accepted by this choice.
|
|
|
|
If not all required elements are available then 0 is returned.
|
|
|
|
"""
|
|
return self.child.accept(values)
|
|
|
|
def parse_args(self, args, index=0):
|
|
return self.child.parse_args(args, index)
|
|
|
|
def parse_kwargs(self, kwargs, name, available_kwargs):
|
|
if self.accepts_multiple:
|
|
if name not in kwargs:
|
|
return {}
|
|
|
|
available_kwargs.remove(name)
|
|
item_kwargs = kwargs[name]
|
|
|
|
result = []
|
|
sub_name = '_value_1' if self.child.accepts_multiple else None
|
|
for sub_kwargs in max_occurs_iter(self.max_occurs, item_kwargs):
|
|
available_sub_kwargs = set(sub_kwargs.keys())
|
|
subresult = self.child.parse_kwargs(
|
|
sub_kwargs, sub_name, available_sub_kwargs)
|
|
|
|
if available_sub_kwargs:
|
|
raise TypeError((
|
|
"%s() got an unexpected keyword argument %r."
|
|
) % (self, list(available_sub_kwargs)[0]))
|
|
|
|
if subresult:
|
|
result.append(subresult)
|
|
if result:
|
|
result = {name: result}
|
|
else:
|
|
result = self.child.parse_kwargs(kwargs, name, available_kwargs)
|
|
return result
|
|
|
|
def parse_xmlelements(self, xmlelements, schema, name=None, context=None):
|
|
"""Consume matching xmlelements
|
|
|
|
:param xmlelements: Dequeue of XML element objects
|
|
:type xmlelements: collections.deque of lxml.etree._Element
|
|
:param schema: The parent XML schema
|
|
:type schema: zeep.xsd.Schema
|
|
:param name: The name of the parent element
|
|
:type name: str
|
|
:param context: Optional parsing context (for inline schemas)
|
|
:type context: zeep.xsd.context.XmlParserContext
|
|
:rtype: dict or None
|
|
|
|
"""
|
|
result = []
|
|
|
|
for _unused in max_occurs_iter(self.max_occurs):
|
|
result.append(
|
|
self.child.parse_xmlelements(
|
|
xmlelements, schema, name, context=context)
|
|
)
|
|
if not xmlelements:
|
|
break
|
|
if not self.accepts_multiple and result:
|
|
return result[0]
|
|
return {name: result}
|
|
|
|
def render(self, parent, value, render_path):
|
|
if not isinstance(value, list):
|
|
values = [value]
|
|
else:
|
|
values = value
|
|
|
|
for value in values:
|
|
self.child.render(parent, value, render_path)
|
|
|
|
def resolve(self):
|
|
self.child = self.child.resolve()
|
|
return self
|
|
|
|
def signature(self, schema=None, standalone=True):
|
|
name = create_prefixed_name(self.qname, schema)
|
|
if standalone:
|
|
return '%s(%s)' % (
|
|
name, self.child.signature(schema, standalone=False))
|
|
else:
|
|
return self.child.signature(schema, standalone=False)
|