home *** CD-ROM | disk | FTP | other *** search
- #
- # 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)
- #
- # 2001-02-20 fixed the bugs in the XML namespaces and also added __repr__
- # method to the Binary class.
- # Houman Gharmi <houmangh@activestate.com>
- #
-
- 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-hacked"
-
- # 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)
-
- def __repr__(self):
- return self.data
-
- def __len__(self):
- return len(self.data)
-
- __str__ = __repr__
-
- 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 str(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 ppmhttplib
- h = ppmhttplib.PPMHTTP(host)
- h.debuglevel = 0
- h.set_debuglevel(0)
- h.postrequest("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)))
- # Removed by ActiveState -- incompatible with Perl SOAP server
- # XXX we should investigate the real problem...the SOAPAction
- # XXX header is required in SOAP 1.1
- # 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")
-
-
-
-
-
-
-
-
-
-
-
-
-
-