Suds产生空元素;如何删除它们?

时间:2012-02-22 02:04:04

标签: python soap suds

[主要编辑基于两天前第一篇文章以来的经验。]

我正在使用Suds构建Python SOAP / XML脚本,但我很难获得生成服务器可接受的SOAP / XML的代码。我原以为问题是Suds没有为内部元素生成前缀,但后来发现缺少前缀(参见Sh-Data和内部元素)不是问题,因为Sh-DataMetaSwitchData元素声明适当的命名空间(见下文)。

<SOAP-ENV:Envelope xmlns:ns3="http://www.metaswitch.com/ems/soap/sh" xmlns:ns0="http://www.metaswitch.com/ems/soap/sh/userdata" xmlns:ns1="http://www.metaswitch.com/ems/soap/sh/servicedata" xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <ns2:Body>
      <ns3:ShUpdate>
         <ns3:UserIdentity>Meribel/TD Test Sub Gateway 3</ns3:UserIdentity>
         <ns3:DataReference>0</ns3:DataReference>
         <ns3:UserData>
            <Sh-Data xmlns="http://www.metaswitch.com/ems/soap/sh/userdata">
               <RepositoryData>
                  <ServiceIndication>Meta_SubG_BaseInformation</ServiceIndication>
                  <SequenceNumber>0</SequenceNumber>
                  <ServiceData>
                     <MetaSwitchData xmlns="http://www.metaswitch.com/ems/soap/sh/servicedata" IgnoreSequenceNumber="False" MetaSwitchVersion="?">
                        <Meta_SubG_BaseInformation Action="apply">
                           <NetworkElementName>Meribel</NetworkElementName>
                           <Description>TD Test Sub Gateway 3</Description>
                           <DomainName>test.datcon.co.uk</DomainName>
                           <MediaGatewayModel>Cisco ATA</MediaGatewayModel>
                           <CallFeatureServerControlStatus/>
                           <CallAgentControlStatus/>
                           <UseStaticNATMapping/>
                           <AuthenticationRequired/>
                           <ProviderStatus/>
                           <DeactivationMode/>
                        </Meta_SubG_BaseInformation>
                     </MetaSwitchData>
                  </ServiceData>
               </RepositoryData>
            </Sh-Data>
         </ns3:UserData>
         <ns3:OriginHost>user@domain.com?clientVersion=7.3</ns3:OriginHost>
      </ns3:ShUpdate>
   </ns2:Body>
</SOAP-ENV:Envelope>

但这仍然失败。问题是Suds为可选元素生成空元素(在WSDL中标记为Mandatory = No)。但是服务器要求可选元素存在具有合理值或不存在,并且我得到以下错误(因为<CallFeatureServerControlStatus/>元素不是允许值之一。

  

提供的用户数据未针对用户数据验证MetaSwitch XML Schema   详细信息:cvc-enumeration-valid:对于枚举'[Controlling,Abandoned,Cautiously controlling]',值''不是facet-valid。它必须是枚举中的值。

如果我将生成的SOAP / XML带入SOAPUI并删除空元素,请求就可以了。

有没有办法让Suds不为可选字段生成空元素,或者让我之后在代码中删除它们?

重大更新

我已经解决了这个问题(我在其他地方已经看到过),但是这种方式非常不优雅。所以我发布我当前的解决方案,希望a)它可以帮助他人和/或b)有人可以建议更好的解决方案。

事实证明,问题不在于Suds为可选元素生成空元素(在WSDL中标记为Mandatory = No)。而是Suds为可选的复杂元素生成空元素。例如,以下Meta_SubG_BaseInformation元素是简单元素,而Suds不会在SOAP / XML中为它们生成任何内容。

<xs:element name="CMTS" type="xs:string" minOccurs="0">
    <xs:annotation>
        <xs:documentation>
            <d:DisplayName firstVersion="5.0" lastVersion="7.4">CMTS</d:DisplayName>
            <d:ValidFrom>5.0</d:ValidFrom>
            <d:ValidTo>7.4</d:ValidTo>
            <d:Type firstVersion="5.0" lastVersion="7.4">String</d:Type>
            <d:BaseAccess firstVersion="5.0" lastVersion="7.4">RWRWRW</d:BaseAccess>
            <d:Mandatory firstVersion="5.0" lastVersion="7.4">No</d:Mandatory>
            <d:MaxLength firstVersion="5.0" lastVersion="7.4">1024</d:MaxLength>
        </xs:documentation>
    </xs:annotation>
</xs:element>

