[Zope-dev] alpha SOAP support

Petru Paler ppetru@coltronix.com
Thu, 22 Jun 2000 01:39:08 +0300


--jI8keyz6grp/JLjh
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

       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

--jI8keyz6grp/JLjh
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="soap.diff"

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:
+# fredrik@pythonware.com
+# 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")

--jI8keyz6grp/JLjh--