Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing array compared to suds-jurko #67

Closed
tpow opened this issue Jan 5, 2022 · 6 comments
Closed

Missing array compared to suds-jurko #67

tpow opened this issue Jan 5, 2022 · 6 comments

Comments

@tpow
Copy link

tpow commented Jan 5, 2022

We recently evaluated switching to suds-community from suds-jurko and found a compatibility problem. Elements that were previous populated with empty lists are now set to None. Here's the comparison:

suds-community gives this:

    (PostLedgerRequest){
       TokenId = None
       Payments = None
       Charges = None
       Refunds = None
       Voids = None
    }

Versus suds-jurko:

    (PostLedgerRequest){
       TokenId = None
       Payments =
          (ArrayOfPaymentInMsg){
             PaymentInMsg[] = <empty>
          }
       Charges =
          (ArrayOfChargeInMsg){
             ChargeInMsg[] = <empty>
          }
       Refunds =
          (ArrayOfRefundInMsg){
             RefundInMsg[] = <empty>
          }
       Voids =
          (ArrayOfVoidInMsg){
             VoidInMsg[] = <empty>
          }
    }

I've dug in to the problem and narrowed it down to the change in behavior due to #14, #15, and #16. Reverting the changed line (as in setattr(data, type.name, value) # if not type.optional() or type.multi_occurrence() else None)) in builder.py fixes the problem for us, but think I understand why the change was made in the first place so suspect that removing all of it is not the right solution.

My guess is that multi_occurrence() needs to look recursively into the nesting before failing. In our case the top level is optional, but the next is not.

It is also possible that we aren't using suds correctly or misunderstand the structure somehow, but I can say that the code works with sud-jurko and not with suds-community.

Although the API we are calling is not public and is controlled by a vendor, I have built the following test that demonstrates the problem. If dropped into the test suite, it should run and fail. Note that this is a pruned down and somewhat modified version of the vendor's schema and almost certainly could be simplified more. Sorry that it is a bit noisy.

It looks like incorporating parts of this into the test suite, perhaps as an expansion to the changes made for the #16 pull request would be beneficial.

We would appreciate any clarification if we are misusing suds somehow and/or confirmation that this is a bug.

Thanks! Tim

import suds
import testutils