<xs:element name="TAGLocation" type="xs:string" minOccurs="0">
    <xs:annotation>
        <xs:documentation>
            <d:DisplayName>Preferred location of Trunk Gateway</d:DisplayName>
            <d:Type>String</d:Type>
            <d:BaseAccess>RWRWRW</d:BaseAccess>
            <d:Mandatory>No</d:Mandatory>
            <d:DefaultValue>None</d:DefaultValue>
            <d:MaxLength>1024</d:MaxLength>
        </xs:documentation>
    </xs:annotation>
</xs:element>

相比之下,下面的Meta_SubG_BaseInformation元素是一个复杂的元素,即使它是可选的,我的代码也没有给它赋值,它最终会生成生成的SOAP / XML。

<xs:element name="ProviderStatus" type="tMeta_SubG_BaseInformation_ProviderStatus" minOccurs="0">
    <xs:annotation>
        <xs:documentation>
            <d:DisplayName>Provider status</d:DisplayName>
            <d:Type>Choice of values</d:Type>
            <d:BaseAccess>R-R-R-</d:BaseAccess>
            <d:Mandatory>No</d:Mandatory>
            <d:Values>
                <d:Value>Unavailable</d:Value>
                <d:Value>Available</d:Value>
                <d:Value>Inactive</d:Value>
                <d:Value>Active</d:Value>
                <d:Value>Out of service</d:Value>
                <d:Value>Quiescing</d:Value>
                <d:Value>Unconfigured</d:Value>
                <d:Value>Pending available</d:Value>
            </d:Values>
        </xs:documentation>
    </xs:annotation>
</xs:element>

Suds为ProviderStatus生成以下内容(如上所述)会扰乱我的服务器。

<ProviderStatus/>

解决方法是在创建父元素之后,在分配值之前将所有Meta_SubG_BaseInformation元素设置为None,如下所示。这对于简单元素来说是多余的,但确实确保未分配的复杂元素不会导致生成SOAP / XML。

subGatewayBaseInformation = client.factory.create('ns1:Meta_SubG_BaseInformation')
for (el) in subGatewayBaseInformation:
  subGatewayBaseInformation.__setitem__(el[0], None)
subGatewayBaseInformation._Action            = 'apply'
subGatewayBaseInformation.NetworkElementName = 'Meribel'
etc...

这导致Suds生成没有空元素的SOAP / XML,这是我的服务器可以接受的。

但有人知道一种更清洁的方法来达到同样的效果吗?

以下解决方案基于dusan和Roland Smith的回答/评论。

此解决方案使用Suds MessagePlugin修剪<SubscriberType/>形式的“空”XML,然后Suds将请求发送到线路上。我们只需要修剪ShUpdates(我们正在更新服务器上的数据),并且逻辑(尤其是索引到子节点以获取服务指示元素列表)是非常特定于WSDL的。它不适用于不同的WSDL。

class MyPlugin(MessagePlugin):
  def marshalled(self, context):
    pruned = []
    req = context.envelope.children[1].children[0]
    if (req.name == 'ShUpdate'):
      si = req.children[2].children[0].children[0].children[2].children[0].children[0]
      for el in si.children:
        if re.match('<[a-zA-Z0-9]*/>', Element.plain(el)):
          pruned.append(el)
      for p in pruned:
        si.children.remove(p)

然后我们只需要在创建客户端时引用该插件。

client = Client(url, plugins=[MyPlugin()])

7 个答案:

答案 0 :(得分:15)

您可以使用插件在发送到服务器之前修改XML(我的答案基于Ronald Smith的解决方案):

from suds.plugin import MessagePlugin
from suds.client import Client
import re

class MyPlugin(MessagePlugin):
    def sending(self, context):
        context.envelope = re.sub('\s+<.*?/>', '', context.envelope)


client = Client(URL_WSDL, plugins=[MyPlugin()])

引用documentation

  

MessagePlugin目前有(5)hooks ::
  (...)
  的发送()
  为插件提供在发送之前检查/修改消息文本的机会。

基本上,在发送XML之前,Suds会调用sending,因此您可以修改生成的XML(包含在context.envelope中)。您必须将插件类MyPlugin传递给Client构造函数才能使其正常工作。

修改

另一种方法是使用marshalled修改XML结构,删除空元素(未经测试的代码):

class MyPlugin(MessagePlugin):
    def marshalled(self, context):
        #remove empty tags inside the Body element
        #context.envelope[0] is the SOAP-ENV:Header element
        context.envelope[1].prune()

答案 1 :(得分:6)

有一种更简单的方法 - 不需要任何Reg Ex或激动人心的迭代器;)

首先,定义插件:

class PrunePlugin(MessagePlugin):
    def marshalled(self, context):
        context.envelope = context.envelope.prune()

