# # XML-RPC CLIENT LIBRARY # $Id$ # # an XML-RPC client interface for Python. # # the marshalling and response parser code can also be used to # implement XML-RPC 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/madscientist # # also note that this version is designed to work with Python 1.5.1 # or newer. it doesn't use any 1.5.2-specific features. # # History: # 1999-01-14 fl Created # 1999-01-15 fl Changed dateTime to use localtime # 1999-01-16 fl Added Binary/base64 element, default to RPC2 service # 1999-01-19 fl Fixed array data element (from Skip Montanaro) # 1999-01-21 fl Fixed dateTime constructor, etc. # 1999-02-02 fl Added fault handling, handle empty sequences, etc. # 1999-02-10 fl Fixed problem with empty responses (from Skip Montanaro) # 1999-06-20 fl Speed improvements, pluggable XML parsers and HTTP transports # # Copyright (c) 1999 by Secret Labs AB. # Copyright (c) 1999 by Fredrik Lundh. # # fredrik@pythonware.com # http://www.pythonware.com # # -------------------------------------------------------------------- # The XML-RPC client interface is # # Copyright (c) 1999 by Secret Labs AB # Copyright (c) 1999 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. # -------------------------------------------------------------------- import string, time import urllib, xmllib from types import * from cgi import escape from base64 import encodestring try: import sgmlop except ImportError: sgmlop = None # accelerator not available __version__ = "0.9.8" # -------------------------------------------------------------------- # 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 ( "" % (self.url, self.errcode, self.errmsg) ) class ResponseError(Error): # indicates a broken response package pass class Fault(Error): # indicates a XML-RPC fault package def __init__(self, faultCode, faultString, **extra): self.faultCode = faultCode self.faultString = faultString def __repr__(self): return ( "" % (self.faultCode, repr(self.faultString)) ) # -------------------------------------------------------------------- # Special values # boolean wrapper # (you must use True or False to generate a "boolean" XML-RPC value) class Boolean: def __init__(self, value = 0): self.value = (value != 0) def encode(self, out): out.write("%d\n" % self.value) def __repr__(self): if self.value: return "" % id(self) else: return "" % id(self) def __int__(self): return self.value def __nonzero__(self): return self.value True, False = Boolean(1), Boolean(0) # # dateTime wrapper # (wrap your iso8601 string or time tuple or localtime time value in # this class to generate a "dateTime.iso8601" XML-RPC 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 "" % (self.value, id(self)) def decode(self, data): self.value = string.strip(data) def encode(self, out): out.write("") out.write(self.value) out.write("\n") # # binary data wrapper (NOTE: this is an extension to Userland's # XML-RPC protocol! only for use with compatible servers!) class Binary: def __init__(self, data=None): self.data = data def decode(self, data): import base64 self.data = base64.decodestring(data) def encode(self, out): import base64, StringIO out.write("\n") base64.encode(StringIO.StringIO(self.data), out) out.write("\n") 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_starttag = target.start self.finish_endtag = target.end self.handle_data = target.data # 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 = None # nuke circular reference def handle_entityref(self, entity): # 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.unknown_starttag = target.start self.handle_data = target.data self.unknown_endtag = target.end xmllib.XMLParser.__init__(self) # -------------------------------------------------------------------- # XML-RPC marshalling and unmarshalling code class Marshaller: """Generate an XML-RPC params chunk 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 XML-RPC params 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, values): self.__out = [] self.write = write = self.__out.append if isinstance(values, Fault): # fault instance write("\n") self.__dump(vars(values)) write("\n") else: # parameter block write("\n") for v in values: write("\n") self.__dump(v) write("\n") write("\n") result = string.join(self.__out, "") del self.__out, self.write # don't need this any more return result def __dump(self, value): try: f = self.dispatch[type(value)] except KeyError: raise TypeError, "cannot marshal %s objects" % type(value) else: f(self, value) def dump_int(self, value): self.write("%s\n" % value) dispatch[IntType] = dump_int def dump_double(self, value): self.write("%s\n" % value) dispatch[FloatType] = dump_double def dump_string(self, value): self.write("%s\n" % escape(value)) 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, value): self.container(value) write = self.write write("\n") for v in value: self.__dump(v) write("\n") dispatch[TupleType] = dump_array dispatch[ListType] = dump_array def dump_struct(self, value): self.container(value) write = self.write write("\n") for k, v in value.items(): write("\n") if type(k) is not StringType: raise TypeError, "dictionary key must be string" write("%s\n" % escape(k)) self.__dump(v) write("\n") write("\n") dispatch[DictType] = dump_struct def dump_instance(self, value): # check for special wrappers if value.__class__ in WRAPPERS: value.encode(self) else: # store instance attributes as a struct (really?) self.dump_struct(value.__dict__) dispatch[InstanceType] = dump_instance class Unmarshaller: # unmarshal an XML-RPC response, based on incoming XML event # messages (start, data, end). call close to get the resulting # data structure # note that this reader is fairly tolerant, and gladly accepts # bogus XML-RPC 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._type = None self._stack = [] self._marks = [] self._data = [] self._methodname = None self.append = self._stack.append def close(self): # return response code and the actual response if self._type is None or self._marks: raise ResponseError() if self._type == "fault": raise apply(Fault, (), self._stack[0]) return tuple(self._stack) def getmethodname(self): return self._methodname # # event handlers def start(self, tag, attrs): # prepare to handle this element if tag in ("array", "struct"): self._marks.append(len(self._stack)) self._data = [] self._value = (tag == "value") def data(self, text): self._data.append(text) dispatch = {} def end(self, tag): # call the appropriate end tag handler try: f = self.dispatch[tag] except KeyError: pass # unknown tag ? else: return f(self) # # element decoders def end_boolean(self, join=string.join): value = join(self._data, "") if value == "0": self.append(False) elif value == "1": self.append(True) else: raise TypeError, "bad boolean value" self._value = 0 dispatch["boolean"] = end_boolean def end_int(self, join=string.join): self.append(int(join(self._data, ""))) self._value = 0 dispatch["i4"] = end_int dispatch["int"] = end_int def end_double(self, join=string.join): self.append(float(join(self._data, ""))) self._value = 0 dispatch["double"] = end_double def end_string(self, join=string.join): self.append(join(self._data, "")) self._value = 0 dispatch["string"] = end_string dispatch["name"] = end_string # struct keys are always strings def end_array(self): mark = self._marks[-1] del self._marks[-1] # map arrays to Python lists self._stack[mark:] = [self._stack[mark:]] self._value = 0 dispatch["array"] = end_array def end_struct(self): mark = self._marks[-1] del self._marks[-1] # map structs to Python dictionaries dict = {} items = self._stack[mark:] for i in range(0, len(items), 2): dict[items[i]] = items[i+1] self._stack[mark:] = [dict] self._value = 0 dispatch["struct"] = end_struct def end_base64(self, join=string.join): value = Binary() value.decode(join(self._data, "")) self.append(value) self._value = 0 dispatch["base64"] = end_base64 def end_dateTime(self, join=string.join): value = DateTime() value.decode(join(self._data, "")) self.append(value) dispatch["dateTime.iso8601"] = end_dateTime def end_value(self): # if we stumble upon an value element with no internal # elements, treat it as a string element if self._value: self.end_string() dispatch["value"] = end_value def end_params(self): self._type = "params" dispatch["params"] = end_params def end_fault(self): self._type = "fault" dispatch["fault"] = end_fault def end_methodName(self, join=string.join): self._methodname = join(self._data, "") dispatch["methodName"] = end_methodName # -------------------------------------------------------------------- # 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, methodname=None, methodresponse=None): # convert a tuple or a fault object to an XML-RPC packet assert type(params) == TupleType or isinstance(params, Fault),\ "argument must be tuple or Fault instance" m = Marshaller() data = m.dumps(params) # standard XML-RPC wrappings if methodname: # a method call data = ( "\n" "\n" "%s\n" "%s\n" "\n" % (methodname, data) ) elif methodresponse or isinstance(params, Fault): # a method response data = ( "\n" "\n" "%s\n" "\n" % data ) return data def loads(data): # convert an XML-RPC packet to data plus a method name (None # if not present). if the XML-RPC packet represents a fault # condition, this function raises a Fault exception. p, u = getparser() p.feed(data) p.close() return u.close(), u.getmethodname() # -------------------------------------------------------------------- # request dispatcher class _Method: # some magic to bind an XML-RPC 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, *args): return self.__send(self.__name, args) class Transport: """Handles an HTTP transaction to an XML-RPC server""" # client identifier (may be overridden) user_agent = "xmlrpclib.py/%s (by www.pythonware.com)" % __version__ def request(self, host, handler, request_body): # issue XML-RPC request import httplib h = httplib.HTTP(host) h.putrequest("POST", handler) # required by HTTP/1.1 h.putheader("Host", host) # required by XML-RPC h.putheader("User-Agent", self.user_agent) h.putheader("Content-Type", "text/xml") h.putheader("Content-Length", str(len(request_body))) h.endheaders() if request_body: h.send(request_body) errcode, errmsg, headers = h.getreply() if errcode != 200: raise ProtocolError( host + handler, errcode, errmsg, headers ) return self.parse_response(h.getfile()) 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 BasicAuthTransport(Transport): """ Transport subclass that provides HTTP basic authentication. """ def __init__(self, username=None, password=None): self.username=username self.password=password def request(self, host, handler, request_body): # issue XML-RPC request import httplib h = httplib.HTTP(host) h.putrequest("POST", handler) # required by HTTP/1.1 h.putheader("Host", host) # required by XML-RPC h.putheader("User-Agent", self.user_agent) h.putheader("Content-Type", "text/xml") h.putheader("Content-Length", str(len(request_body))) # basic auth if self.username is not None and self.password is not None: h.putheader("AUTHORIZATION", "Basic %s" % string.replace( encodestring("%s:%s" % (self.username, self.password)), "\012", "")) h.endheaders() if request_body: h.send(request_body) errcode, errmsg, headers = h.getreply() if errcode != 200: raise xmlrpclib.ProtocolError( host + handler, errcode, errmsg, headers ) return self.parse_response(h.getfile()) class Server: """Represents a connection to an XML-RPC 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 XML-RPC protocol" self.__host, self.__handler = urllib.splithost(uri) if not self.__handler: self.__handler = "/RPC2" if transport is None: transport = Transport() self.__transport = transport def __request(self, methodname, params): # call a method on the remote server request = dumps(params, methodname) response = self.__transport.request( self.__host, self.__handler, request ) if len(response) == 1: return response[0] return response def __repr__(self): return ( "" % (self.__host, self.__handler) ) __str__ = __repr__ def __getattr__(self, name): # magic method dispatcher return _Method(self.__request, name) if __name__ == "__main__": # simple test program (from the XML-RPC specification) # server = Server("http://localhost:8000") # local server server = Server("http://betty.userland.com") print server try: print server.examples.getStateName(41) except Error, v: print "ERROR", v