ledger_wsdl = """<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://www.example.com/Soa/Foundation/" xmlns:s1="http://www.example.com/Soa/Foundation/MessageDefinition.xsd" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" targetNamespace="http://www.example.com/Soa/Foundation/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
  <wsdl:types>
    <s:schema elementFormDefault="qualified" targetNamespace="http://www.example.com/Soa/Foundation/">
      <s:import namespace="http://www.example.com/Soa/Foundation/MessageDefinition.xsd" />
      <s:element name="PostLedger">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" ref="s1:PostLedgerRequest" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:element name="PostLedgerResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" ref="s1:PostLedgerResponse" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:element name="RequestHeader" type="tns:RequestHeader" />
      <s:complexType name="RequestHeader">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="Headers" type="tns:ArrayOfAnyType" />
        </s:sequence>
        <s:anyAttribute />
      </s:complexType>
      <s:complexType name="ArrayOfAnyType">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="anyType" nillable="true" />
        </s:sequence>
      </s:complexType>
      <s:element name="ResponseHeader" type="tns:ResponseHeader" />
      <s:complexType name="ResponseHeader">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="Headers" type="tns:ArrayOfAnyType" />
        </s:sequence>
        <s:anyAttribute />
      </s:complexType>
    </s:schema>
    <s:schema elementFormDefault="qualified" targetNamespace="http://www.example.com/Soa/Foundation/MessageDefinition.xsd">
      <s:element name="PostLedgerRequest" type="s1:PostLedgerRequest" />
      <s:complexType name="PostLedgerRequest">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericRequest">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="1" name="Payments" type="s1:ArrayOfPaymentInMsg" />
              <s:element minOccurs="0" maxOccurs="1" name="Charges" type="s1:ArrayOfChargeInMsg" />
              <s:element minOccurs="0" maxOccurs="1" name="Refunds" type="s1:ArrayOfRefundInMsg" />
              <s:element minOccurs="0" maxOccurs="1" name="Voids" type="s1:ArrayOfVoidInMsg" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="GenericRequest">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="TokenId" type="s:string" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfPaymentInMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="PaymentInMsg" nillable="true" type="s1:PaymentInMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="PaymentInMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:LedgerInMsg">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="ScheduledPayment" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="1" default="false" name="AddScheduledPayment" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="EnrollmentId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentAcademicYearId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="None" name="AwardYear" type="s1:AwardYearType" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="CourseId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentAddressId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="AddressTypeId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="BankAccountId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="" name="CCAuthorization" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" default="" name="CCDeclineCode" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="CreditCardId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1.0" name="InterestAmount" type="s:decimal" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="FundSourceId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentAidId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="" name="CheckNumber" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="CreditCardBatchId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="ScheduledPaymentId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="unbounded" name="PaymentDistributionMsgs" type="s1:PaymentDistributionMsg" />
              <s:element minOccurs="1" maxOccurs="1" name="PaymentType" type="s1:PaymentType" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="LedgerInMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericInMsg">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="StudentId" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="Amount" type="s:decimal" />
              <s:element minOccurs="1" maxOccurs="1" name="TransactionDate" type="s:dateTime" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="CampusId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Description" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" default="1900-01-01T00:00:00" name="PostingDate" type="s:dateTime" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="TermId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="" name="ReferenceNumber" type="s:string" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="GenericInMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericMsg">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="1" name="MessageState" type="s:string" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="GenericMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" default="-1" name="MessageId" type="s:int" />
          <s:element minOccurs="0" maxOccurs="1" name="CustomAttributes" type="s1:ArrayOfCustomAttributeMsg" />
          <s:element minOccurs="0" maxOccurs="1" default="-1" name="CorrelationId" type="s:int" />
          <s:element minOccurs="0" maxOccurs="1" default="false" name="SkipSemanticValidation" type="s:boolean" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfCustomAttributeMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="CustomAttributeMsg" nillable="true" type="s1:CustomAttributeMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="CustomAttributeMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="Name" type="s:string" />
          <s:element minOccurs="0" maxOccurs="1" name="Value" type="s:string" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="GenericOutMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericMsg">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="1" name="MessageResult" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" default="OK" name="MessageStatus" type="s1:MessageStatusType" />
              <s:element minOccurs="0" maxOccurs="1" name="MessageErrorCode" type="s:string" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:simpleType name="MessageStatusType">
        <s:restriction base="s:string">
          <s:enumeration value="OK" />
          <s:enumeration value="FailedValidation" />
          <s:enumeration value="FailedExecution" />
          <s:enumeration value="FailedAuthorization" />
          <s:enumeration value="FailedOther" />
        </s:restriction>
      </s:simpleType>
      <s:complexType name="VoidOutMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericOutMsg" />
        </s:complexContent>
      </s:complexType>
      <s:complexType name="LedgerOutMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericOutMsg">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="Id" type="s:int" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="RefundOutMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:LedgerOutMsg" />
        </s:complexContent>
      </s:complexType>
      <s:complexType name="ChargeOutMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:LedgerOutMsg" />
        </s:complexContent>
      </s:complexType>
      <s:complexType name="PaymentOutMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:LedgerOutMsg" />
        </s:complexContent>
      </s:complexType>
      <s:complexType name="VoidInMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericInMsg">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="StudentId" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="TransactionId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Comment" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="IsNonSufficientFunds" type="s:boolean" />
              <s:element minOccurs="1" maxOccurs="1" name="IsReschedule" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="SubsidiaryId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="1900-01-01T00:00:00" name="TransactionDate" type="s:dateTime" />
              <s:element minOccurs="0" maxOccurs="1" default="1900-01-01T00:00:00" name="PostingDate" type="s:dateTime" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="RefundInMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:LedgerInMsg">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="ScheduledRefunds" type="s:boolean" />
              <s:element minOccurs="1" maxOccurs="1" name="ReadyToSendToCOD" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="EnrollmentId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="FaHeaderId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentAcademicYearId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="Comment" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" name="CheckNumber" type="s:string" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="BankAccountId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentAddressId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="1900-01-01T00:00:00" name="DateSent" type="s:dateTime" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="RefundId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" name="ReceiptNo" type="s:string" />
              <s:element minOccurs="1" maxOccurs="1" name="IsStipend" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="CreditCardBatchId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StipendSchedId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentCreditCardId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="false" name="AllowRefundGreaterThanBalance" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="unbounded" name="DisbursementAmountMsg" type="s1:DisbursementAmountMsg" />
              <s:element minOccurs="1" maxOccurs="1" name="ReturnMethod" type="s1:ReturnMethod" />
              <s:element minOccurs="1" maxOccurs="1" name="PaymentType" type="s1:PaymentType" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="DisbursementAmountMsg">
        <s:sequence>
          <s:element minOccurs="1" maxOccurs="1" name="DisbursementId" type="s:int" />
          <s:element minOccurs="1" maxOccurs="1" name="Amount" type="s:decimal" />
        </s:sequence>
      </s:complexType>
      <s:simpleType name="ReturnMethod">
        <s:restriction base="s:string">
          <s:enumeration value="Net" />
          <s:enumeration value="Check" />
          <s:enumeration value="MasterCheck" />
          <s:enumeration value="EFT" />
          <s:enumeration value="ACH" />
          <s:enumeration value="CreditCard" />
        </s:restriction>
      </s:simpleType>
      <s:simpleType name="PaymentType">
        <s:restriction base="s:string">
          <s:enumeration value="Cash" />
          <s:enumeration value="Check" />
          <s:enumeration value="CreditCard" />
          <s:enumeration value="EFT" />
          <s:enumeration value="NonCash" />
          <s:enumeration value="ACH" />
          <s:enumeration value="Other" />
        </s:restriction>
      </s:simpleType>
      <s:complexType name="ChargeInMsg">
        <s:complexContent mixed="false">
          <s:extension base="s1:LedgerInMsg">
            <s:sequence>
              <s:element minOccurs="1" maxOccurs="1" name="TransactionCodeId" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="TransactionType" type="s1:TransactionType" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="StudentAcademicYearId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="EnrollmentId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="false" name="UpdateEnrollmentDateBilled" type="s:boolean" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="PaymentPeriodId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="CourseSectionId" type="s:int" />
              <s:element minOccurs="0" maxOccurs="1" default="-1" name="FeeId" type="s:int" />
              <s:element minOccurs="1" maxOccurs="1" name="Offset" nillable="true" type="s:int" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:simpleType name="TransactionType">
        <s:restriction base="s:string">
          <s:enumeration value="Invoice" />
          <s:enumeration value="DebitMemo" />
          <s:enumeration value="CreditMemo" />
          <s:enumeration value="Payment" />
        </s:restriction>
      </s:simpleType>
      <s:simpleType name="AwardYearType">
        <s:restriction base="s:string">
          <s:enumeration value="None" />
          <s:enumeration value="AY2020_21" />
          <s:enumeration value="AY2021_22" />
          <s:enumeration value="AY2022_23" />
          <s:enumeration value="AY2023_24" />
          <s:enumeration value="AY2024_25" />
          <s:enumeration value="AY2025_26" />
        </s:restriction>
      </s:simpleType>
      <s:complexType name="PaymentDistributionMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="BillCode" type="s:string" />
          <s:element minOccurs="1" maxOccurs="1" name="AmountApplied" type="s:decimal" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfChargeInMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="ChargeInMsg" nillable="true" type="s1:ChargeInMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfRefundInMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="RefundInMsg" nillable="true" type="s1:RefundInMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfVoidInMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="VoidInMsg" nillable="true" type="s1:VoidInMsg" />
        </s:sequence>
      </s:complexType>
      <s:element name="PostLedgerResponse" type="s1:PostLedgerResponse" />
      <s:complexType name="PostLedgerResponse">
        <s:complexContent mixed="false">
          <s:extension base="s1:GenericResponse">
            <s:sequence>
              <s:element minOccurs="0" maxOccurs="1" name="Payments" type="s1:ArrayOfPaymentOutMsg" />
              <s:element minOccurs="0" maxOccurs="1" name="Charges" type="s1:ArrayOfChargeOutMsg" />
              <s:element minOccurs="0" maxOccurs="1" name="Refunds" type="s1:ArrayOfRefundOutMsg" />
              <s:element minOccurs="0" maxOccurs="1" name="Voids" type="s1:ArrayOfVoidOutMsg" />
            </s:sequence>
          </s:extension>
        </s:complexContent>
      </s:complexType>
      <s:complexType name="GenericResponse">
        <s:sequence>
          <s:element minOccurs="1" maxOccurs="1" name="Status" type="s1:TrxStatus" />
          <s:element minOccurs="0" maxOccurs="1" name="TrxResult" type="s:string" />
          <s:element minOccurs="0" maxOccurs="1" name="TokenId" type="s:string" />
        </s:sequence>
      </s:complexType>
      <s:simpleType name="TrxStatus">
        <s:restriction base="s:string">
          <s:enumeration value="OK" />
          <s:enumeration value="ErrorSQL" />
          <s:enumeration value="ErrorBusinessLogic" />
          <s:enumeration value="ErrorWebService" />
          <s:enumeration value="ErrorArguments" />
          <s:enumeration value="ErrorSecurity" />
          <s:enumeration value="ErrorSystem" />
          <s:enumeration value="ErrorMultiple" />
        </s:restriction>
      </s:simpleType>
      <s:complexType name="ArrayOfPaymentOutMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="PaymentOutMsg" nillable="true" type="s1:PaymentOutMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfChargeOutMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="ChargeOutMsg" nillable="true" type="s1:ChargeOutMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfRefundOutMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="RefundOutMsg" nillable="true" type="s1:RefundOutMsg" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfVoidOutMsg">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="VoidOutMsg" nillable="true" type="s1:VoidOutMsg" />
        </s:sequence>
      </s:complexType>
    </s:schema>
  </wsdl:types>
  <wsdl:message name="PostLedgerSoapIn">
    <wsdl:part name="parameters" element="tns:PostLedger" />
  </wsdl:message>
  <wsdl:message name="PostLedgerSoapOut">
    <wsdl:part name="parameters" element="tns:PostLedgerResponse" />
  </wsdl:message>
  <wsdl:message name="PostLedgerRequestHeader">
    <wsdl:part name="RequestHeader" element="tns:RequestHeader" />
  </wsdl:message>
  <wsdl:message name="PostLedgerResponseHeader">
    <wsdl:part name="ResponseHeader" element="tns:ResponseHeader" />
  </wsdl:message>
  <wsdl:portType name="LedgerWebServiceSoap">
    <wsdl:operation name="PostLedger">
      <wsdl:input message="tns:PostLedgerSoapIn" />
      <wsdl:output message="tns:PostLedgerSoapOut" />
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="LedgerWebServiceSoap" type="tns:LedgerWebServiceSoap">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="PostLedger">
      <soap:operation soapAction="http://www.example.com/Soa/Foundation/PostLedger" style="document" />
      <wsdl:input>
        <soap:body use="literal" />
        <soap:header message="tns:PostLedgerRequestHeader" part="RequestHeader" use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal" />
        <soap:header message="tns:PostLedgerResponseHeader" part="ResponseHeader" use="literal" />
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:binding name="LedgerWebServiceSoap12" type="tns:LedgerWebServiceSoap">
    <soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="PostLedger">
      <soap12:operation soapAction="http://www.example.com/Soa/Foundation/PostLedger" style="document" />
      <wsdl:input>
        <soap12:body use="literal" />
        <soap12:header message="tns:PostLedgerRequestHeader" part="RequestHeader" use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap12:body use="literal" />
        <soap12:header message="tns:PostLedgerResponseHeader" part="ResponseHeader" use="literal" />
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="LedgerWebService">
    <wsdl:port name="LedgerWebServiceSoap" binding="tns:LedgerWebServiceSoap">
      <soap:address location="https://api.example.com/webservices/LedgerWebService.asmx" />
    </wsdl:port>
    <wsdl:port name="LedgerWebServiceSoap12" binding="tns:LedgerWebServiceSoap12">
      <soap12:address location="https://api.example.com/webservices/LedgerWebService.asmx" />
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>
"""

