python-zeep icon indicating copy to clipboard operation
python-zeep copied to clipboard

Soap Headers, pass an lxml Element object when the wsdl doesn’t define it but the server expects it?

Open johnziebro opened this issue 2 years ago • 3 comments

I've had a long-standing question on Stackoverflow regarding this question, but have not found a solution/answer here, 7k views.

The basics issue is that the api has both an authentication and a paging mechanism that are not defined in the wsdl. Authentication does work by providing a custom xsd.Element. Solutions manually defining the xsd.Element for pager have not worked since the wsdl does not appear to expect it.

API Docs:

Pagination; this method returns paginated results. To specify pages or results per page, use the pager header:

  <ns:pager>
    <page>1</page>
    <per_page>100</per_page>
  </ns:pager>
</soapenv:Header>

Max per page is 100

Pagination information is returned in a pager header:

<soapenv:Header>
  <ns:pager>
    <page>1</page>
    <per_page>100</per_page>
    <next_page>2</next_page>
    <page_items>100</page_items>
    <total_items>2829</total_items>
    <total_pages>29</total_pages>
  </ns:pager>
</soapenv:Header>

A header snippet provided by the API developer:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="https://api.redacted.net/2.0/">
   <soapenv:Header>
     <ns:apiKey>
        <api_key>***</api_key>
     </ns:apiKey>
     <pager>
        <page>2</page>
        <per_page>10</per_page>
     </pager>
   </soapenv:Header>
   <soapenv:Body>
      <ns:getCaseUpdates>
         <case_id>2038217180</case_id>
      </ns:getCaseUpdates>
   </soapenv:Body>
</soapenv:Envelope>

Zeep Soap Headers docs page provides a note for an alternative solution. The SO question which does address use of lxml appears to replace the Header element completely. This won't work since the API requires authentication through the Header as well, and that part is currently working (again even though it is not defined in the wsdl).

"4. Another option is to pass an lxml Element object. This is generally useful if the wsdl doesn’t define a soap header but the server does expect it."

Could you direct me to an example for this method, or if possible help with an answer to the SO question?

Thank you for your time and work on this project.

johnziebro avatar Apr 23 '22 18:04 johnziebro

I've bountied the SO question for the next 7 days, any answers would be appreciated. https://stackoverflow.com/questions/54201921/in-python-how-to-set-soapheaders-for-zeep-using-dictionaries

johnziebro avatar Apr 24 '22 12:04 johnziebro

This method shows in comments some of the approaches I have tried:

    def get_pager(self, page: int = 1, per_page: int = 100):
        """ Create non-namespaced header element that contains the page and records per page.
        
        <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:ns="https://api.redacted.net/2.0/">
           <soapenv:Header>
             <ns:apiKey>
                <api_key>***</api_key>
             </ns:apiKey>
             <pager>
                <page>2</page>
                <per_page>100</per_page>
             </pager>
           </soapenv:Header>
           <soapenv:Body>
              <ns:getCaseUpdates>
                 <case_id>2038217180</case_id>
              </ns:getCaseUpdates>
           </soapenv:Body>
        </soapenv:Envelope>
        """
        
        result = None
        
        # from factory, wont work; pager is not in WSDL
        """result = self.factory.pager()
        result.page = page
        result.per_page = per_page"""

        # as complex type, does not work do to namespacing?
        """result = xsd.Element(
            'pager',
            xsd.ComplexType([
                xsd.Element(
                    'page', xsd.Integer()
                ),
                xsd.Element(
                    'per_page', xsd.Integer()
                )
            ])
        )
        result = result(page, per_page)"""

        # as dict won't work
        """ result = {"pager": {"page": page, "per_page": per_page}}"""

        return result

johnziebro avatar Apr 24 '22 13:04 johnziebro

Attempting to combine the AuthenticateRequest and pager elements in this way fails:

def get_header_w_pager(self, page: int = 1, per_page: int = 100):
    """ Create persistent header that contains the api key and non-namespaced pager."""

    header = xsd.ComplexType([
        xsd.Element(
            "{wsdl}AuthenticateRequest",
            xsd.ComplexType([
                xsd.Element("{wsdl}api_key", xsd.String())]),
        ),
        xsd.Element(
            'pager',
            xsd.ComplexType([
                xsd.Element(
                    'page', xsd.Integer()
                ),
                xsd.Element(
                    'per_page', xsd.Integer()
                )
            ])
        )
    ])

    # Insert api key into persistent header
    api_key = f"{self.config_prefix}KEY"
    key = CFG.get(api_key)
    return header(api_key=key, page=page, per_page=per_page)

TypeError: ComplexType() got an unexpected keyword argument 'page'. Signature: AuthenticateRequest: {api_key: xsd:string}, pager: {page: xsd:integer, per_page: xsd:integer}

johnziebro avatar Apr 24 '22 13:04 johnziebro