#
# 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")