def test_missing_array():
    """
    suds-community gives this:
    -----
    (PostLedgerRequest){
       TokenId = None
       Payments = None
       Charges = None
       Refunds = None
       Voids = None
    }

    versus suds-jurko:
    ----
    (PostLedgerRequest){
       TokenId = None
       Payments =
          (ArrayOfPaymentInMsg){
             PaymentInMsg[] = <empty>
          }
       Charges =
          (ArrayOfChargeInMsg){
             ChargeInMsg[] = <empty>
          }
       Refunds =
          (ArrayOfRefundInMsg){
             RefundInMsg[] = <empty>
          }
       Voids =
          (ArrayOfVoidInMsg){
             VoidInMsg[] = <empty>
          }
    }

    All of this seems to be caused by github issues #14, #15, and #16
    https://github.com/suds-community/suds/pull/16
    """
    wsdl = suds.byte_str(ledger_wsdl)
    client = testutils.client_from_wsdl(wsdl)
    # print(client)
    assert client is not None
    req = client.factory.create("ns1:PostLedgerRequest")
    print(req)
    for a in ["Payments", "Charges", "Refunds", "Voids"]:
        assert hasattr(req, a)
        assert getattr(req, a) is not None
    assert req.Payments.__class__.__name__ == "ArrayOfPaymentInMsg"
    assert req.Charges.__class__.__name__ == "ArrayOfChargeInMsg"
    assert req.Refunds.__class__.__name__ == "ArrayOfRefundInMsg"
    assert req.Voids.__class__.__name__ == "ArrayOfVoidInMsg"

    print(req.Payments)
    print(req.Payments.PaymentInMsg)
    assert isinstance(req.Payments.PaymentInMsg, list)

    # Try setting the value
    sample_charge = client.factory.create("ns1:ChargeInMsg")
    # ... We would populate all of sample_charge here
    sample_charge.StudentId = 1234
    sample_charge.Description = "Room and Board"
    sample_charge.Amount = 987.65
    # Add to the request
    req.Charges.ChargeInMsg = [sample_charge,]
    # if req.Charges is None, cannot do above, but
    # the following doesn't seem to work correctly
    # req.Charges = [sample_charge,]
    print(req)
    assert req.Charges.ChargeInMsg
    print(req.Charges.ChargeInMsg[0])
    assert req.Charges.ChargeInMsg[0] == sample_charge
