suds-community / suds

Suds is a lightweight SOAP python client for consuming Web Services. A community fork of the jurko fork.
https://suds.readthedocs.io/
GNU Lesser General Public License v3.0
169 stars 53 forks source link

Missing array compared to suds-jurko #67

Closed tpow closed 2 years ago

tpow commented 2 years ago

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 commented 2 years ago

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: https://github.com/BingAds/BingAds-Python-SDK/commit/8d1a51595f9f4776d2b719be7f3fc1378cee4f4f#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:

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



What do you think?
tpow commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

Is it worth adding it to the readme/docs?

https://github.com/suds-community/suds/commit/366f7f1616595b9e4163a3f90fc6e84ac0ae23f5 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: https://github.com/suds-community/suds/commit/b6d1d0979e1847a6b8f267be1888892e9327c2a6, 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.