然后在创建客户端时使用它:

client = Client(url, plugins=[PrunePlugin()])

prune()方法将删除所有空节点,如下所示:http://jortel.fedorapeople.org/suds/doc/suds.sax.element.Element-class.html

答案 2 :(得分:4)

Suds工厂方法生成一个常规Python对象,该对象具有映射到WSDL类型定义的常规python属性。

您可以使用&#39; del&#39;内置函数来删除属性。

>>> order_details = c.factory.create('ns2:OrderDetails')
>>> order_details
(OrderDetails){
   Amount = None
   CurrencyCode = None
   OrderChannelType =
      (OrderChannelType){
         value = None
      }
   OrderDeliveryType =
      (OrderDeliveryType){
         value = None
      }
   OrderLines =
      (ArrayOfOrderLine){
         OrderLine[] = <empty>
      }
   OrderNo = None
   TotalOrderValue = None
 }
>>> del order_details.OrderLines
>>> del order_details.OrderDeliveryType
>>> del order_details.OrderChannelType
>>> order_details
(OrderDetails){
   Amount = None
   CurrencyCode = None
   OrderNo = None
   TotalOrderValue = None
 }

答案 3 :(得分:2)

您可以使用正则表达式过滤掉空元素。

假设您的XML数据位于字符串xmltext;

import re
filteredtext = re.sub('\s+<.*?/>', '', xmltext)

答案 4 :(得分:1)

您如何看待以下MonkeyPatch跳过None值的复杂类型?

from suds.mx.literal import Typed
old_skip = Typed.skip
def new_skip(self, content):
    x = old_skip(self, content)
    if not x and getattr(content.value, 'value', False) is None:
        x = True
    return x
Typed.skip = new_skip

答案 5 :(得分:1)

我知道这个很久以前已经关闭了,但在亲自处理这个问题后,我发现目前的答案缺乏。

在MessagePlugin上使用发送方法将不起作用,因为尽管文档强调了这一点,但实际上您无法从那里更改消息字符串。您只能检索最终结果。

如前所述,编组方法最适用于此,因为它允许您影响XML。我创建了以下插件来解决自己的问题:

class ClearEmpty(MessagePlugin):
    def clear_empty_tags(self, tags):
        for tag in tags:
            children = tag.getChildren()[:]
            if children:
                self.clear_empty_tags(children)
            if re.match(r'^<[^>]+?/>$', tag.plain()):
                tag.parent.remove(tag)

    def marshalled(self, context):
        self.clear_empty_tags(context.envelope.getChildren()[:])

这将消除所有空标签。如果你只需要从某个地方删除一些空标签,你可以根据需要定制它,但这个递归函数是有效的(除非你的XML模式如此难以理解,以至于嵌套大于Python的调用深度),不应该导致问题。请注意,我们在这里复制列表,因为使用remove()会在我们迭代时对它们进行修改并导致问题。

另外请注意,其他答案给出的正则表达式不好 - \s+<.*?/>上使用的<test> <thingus/> </test>将匹配<test> <thingus/>,而不只是<thingus/>你可能会期待。这是因为>.视为“任何字符”。如果你真的需要使用正则表达式来解决渲染的XML上的这个问题(注意:XML是一个复杂的语法,由词法分析器更好地处理),正确的语法将是<[^>]*/>

我们在这里使用它是因为我无法找出最正确的方式来询问词法分析器'这是一个独立的空标记',而不是检查标记的渲染输出和正则表达式。在这种情况下,我还添加了^$令牌,因为在此方法中呈现标记会呈现其整个上下文,因此这意味着特定标记下方的任何空白标记都将匹配。我们只希望匹配一个特定标记,以便我们告诉API将其从树中删除。

最后,为了帮助那些搜索可能首先提示这个问题的人,当我收到这样的错误消息时,问题出现了:

cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration

这是因为空标记会导致服务器将该标记下的所有内容解释为空值/空字符串。

答案 6 :(得分:0)

我以为我会在上面的解决方案上分享一个非常简单的更新,它应该适用于任何WSDL: 请注意,发送方法不是必需的 - 因此您可以审核更改,因为客户端的调试请求打印在编组方法运行之前触发

class XMLBS_Plugin(MessagePlugin):
def marshalled(self, context):
    def w(x):
        if x.isempty():
            print "EMPTY: ", x
            x.detach()

    context.envelope.walk(w)

def sending(self,context):
    c = copy.deepcopy(context.envelope)
    c=c.replace('><','>\n<') # some sort of readability
    logging.info("SENDING: \n%s"%c)