[Major Edit based on experience since 1st post two days ago.]
I am building a Python SOAP/XML script using Suds, but am struggling to get the code to generate SOAP/X
You can use a plugin to modify the XML before is sent to the server (my answer is based on Ronald Smith's solution):
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()])
Citing the documentation:
The MessagePlugin currently has (5) hooks ::
(...)
sending()
Provides the plugin with the opportunity to inspect/modify the message text before it is sent.
Basically Suds will call sending
before the XML is sent, so you can modify the generated XML (contained in context.envelope
). You have to pass the plugin class MyPlugin to the Client
constructor for this to work.
Edit
Another way is to use marshalled
to modify the XML structure, removing the empty elements (untested code):
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()
I know this one was closed a long time ago, but after working on the problem personally I find the current answers lacking.
Using the sending method on the MessagePlugin won't work, because despite what the documentation heavily implies, you cannot actually change the message string from there. You can only retrieve the final result.
The marshalled method, as previously mentioned, is best for this, as it does allow you to affect the XML. I created the following plugin to fix the issue for myself:
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()[:])
This will eliminate all empty tags. You can tailor this as needed if you only need to remove some empty tags from some place, but this recursive function works and (unless your XML schema is so unspeakably bad as to have nesting greater than the call depth of Python), shouldn't cause a problem. Note that we copy the lists here because using remove() mangles them as we're iterating and causes problems.
On an additional note, the regex that has been given by other answers is bad-- \s+<.*?/>
used on <test> <thingus/> </test>
will match <test> <thingus/>
, and not just <thingus/>
as you might expect. This is because >
is considered 'any character' by .
. If you really need to use regex to fix this problem on rendered XML (Note: XML is a complex syntax that is better handled by a lexer), the correct one would be <[^>]*/>
.
We use it here because I could not figure out the most correct way to ask the lexer 'is this a stand alone empty tag', other than to examine that tag's rendered output and regex against that. In such case I also added the ^
and $
tokens because rendering the tag in this method renders its entire context, and so that means that any blank tag underneath a particular tag would be matched. We just want the one particular tag to be matched so we can tell the API to remove it from the tree.
Finally, to help those searching for what might have prompted this question in the first place, the issue came up for me when I received an error message like this:
cvc-enumeration-valid: Value '' is not facet-valid with respect to enumeration
This is because the empty tag causes the server to interpret everything that would be under that tag as null values/empty strings.
There's an even easier way - no need for any Reg Ex or exciting iterators ;)
First, define the plugin:
class PrunePlugin(MessagePlugin):
def marshalled(self, context):
context.envelope = context.envelope.prune()
Then use it when creating the client:
client = Client(url, plugins=[PrunePlugin()])
The prune() method will remove any empty nodes, as documented here: http://jortel.fedorapeople.org/suds/doc/suds.sax.element.Element-class.html
The Suds factory method generates a regular Python object with regular python attributes that map to the WSDL type definition.
You can use the 'del' builtin function to remove attributes.
>>> 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
}
What do you think of the following MonkeyPatch to skip complex types with a None
value?
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
I thought I'd share a pretty simple update on the solution above that should work for any WSDL: Note that the sending method is not necessary - it's so that you can audit your changes, as the Client's debug request printing fires before the marshal method runs.
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)