@phillbaker
Copy link
Member

Hi @tpow thanks for opening an issue and for providing a test case, that's fantastic.

My guess is that multi_occurrence() needs to look recursively into the nesting before failing. In our case the top level is optional, but the next is not.

To clarify here, Payments, Charges, Refunds, Voids are marked as optional with the minOccurs="0":

<s:element minOccurs="0" maxOccurs="1" name="Payments" type="s1:ArrayOfPaymentInMsg" />
<s:element minOccurs="0" maxOccurs="1" name="Charges" type="s1:ArrayOfChargeInMsg" />
<s:element minOccurs="0" maxOccurs="1" name="Refunds" type="s1:ArrayOfRefundInMsg" />
<s:element minOccurs="0" maxOccurs="1" name="Voids" type="s1:ArrayOfVoidInMsg" />

where is the next level marked as required? Does the WSDL itself leave these objects as optional, but the server interpretation require them?

req.Charges.ChargeInMsg = [sample_charge,]
# if req.Charges is None, cannot do above, but
# the following doesn't seem to work correctly
# req.Charges = [sample_charge,]

Would something like req.Charges = client.factory.create("ns1:ArrayOfChargeInMsg") work?

Since this is less convenient and a pain to convert codebases if you're trying to convert to suds-community, one workaround is here: BingAds/BingAds-Python-SDK@8d1a515#diff-84a7c57706647cffbba7321972ce82a9aa06e7d49b8c32b4d7a34417ffba9e45R13, provide a custom Builder class to client.factory.builder that has the desired behavior.

