Hi everyone, Here's a very rough cut at adding SOAP support to Zope. It is mostly a shameless adaptation of the already existing XML-RPC support and it probably has alot of problems, but it should provide a starting point. Don't expect too much out of it for now since: 1) it was written by someone (me :) with about two hours of previous SOAP experience and 2) it is 1:34 AM here and I wrote it in the last hour :) Having said that, I was able to call a method inside Zope and retrieve back the result, so I attach a diff against a current Zope CVS checkout. Feedback is more than welcome. -Petru
diff -urN --exclude-from=exclude _zope-untouched/lib/python/ZPublisher/HTTPRequest.py soap-zope/lib/python/ZPublisher/HTTPRequest.py --- _zope-untouched/lib/python/ZPublisher/HTTPRequest.py Sat Jun 10 02:57:58 2000 +++ soap-zope/lib/python/ZPublisher/HTTPRequest.py Thu Jun 22 01:23:28 2000 @@ -94,6 +94,7 @@ from Converters import get_converter from maybe_lock import allocate_lock xmlrpc=None # Placeholder for module that we'll import if we have to. +soap=None isCGI_NAME = { 'SERVER_SOFTWARE' : 1, @@ -347,13 +348,23 @@ if (fs.headers.has_key('content-type') and fs.headers['content-type'] == 'text/xml' and method == 'POST'): - # Ye haaa, XML-RPC! - global xmlrpc - if xmlrpc is None: import xmlrpc - meth, self.args = xmlrpc.parse_input(fs.value) - response=xmlrpc.response(response) - other['RESPONSE']=self.response=response - other['REQUEST_METHOD']='' # We don't want index_html! + if environ.has_key('HTTP_SOAPACTION'): + # this is a SOAP request + global soap + if soap is None: + import soap + meth, self.args = soap.parse_input(fs.value) + response = soap.response(response) + other['RESPONSE'] = self.response = response + other['REQUEST_METHOD'] = '' + else: + # Ye haaa, XML-RPC! + global xmlrpc + if xmlrpc is None: import xmlrpc + meth, self.args = xmlrpc.parse_input(fs.value) + response=xmlrpc.response(response) + other['RESPONSE']=self.response=response + other['REQUEST_METHOD']='' # We don't want index_html! else: self._file=fs.file else: diff -urN --exclude-from=exclude _zope-untouched/lib/python/ZPublisher/soap.py soap-zope/lib/python/ZPublisher/soap.py --- _zope-untouched/lib/python/ZPublisher/soap.py Thu Jan 1 02:00:00 1970 +++ soap-zope/lib/python/ZPublisher/soap.py Thu Jun 22 01:23:12 2000 @@ -0,0 +1,116 @@ +"""SOAP support module + +Written by Petru Paler + +Based on the XML-RPC Zope support module written by Eric Kidd at UserLand +software, with much help from Jim Fulton at DC. + +This code hooks Zope up to Fredrik Lundh's Python SOAP library. +""" + +import sys +from string import replace +from HTTPResponse import HTTPResponse +import soaplib + +def parse_input(data): + """Parse input data and return a method path and argument tuple + + The data is a string. + """ + method, params = soaplib.loads(data) + # Translate '.' to '/' in meth to represent object traversal. + method = replace(method, '.', '/') + return method, params + +# See below +# +# def response(anHTTPResponse): +# """Return a valid ZPublisher response object +# +# Use data already gathered by the existing response. +# The new response will replace the existing response. +# """ +# # As a first cut, lets just clone the response and +# # put all of the logic in our refined response class below. +# r=Response() +# r.__dict__.update(anHTTPResponse.__dict__) +# return r + + + +######################################################################## +# Possible implementation helpers: + +class Response: + """Customized Response that handles SOAP-specific details. + + We override setBody to marhsall Python objects into SOAP. We + also override exception to convert errors to SOAP faults. + + If these methods stop getting called, make sure that ZPublisher is + using the soap.Response object created above and not the original + HTTPResponse object from which it was cloned. + + It's probably possible to improve the 'exception' method quite a bit. + The current implementation, however, should suffice for now. + """ + + # Because we can't predict what kind of thing we're customizing, + # we have to use delegation, rather than inheritence to do the + # customization. + + def __init__(self, real): self.__dict__['_real']=real + + def __getattr__(self, name): return getattr(self._real, name) + def __setattr__(self, name, v): return setattr(self._real, name, v) + def __delattr__(self, name): return delattr(self._real, name) + + def setBody(self, body, title='', is_error=0, bogus_str_search=None): + if isinstance(body, soaplib.Fault): + # Convert Fault object to SOAP response. + body=soaplib.dumps(soaplib.Fault(1, body)) + else: + # Marshall our body as an SOAP response. Strings will be sent + # strings, integers as integers, etc. We do *not* convert + # everything to a string first. + try: + body = soaplib.dumps(soaplib.Response('foo', None, (body,)), 1) + except: + self.exception() + return + # Set our body to the XML-RPC message, and fix our MIME type. + self._real.setBody(body) + self._real.setHeader('content-type', 'text/xml') + return self + + def exception(self, fatal=0, info=None, + absuri_match=None, tag_search=None): + # Fetch our exception info. t is type, v is value and tb is the + # traceback object. + if type(info) is type(()) and len(info)==3: t,v,tb = info + else: t,v,tb = sys.exc_info() + + # Create an appropriate Fault object. Unfortunately, we throw away + # most of the debugging information. More useful error reporting is + # left as an exercise for the reader. + Fault=soaplib.Fault + f=None + try: + if isinstance(v, Fault): + f=v + elif isinstance(v, Exception): + f=Fault(-1, "Unexpected Zope exception: " + str(v)) + else: + f=Fault(-2, "Unexpected Zope error value: " + str(v)) + except: + f=Fault(-3, "Unknown Zope fault type") + + # Do the damage. + self.setBody(soaplib.dumps(f)) + self._real.setHeader('content-type', 'text/xml') + self._real.setStatus(200) + + return tb + +response=Response diff -urN --exclude-from=exclude _zope-untouched/lib/python/soaplib.py soap-zope/lib/python/soaplib.py --- _zope-untouched/lib/python/soaplib.py Thu Jan 1 02:00:00 1970 +++ soap-zope/lib/python/soaplib.py Thu Jun 22 00:21:33 2000 @@ -0,0 +1,828 @@ +# +# SOAP CLIENT LIBRARY +# $Id$ +# +# an SOAP 1.1 client interface for Python. +# +# the marshalling and response parser code can also be used to +# implement SOAP servers. +# +# Notes: +# this version uses the sgmlop XML parser, if installed. this is +# typically 10-15x faster than using Python's standard XML parser. +# +# you can get the sgmlop distribution from: +# +# http://www.pythonware.com/products/xml +# +# also note that this version is designed to work with Python 1.5.2 +# and newer. it does not work under 1.5.1 or earlier. +# +# Contact: +# [EMAIL PROTECTED] +# http://www.pythonware.com +# +# History: +# 2000-01-05 fl First experimental version (based on xmlrpclib.py) +# 2000-02-15 fl Updated to SOAP 1.0 +# 2000-04-30 fl Major overhaul, updated for SOAP 1.1 +# 2000-05-27 fl Fixed xmllib support +# 2000-06-20 fl Frontier interoperability testing +# +# Copyright (c) 1999-2000 by Secret Labs AB. +# Copyright (c) 1999-2000 by Fredrik Lundh. +# +# Portions of this engine have been developed in cooperation with +# Loudcloud, Inc. +# +# -------------------------------------------------------------------- +# The soaplib.py library is +# +# Copyright (c) 1999-2000 by Secret Labs AB +# Copyright (c) 1999-2000 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + +# FIXME: handle Fault messages correctly +# FIXME: handle homogenous arrays +# FIXME: handle more time types +# FIXME: add support for more character sets (under 1.6) +# FIXME: incorporate id/href patches (from John Lehmann) + +import string, time, re +import urllib, xmllib +from types import * +from cgi import escape + +try: + import sgmlop +except ImportError: + sgmlop = None # accelerator not available + +# how far we're from a real 1.0 :-) +__version__ = "0.8" + +# schema namespaces +NS_XSD = "http://www.w3.org/1999/XMLSchema/" +NS_XSI = "http://www.w3.org/1999/XMLSchema/instance/" + +# soap namespaces +NS_ENV = "http://schemas.xmlsoap.org/soap/envelope/" +NS_ENC = "http://schemas.xmlsoap.org/soap/encoding/" + +# experimental extensions +NS_LAB = "http://www.pythonware.com/soap/" + +# envelope tag. declare all namespaces used by the payload +ENVELOPE = ( + "<env:Envelope " + "xmlns:env='" + NS_ENV + "' " + "xmlns:enc='" + NS_ENC + "' " + "xmlns:lab='" + NS_LAB + "' " + "xmlns:xsd='" + NS_XSD + "' " + "xmlns:xsi='" + NS_XSI + "' " + "env:encodingStyle='" + NS_ENC + "'" + ">\n" + ) + +SOAPTAGS = NS_ENV + "Envelope", NS_ENV + "Body" + +# check if a string is valid XML tag +_is_valid_tag = re.compile("^[a-zA-Z_][-a-zA-Z0-9._]*$").match +# FIXME: this should reject strings that start with [Xx][Mm][Ll] + +# -------------------------------------------------------------------- +# Exceptions + +class Error: + # base class for client errors + pass + +class ProtocolError(Error): + # indicates an HTTP protocol error + def __init__(self, url, errcode, errmsg, headers): + self.url = url + self.errcode = errcode + self.errmsg = errmsg + self.headers = headers + def __repr__(self): + return ( + "<ProtocolError for %s: %s %s>" % + (self.url, self.errcode, repr(self.errmsg)) + ) + +class ResponseError(Error): + # indicates a broken response package + pass + +class Fault(Error): + # indicates a SOAP fault package + def __init__(self, faultcode, faultstring, detail=None): + self.faultcode = faultcode + self.faultstring = faultstring + self.detail = None + def encode(self, out): + write = out.write + write("<env:Fault>\n") + write("<faultcode>%s</faultcode>" % self.faultcode) + write("<faultstring>%s</faultstring>" % self.faultstring) + if self.detail: + out._dump("detail", self.detail) + write("</env:Fault>\n") + def __repr__(self): + return ( + "<Fault %s: %s>" % + (self.faultcode, repr(self.faultstring)) + ) + +class MethodCall: + # (experimental) represents a set of named parameters + def __init__(self, method, namespace, pargs=(), kwargs={}): + self.method = method + self.namespace = namespace + self.pargs = pargs + self.kwargs = kwargs + def encode(self, out): + if self.namespace: + method = "rpc:" + self.method + out.write("<%s xmlns:rpc=%s>\n" % (method, repr(self.namespace))) + else: + method = self.method + out.write("<%s>\n" % method) + i = 1 + for v in self.pargs: + out._dump("v.%d" % i, v) + i = i + 1 + for k, v in self.kwargs.items(): + out._dump(k, v) + out.write("</%s>\n" % method) + +class Response: + # (experimental) represents a response tuple + def __init__(self, method, namespace, value=()): + self.method = method + self.namespace = namespace + self.value = value + def encode(self, out): + method = self.method + "Response" + if self.namespace: + method = "rpc:" + method + out.write("<%s xmlns:rpc=%s>\n" % (method, repr(self.namespace))) + else: + out.write("<%s>\n" % method) + i = 1 + for v in self.value: + out._dump("v.%d" % i, v) + i = i + 1 + out.write("</%s>\n" % method) + + +# -------------------------------------------------------------------- +# Special values + +# boolean wrapper +# (you must use True or False to generate a "boolean" SOAP value) + +class Boolean: + + def __init__(self, value = 0): + self.value = not not value + + def encode(self, tag, out): + out.write( + "<%s xsi:type='xsd:boolean'>%d</%s>\n" % (tag, self.value, tag) + ) + + def __repr__(self): + if self.value: + return "<Boolean True at %x>" % id(self) + else: + return "<Boolean False at %x>" % id(self) + + def __int__(self): + return self.value + + def __nonzero__(self): + return self.value + +True, False = Boolean(1), Boolean(0) + +# +# timePeriod wrapper +# (wrap your iso8601 string or time tuple or localtime time value in +# this class to generate a "timePeriod" SOAP value) + +class DateTime: + + def __init__(self, value = 0): + t = type(value) + if t is not StringType: + if t is not TupleType: + value = time.localtime(value) + value = time.strftime("%Y%m%dT%H:%M:%S", value) + self.value = value + + def __repr__(self): + return "<DateTime %s at %x>" % (self.value, id(self)) + + def decode(self, data): + self.value = string.strip(data) + + def encode(self, tag, out): + out.write( + "<%s xsi:type='xsd:timePeriod'>%s</%s>\n", (tag, self.value, tag) + ) + +# +# binary data wrapper. you must use this for strings that are not +# valid XML strings (in other words, strings that cannot be stored +# as a plain XML element). + +class Binary: + + def __init__(self, data=None): + self.data = data + + def decode(self, data): + import base64 + self.data = base64.decodestring(data) + + def encode(self, tag, out): + import base64, StringIO + out.write("<%s xsi:type='enc:base64'>\n" % tag) + base64.encode(StringIO.StringIO(self.data), out) + out.write("</%s>\n" % tag) + +WRAPPERS = DateTime, Binary, Boolean + +# -------------------------------------------------------------------- +# XML parsers + +if sgmlop: + + class FastParser: + # sgmlop based XML parser. this is typically 15x faster + # than SlowParser... + + def __init__(self, target): + + # setup callbacks + self.finish_endtag = target.end + self.handle_data = target.data + + self.start = target.start + + self.namespace = {"xmlns": "xmlns"} + + # activate parser + self.parser = sgmlop.XMLParser() + self.parser.register(self) + self.feed = self.parser.feed + self.entity = { + "amp": "&", "gt": ">", "lt": "<", + "apos": "'", "quot": '"' + } + + def close(self): + try: + self.parser.close() + finally: + self.parser = self.feed = None # nuke circular reference + + def finish_starttag(self, tag, attrs): + # FIXME: this doesn't handle nested namespaces + items = attrs.items() + for k, v in items: + if k[:6] == "xmlns:": + self.namespace[k[6:]] = v + # fixup names + if ":" in tag: + ns, tag = string.split(tag, ":") + tag = self.namespace[ns] + tag + for k, v in items: + if ":" in k: + ns, k = string.split(k, ":") + k = self.namespace[ns] + k + attrs[k] = v + self.start(tag, attrs, self.namespace.get) + + def handle_entityref(self, entity): + # <string> entity + try: + self.handle_data(self.entity[entity]) + except KeyError: + self.handle_data("&%s;" % entity) + +else: + + FastParser = None + +class SlowParser(xmllib.XMLParser): + # slow but safe standard parser, based on the XML parser in + # Python's standard library + + def __init__(self, target): + self.__start = target.start + self.handle_data = target.data + self.unknown_endtag = target.end + xmllib.XMLParser.__init__(self) + + def __namespace(self, prefix): + # map namespace prefix to full namespace + for tag, namespace, tag in self.stack: + if namespace.has_key(prefix): + return namespace[prefix] + return None + + def unknown_starttag(self, tag, attrs): + # fixup tags and attribute names (ouch!) + tag = string.replace(tag, " ", "") + for k, v in attrs.items(): + k = string.replace(k, " ", "") + attrs[k] = v + self.__start(tag, attrs, self.__namespace) + +# -------------------------------------------------------------------- +# SOAP marshalling and unmarshalling code + +class Marshaller: + """Generate an SOAP body from a Python data structure""" + + # USAGE: create a marshaller instance for each set of parameters, + # and use "dumps" to convert your data (represented as a tuple) to + # a SOAP body chunk. to write a fault response, pass a Fault + # instance instead. you may prefer to use the "dumps" convenience + # function for this purpose (see below). + + # by the way, if you don't understand what's going on in here, + # that's perfectly ok. + + def __init__(self): + self.memo = {} + self.data = None + + dispatch = {} + + def dumps(self, value, envelope=0): + self.__out = [] + self.write = write = self.__out.append + if envelope: + write(ENVELOPE) + write("<env:Body>\n") + if (isinstance(value, MethodCall) or + isinstance(value, Response) or + isinstance(value, Fault)): + value.encode(self) + else: + for item in value: + self._dump("v", item) + write("</env:Body>\n") + if envelope: + write("</env:Envelope>\n") + result = string.join(self.__out, "") + del self.__out, self.write # don't need this any more + return result + + def _dump(self, tag, value): + try: + f = self.dispatch[type(value)] + except KeyError: + raise TypeError, "cannot marshal %s objects" % type(value) + else: + f(self, tag, value) + + def dump_none(self, tag, value): + self.write( + "<%s xsi:null='1'></%s>\n" % (tag, tag) + ) + dispatch[NoneType] = dump_none + + def dump_int(self, tag, value): + self.write( + "<%s xsi:type='xsd:int'>%s</%s>\n" % (tag, value, tag) + ) + dispatch[IntType] = dump_int + + def dump_long(self, tag, value): + self.write( + "<%s xsi:type='xsd:integer'>%s</%s>\n" % (tag, value, tag) + ) + + def dump_long_old(self, tag, value): + value = str(value)[:-1] # 1.5.2 and earlier + self.write( + "<%s xsi:type='xsd:integer'>%s</%s>\n" % (tag, value, tag) + ) + + if ("%s" % 1L) == "1": + dispatch[LongType] = dump_long + else: + dispatch[LongType] = dump_long_old + + def dump_double(self, tag, value): + self.write( + "<%s xsi:type='xsd:double'>%s</%s>\n" % (tag, value, tag) + ) + dispatch[FloatType] = dump_double + + def dump_string(self, tag, value): + self.write( + "<%s xsi:type='xsd:string'>%s</%s>\n" % (tag, escape(value), tag) + ) + dispatch[StringType] = dump_string + + def container(self, value): + if value: + i = id(value) + if self.memo.has_key(i): + raise TypeError, "cannot marshal recursive data structures" + self.memo[i] = None + + def dump_array(self, tag, value): + self.container(value) + write = self.write + write("<%s enc:arrayType='xsd:ur-type[%d]'>\n" % (tag, len(value))) + for v in value: + self._dump("v", v) + write("</%s>\n" % tag) + dispatch[TupleType] = dump_array + dispatch[ListType] = dump_array + + def dump_dict(self, tag, value): + self.container(value) + write = self.write + write("<%s xsi:type='lab:PythonDict'>\n" % tag) + for k, v in value.items(): + self._dump("k", k) + self._dump("v", v) + write("</%s>\n" % tag) + dispatch[DictType] = dump_dict + + def dump_instance(self, tag, value): + # check for special wrappers + if value.__class__ in WRAPPERS: + value.encode(tag, self) + else: + write = self.write + write("<%s>\n" % tag) + for k, v in vars(value).items(): + self._dump(k, v) + write("</%s>\n" % tag) + dispatch[InstanceType] = dump_instance + + +class Unmarshaller: + + # unmarshal an SOAP response, based on incoming XML events (start, + # data, end). call close to get the resulting data structure + + # note that this reader is fairly tolerant, and gladly accepts + # bogus SOAP data without complaining (but not bogus XML). + + # and again, if you don't understand what's going on in here, + # that's perfectly ok. + + def __init__(self): + self._stack = [] + self._marks = [] + self._data = [] + self.append = self._stack.append + + def close(self): + # return response code and the actual response + if self._marks or len(self._stack) != 1: + raise ResponseError() + # FIXME: should this really be done here? + method, params = self._stack[0] + args = [] + for k, v in params: + args.append(v) + return method, tuple(args) + + # + # event handlers + + def start(self, tag, attrs, fixup): + # handle start tag + type = attrs.get(NS_XSI + "type") + if type is None: + type = attrs.get(NS_ENC + "arrayType") + if type and ":" in type: + prefix, type = string.split(type, ":") + ns = fixup(prefix) + if ns is None: + raise SyntaxError,\ + "undefined namespace prefix %s" % repr(prefix) + type = ns + type + self._marks.append((tag, type, len(self._stack))) + self._data = [] + + def data(self, text): + self._data.append(text) + + dispatch = {} + + def end(self, tag): + # call the appropriate type handler + tag, type, mark = self._marks.pop() + if type is None and tag in SOAPTAGS: + pass + else: + try: + f = self.dispatch[type] + except KeyError: + self.end_unknown(type) + else: + self._mark = mark + self._stack.append((tag, f(self))) + + # + # element decoders + + def end_unknown(self, type, join=string.join): + print "***", type + raise SyntaxError, ("unknown type %s (for now)" % repr(type)) + + def end_boolean(self, join=string.join): + value = join(self._data, "") + if value in ("0", "false"): + return False + elif value in ("1", "true"): + return True + else: + raise TypeError, "bad boolean value" + dispatch[NS_XSD + "boolean"] = end_boolean + + def end_int(self, join=string.join): + return int(join(self._data, "")) + dispatch[NS_XSD + "int"] = end_int + dispatch[NS_XSD + "byte"] = end_int + dispatch[NS_XSD + "unsignedByte"] = end_int + dispatch[NS_XSD + "short"] = end_int + dispatch[NS_XSD + "unsignedShort"] = end_int + dispatch[NS_XSD + "long"] = end_int + + def end_long(self, join=string.join): + return long(join(self._data, "")) + dispatch[NS_XSD + "integer"] = end_long + dispatch[NS_XSD + "unsignedInt"] = end_long + dispatch[NS_XSD + "unsignedLong"] = end_long + + def end_double(self, join=string.join): + return float(join(self._data, "")) + dispatch[NS_XSD + "double"] = end_double + + def end_string(self, join=string.join): + return join(self._data, "") + dispatch[NS_XSD + "string"] = end_string + + def end_array(self): + # map arrays to Python lists + list = [] + data = self._stack[self._mark:] + del self._stack[self._mark:] + for tag, item in data: + list.append(item) + return list + dispatch[NS_XSD + "ur-type[]"] = end_array + + def end_dict(self): + dict = {} + data = self._stack[self._mark:] + del self._stack[self._mark:] + for i in range(0, len(data), 2): + dict[data[i][1]] = data[i+1][1] + return dict + dispatch[NS_LAB + "PythonDict"] = end_dict + + def end_struct(self): + # typeless elements are assumed to be structs. map + # them to a (name, value) list + data = self._stack[self._mark:] + del self._stack[self._mark:] + return data + dispatch[None] = end_struct + + def end_base64(self, join=string.join): + value = Binary() + value.decode(join(self._data, "")) + return value + dispatch[NS_ENC + "base64"] = end_base64 + + def end_dateTime(self, join=string.join): + value = DateTime() + value.decode(join(self._data, "")) + return value + dispatch[NS_XSD + "timePeriod"] = end_dateTime + dispatch[NS_XSD + "timeInstant"] = end_dateTime + +# -------------------------------------------------------------------- +# convenience functions + +def getparser(): + # get the fastest available parser, and attach it to an + # unmarshalling object. return both objects. + target = Unmarshaller() + if FastParser: + return FastParser(target), target + return SlowParser(target), target + +def dumps(params, envelope=0): + # convert a tuple or a fault object to an SOAP packet + + assert (type(params) == TupleType or + isinstance(params, MethodCall) or + isinstance(params, Response) or + isinstance(params, Fault) + ),\ + "argument must be tuple, method call/response, or fault instance" + + m = Marshaller() + + return m.dumps(params, envelope) + +def loads(data): + # convert an SOAP packet to data plus a method name (None + # if not present). if the SOAP packet represents a fault + # condition, this function raises a Fault exception. + p, u = getparser() + p.feed(data) + p.close() + return u.close() + + +# -------------------------------------------------------------------- +# request dispatcher + +class _Method: + # some magic to bind an SOAP method to an RPC server. + # supports "nested" methods (e.g. examples.getStateName) + def __init__(self, send, name): + self.__send = send + self.__name = name + def __getattr__(self, name): + return _Method(self.__send, "%s.%s" % (self.__name, name)) + def __call__(self, *pargs, **kwargs): + return self.__send(self.__name, pargs, kwargs) + +class Transport: + """Handles an HTTP transaction to an SOAP server""" + + # client identifier (may be overridden) + user_agent = "soaplib.py/%s (from www.pythonware.com)" % __version__ + + def request(self, host, handler, request_body): + # issue SOAP request + + import httplib + h = httplib.HTTP(host) + h.debuglevel = 0 + h.putrequest("POST", handler) + + # required by HTTP/1.1 + h.putheader("Host", host) + + if h.debuglevel > 0: + print "-- REQUEST --" + print request_body + + # required by SOAP + h.putheader("User-Agent", self.user_agent) + h.putheader("Content-Type", "text/xml") + h.putheader("Content-Length", str(len(request_body))) + h.putheader("SOAPAction", '""') + + h.endheaders() + + if request_body: + h.send(request_body) + + errcode, errmsg, headers = h.getreply() + + if h.debuglevel > 0: + print "-- RESPONSE --" + print errcode, errmsg, headers + + if errcode not in (200, 500): + raise ProtocolError( + host + handler, + errcode, errmsg, + headers + ) + + response = self.parse_response(h.getfile()) + + # should this really be done in here? + response = response[1] + if len(response) == 1: + response = response[0] + + return response + + def parse_response(self, f): + # read response from input file, and parse it + + p, u = getparser() + + while 1: + response = f.read(1024) + if not response: + break + p.feed(response) + + f.close() + p.close() + + return u.close() + + +class ServerProxy: + """Represents a connection to an SOAP server""" + + def __init__(self, uri, transport=None): + # establish a "logical" server connection + + # get the url + type, uri = urllib.splittype(uri) + if type != "http": + raise IOError, "unsupported SOAP protocol" + self.__host, self.__handler = urllib.splithost(uri) + if not self.__handler: + self.__handler = "/" + + if transport is None: + transport = Transport() + self.__transport = transport + + self.__methods = {} # known methods + + def _defmethod(self, methodname, namespace): + # associate information with a remote methodname + + self.__methods[methodname] = namespace + + def __request(self, methodname, pargs, kwargs): + # call a method on the remote server + + # wrap the arguments up + params = MethodCall( + methodname, self.__methods.get(methodname), pargs, kwargs + ) + + request = dumps(params, 1) + + response = self.__transport.request( + self.__host, + self.__handler, + request + ) + + return response + + def __repr__(self): + return ( + "<Server proxy for %s%s>" % + (self.__host, self.__handler) + ) + + __str__ = __repr__ + + def __getattr__(self, name): + # magic method dispatcher + return _Method(self.__request, name) + + def __getitem__(self, name): + # alternate method dispatcher + return _Method(self.__request, name) + + +# xmlrpclib compatibility +Server = ServerProxy + + +if __name__ == "__main__": + + # simple test, using soapserver.py + server = ServerProxy("http://localhost:8000") + print server.call("hello")