pythonsoapxsdwsdlzeep

Creating XML sequences with zeep / python


I am using zeep (Python 3.6) to interface with a SOAP API, and working with a WSDL schema which contains this section:

<xs:element name="passengers">
    <xs:complexType>
        <xs:sequence>
            <xs:element maxOccurs="unbounded" name="passenger" type="com:PassengerType"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

So I'd like my zeep-generated XML to look like this:

<book:passengers>
    <book:passenger>
        ...redacted...
    </book:passenger>
</book:passengers>

My first attempt at achieving this with Zeep looked like this:

passengers = [factories.PassengerType()]

However, when sending this to my SOAP API, this generated the following error:

File "/usr/local/lib/python3.6/site-packages/zeep/xsd/elements/element.py", line 220, in validate
  "Missing element %s" % (self.name), path=render_path)
zeep.exceptions.ValidationError: Missing element passenger (createBookingRecordRequest.passengers)

I believe this is because my 'passengers' attribute should contain a Zeep object with the tag name "passenger", which would contain my list of elements. I've tried tinkering with the zeep.xsd.AnyType to achieve this, but haven't succeeded yet.

Any suggestions would be appreciated.


Solution

  • Answering my own question as I've now solved it, and not received any other answers.

    The root of this problem is that I am trying to create XML element that is not explicitly defined as a type by my SOAP API's WSDL. This is OK though, as Zeep will still be generating type objects for it, it just won't be assigning those types to a particular name, so we have to jump through a few extra hoops to get at those types. This is what it took me a little time to figure out.

    You can get at these objects by accessing them through any parent type. They are stored in an attribute named elements as a list of 2-tuples. In this case, my PassengerType objects are supposed to be contained by a sequence container with the attribute name 'passengers'. If, for example, my parent type is named ParentType, I could use this 'passengers' sequence like so:

    passengers = dict(ParentType.elements)['passengers'](
        PassengerType(),
        ...
        PassengerType()
    )
    

    Here, we are turning the elements object into a dict (exploiting the fact that it's a list of 2-tuples where the first item is a string of the attribute name), and then pulling the elements out by name.

    The resultant object can be passed directly into ParentType like:

    ParentType(passengers=passengers)
    

    Simple.

    One other alternative I discovered is to explicitly build the type with zeep's xsd objects. Example shown below.

    from lxml import etree
    from zeep import xsd
    
    PassengersType = xsd.ComplexType(
        xsd.Sequence([
            xsd.Element('passengers', PassengerType, min_occurs=1, max_occurs='unbounded')
        ]), qname=etree.QName("{http://example.com/schema}passengers")
    )
    

    I think this isn't as nice, but might be useful to someone landing here.