Since this has come up a few times, I could see a few options going forward:

  • Update the code if there is a way to determine whether the attribute is optional by examining the WSDL
  • Add an explicit configuration option to the top level suds client configuring this
  • Refactor the Builder class to extract a method to determine if a node should be built. By default it would have the current implementation, but could be more easily subclassed and overridden without having to duplicate logic. E.g.
class Builder:
    # ...
    def skip_value(self, type):
        """ get whether or not to skip setting the value """
        return type.optional() and not type.multi_occurrence()

class MyBuilder:
    def skip_value(self, type):
        return False # always set value

What do you think?

@tpow
Copy link
Author

tpow commented Jan 6, 2022

@phillbaker Thank you for the helpful feedback and suggestions. It is possible that this API is incorrectly constructed, but as we say at work "the real world is messy" and we need to deal with it.

When I said that multi_occurrence() may need to look recursively, I was thinking about this (sorry I didn't note this explicitly):

Although as you noticed, these are "optional" with minOccurs = "0"

<s:element minOccurs="0" maxOccurs="1" name="Payments" type="s1:ArrayOfPaymentInMsg" />
<s:element minOccurs="0" maxOccurs="1" name="Charges" type="s1:ArrayOfChargeInMsg" />
<s:element minOccurs="0" maxOccurs="1" name="Refunds" type="s1:ArrayOfRefundInMsg" />
<s:element minOccurs="0" maxOccurs="1" name="Voids" type="s1:ArrayOfVoidInMsg" />

However, the elements they reference are not (and also are complex types). For example, the ArrayOfChargeInMsg has maxOccurs="unbounded":

<s:complexType name="ArrayOfChargeInMsg">
      <s:sequence>
        <s:element minOccurs="0" maxOccurs="unbounded" name="ChargeInMsg" nillable="true"
    type="s1:ChargeInMsg" />
      </s:sequence>
</s:complexType>

Because of this, I was thinking Payments, Charges, Refunds and Voids would all be created and set to empty lists like with suds-jurko.

I'm not sure if this postledger API would handle some of them being None. Based on the schema, I would hope so. I suspect the real reason it fails is because it needs at least one of them to be populated, but I'm not certain. You might be correct that I could do:

req.Charges = client.factory.create("ns1:ArrayOfChargeInMsg")

but then I would still need to create the ChargeInMsg and do the setup. It would be a little inconvenient to have to do both. That was my understanding of why multi_occurrence() was created in the first place -- it was trying to make it so arrays existed even though they are "optional". I think this is a nested complex type variant of the same problem. (In other words, your first bullet point: I believe there a way to determine whether the attribute is optional, or rather is a list. The others may be helpful for those messy real world situations, but I think it would be easier if suds provided the empty lists in this case.)

What are your thoughts?
Tim

@tpow
Copy link
Author

tpow commented Jan 6, 2022

Update: we tested the API with additional code:

if req.Charges is None:
    req.Charges = client.factory.create("ns1:ArrayOfChargeInMsg")

This generated a request like this:

(PostLedgerRequest){
  TokenId = "<redacted>"
  Payments = None
  Charges =
    (ArrayOfChargeInMsg){
      ChargeInMsg[] =
      (ChargeInMsg){
          StudentId = 1234
          Amount = 987.65
          Description = "Room and Board"
          ... plus other parameters redacted for simplicity and clarity ... 
      },
    }
  Refunds = None
  Voids = None
}

This worked fine, so the API is handling the "optional" values as the schema indicates. I still think it would be convenient (and assist with the transition from suds-jurko) to construct the empty lists so we don't need to build the ArrayOfChargeInMsg, but this works.

Thanks!

@phillbaker
Copy link
Member

I pushed a change which tries to implement the suggestion on https://github.com/suds-community/suds/tree/bugfix/complex-sequence, however, it causes other tests to fail.

For now, I've implemented the alternative approach involving subclassing, see 366f7f1.

@tpow
Copy link
Author

tpow commented Feb 3, 2022

Thanks for working on this @phillbaker. I believe the subclassing approach may be all that is needed and is easy enough. Is it worth adding it to the readme/docs?

One point of clarification: I believe the description in 366f7f1 is inaccurate. This behavior was in the latest released version of suds-jurko which is why I reported it. I believe suds-community implemented the new behavior before any official release, but suds-jurko definitely had the old behavior in the released versions.

@phillbaker
Copy link
Member

Is it worth adding it to the readme/docs?

366f7f1 includes an update to the readme, https://github.com/suds-community/suds#initializing-optional-arrays-with-lists

This behavior was in the latest released version of suds-jurko

Hm, the last released version of suds-jurko is 0.6 (https://pypi.org/project/suds-jurko/#history), corresponding to this tag: https://github.com/suds-community/suds/releases/tag/release-0.6, released in 2014. Issue #14 identifies this commit as introducing the change: b6d1d09, from 2015. So it doesn't seem like it was in suds-jurko, but it was released in suds-community in v0.7.0 (the first suds-community release) and then the change in #15 was released in suds-community 0.8.0. So suds-community definitely changed.

Thanks for opening the issue and providing helpful feedback! I'm going to close this issue for now, if folks can make bugfix/complex-sequence work, would be happy to review a PR that implements that behavior as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants