diff options
author | Fredrik Lundh <fredrik@pythonware.com> | 2001-07-11 17:42:21 (GMT) |
---|---|---|
committer | Fredrik Lundh <fredrik@pythonware.com> | 2001-07-11 17:42:21 (GMT) |
commit | b905633be016fabfee2290cb34d4a9c9fb5fe517 (patch) | |
tree | 54fb10889b22d2dda8c9c20bd96fd4350f985caf /Lib | |
parent | c2c12dc31cfbe1babe3e8f7f6a4f5bc709e684da (diff) | |
download | cpython-b905633be016fabfee2290cb34d4a9c9fb5fe517.zip cpython-b905633be016fabfee2290cb34d4a9c9fb5fe517.tar.gz cpython-b905633be016fabfee2290cb34d4a9c9fb5fe517.tar.bz2 |
xmlrpclib for python 2.2; initial checkin
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/xmlrpclib.py | 934 |
1 files changed, 934 insertions, 0 deletions
diff --git a/Lib/xmlrpclib.py b/Lib/xmlrpclib.py new file mode 100644 index 0000000..51a974b --- /dev/null +++ b/Lib/xmlrpclib.py @@ -0,0 +1,934 @@ +# +# 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 is designed to work with Python 1.5.2 or newer. +# unicode encoding support requires at least Python 1.6. +# experimental HTTPS requires Python 2.0 built with SSL sockets. +# expat parser support requires Python 2.0 with pyexpat support. +# +# 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 parsers/transports (0.9.8) +# 2000-11-28 fl Changed boolean to check the truth value of its argument +# 2001-02-24 fl Added encoding/Unicode/SafeTransport patches +# 2001-02-26 fl Added compare support to wrappers (0.9.9/1.0b1) +# 2001-03-28 fl Make sure response tuple is a singleton +# 2001-03-29 fl Don't require empty params element (from Nicholas Riley) +# 2001-06-10 fl Folded in _xmlrpclib accelerator support +# +# Copyright (c) 1999-2001 by Secret Labs AB. +# Copyright (c) 1999-2001 by Fredrik Lundh. +# +# info@pythonware.com +# http://www.pythonware.com +# +# -------------------------------------------------------------------- +# The XML-RPC client interface is +# +# Copyright (c) 1999-2001 by Secret Labs AB +# Copyright (c) 1999-2001 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. +# -------------------------------------------------------------------- + +# +# things to look into before 1.0 final: + +# TODO: unicode marshalling -DONE +# TODO: ascii-compatible encoding support -DONE +# TODO: safe transport -DONE (but mostly untested) +# TODO: sgmlop memory leak -DONE +# TODO: sgmlop xml parsing -DONE +# TODO: support unicode method names -DONE +# TODO: update selftest -DONE +# TODO: add docstrings -DONE +# TODO: clean up parser encoding (trust the parser) -DONE +# TODO: expat support -DONE +# TODO: _xmlrpclib accelerator support -DONE +# TODO: use smarter/faster escape from effdom +# TODO: support basic authentication (see robin's patch) +# TODO: fix host tuple handling in the server constructor +# TODO: let transport verify schemes +# TODO: update documentation +# TODO: authentication plugins +# TODO: memo problem (see HP's mail) + +import re, string, time, operator +import urllib, xmllib +from types import * +from cgi import escape + +try: + unicode +except NameError: + unicode = None # unicode support not available + +def _decode(data, encoding, is8bit=re.compile("[\x80-\xff]").search): + # decode non-ascii string (if possible) + if unicode and encoding and is8bit(data): + data = unicode(data, encoding) + return data + +if unicode: + def _stringify(string): + # convert to 7-bit ascii if possible + try: + return str(string) + except UnicodeError: + return string +else: + def _stringify(string): + return string + +__version__ = "1.0b2" + +# -------------------------------------------------------------------- +# 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, 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 ( + "<Fault %s: %s>" % + (self.faultCode, repr(self.faultString)) + ) + +# -------------------------------------------------------------------- +# Special values + +# boolean wrapper +# use True or False to generate a "boolean" XML-RPC value + +class Boolean: + + def __init__(self, value = 0): + self.value = operator.truth(value) + + def encode(self, out): + out.write("<value><boolean>%d</boolean></value>\n" % self.value) + + def __cmp__(self, other): + if isinstance(other, Boolean): + other = other.value + return cmp(self.value, other) + + 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) + +def boolean(value, truefalse=(False, True)): + # convert any Python value to XML-RPC boolean + return truefalse[operator.truth(value)] + +# +# dateTime wrapper +# wrap your iso8601 string or time tuple or localtime integer value +# in this class to generate a "dateTime.iso8601" XML-RPC value + +class DateTime: + + def __init__(self, value=0): + t = type(value) + if not isinstance(t, StringType): + if not isinstance(t, TupleType): + if value == 0: + value = time.time() + value = time.localtime(value) + value = time.strftime("%Y%m%dT%H:%M:%S", value) + self.value = value + + def __cmp__(self, other): + if isinstance(other, DateTime): + other = other.value + return cmp(self.value, other) + + def __repr__(self): + return "<DateTime %s at %x>" % (self.value, id(self)) + + def decode(self, data): + self.value = string.strip(data) + + def encode(self, out): + out.write("<value><dateTime.iso8601>") + out.write(self.value) + out.write("</dateTime.iso8601></value>\n") + +def datetime(data): + value = DateTime() + value.decode(data) + return value + +# +# binary data wrapper + +class Binary: + + def __init__(self, data=None): + self.data = data + + def __cmp__(self, other): + if isinstance(other, Binary): + other = other.data + return cmp(self.data, other) + + def decode(self, data): + import base64 + self.data = base64.decodestring(data) + + def encode(self, out): + import base64, StringIO + out.write("<value><base64>\n") + base64.encode(StringIO.StringIO(self.data), out) + out.write("</base64></value>\n") + +def binary(data): + value = Binary() + value.decode(data) + return value + +WRAPPERS = DateTime, Binary, Boolean + +# -------------------------------------------------------------------- +# XML parsers + +try: + # optional xmlrpclib accelerator. for more information on this + # component, contact info@pythonware.com + import _xmlrpclib + FastParser = _xmlrpclib.Parser + FastUnmarshaller = _xmlrpclib.Unmarshaller +except (AttributeError, ImportError): + FastParser = FastUnmarshaller = None + +# +# the SGMLOP parser is about 15x faster than Python's builtin +# XML parser. SGMLOP sources can be downloaded from: +# +# http://www.pythonware.com/products/xml/sgmlop.htm +# + +try: + import sgmlop + if not hasattr(sgmlop, "XMLParser"): + raise ImportError +except ImportError: + SgmlopParser = None # sgmlop accelerator not available +else: + class SgmlopParser: + def __init__(self, target): + + # setup callbacks + self.finish_starttag = target.start + self.finish_endtag = target.end + self.handle_data = target.data + self.handle_xml = target.xml + + # 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 handle_proc(self, tag, attr): + m = re.search("encoding\s*=\s*['\"]([^\"']+)[\"']", attr) + if m: + self.handle_xml(m.group(1), 1) + + def handle_entityref(self, entity): + # <string> entity + try: + self.handle_data(self.entity[entity]) + except KeyError: + self.handle_data("&%s;" % entity) + +try: + from xml.parsers import expat +except ImportError: + ExpatParser = None +else: + class ExpatParser: + # fast expat parser for Python 2.0. this is about 50% + # slower than sgmlop, on roundtrip testing + def __init__(self, target): + self._parser = parser = expat.ParserCreate(None, None) + self._target = target + parser.StartElementHandler = target.start + parser.EndElementHandler = target.end + parser.CharacterDataHandler = target.data + encoding = None + if not parser.returns_unicode: + encoding = "utf-8" + target.xml(encoding, None) + + def feed(self, data): + self._parser.Parse(data, 0) + + def close(self): + self._parser.Parse("", 1) # end of data + del self._target, self._parser # get rid of circular references + +class SlowParser(xmllib.XMLParser): + # slow but safe standard parser, based on the XML parser in + # Python's standard library. this is about 10 times slower + # than sgmlop, on roundtrip testing. + def __init__(self, target): + self.handle_xml = target.xml + 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, encoding=None): + self.memo = {} + self.data = None + self.encoding = encoding + + dispatch = {} + + def dumps(self, values): + self.__out = [] + self.write = write = self.__out.append + if isinstance(values, Fault): + # fault instance + write("<fault>\n") + self.__dump(vars(values)) + write("</fault>\n") + else: + # parameter block + write("<params>\n") + for v in values: + write("<param>\n") + self.__dump(v) + write("</param>\n") + write("</params>\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("<value><int>%s</int></value>\n" % value) + dispatch[IntType] = dump_int + + def dump_double(self, value): + self.write("<value><double>%s</double></value>\n" % value) + dispatch[FloatType] = dump_double + + def dump_string(self, value): + self.write("<value><string>%s</string></value>\n" % escape(value)) + dispatch[StringType] = dump_string + + if unicode: + def dump_unicode(self, value): + value = value.encode(self.encoding) + self.write("<value><string>%s</string></value>\n" % escape(value)) + dispatch[UnicodeType] = dump_unicode + + 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("<value><array><data>\n") + for v in value: + self.__dump(v) + write("</data></array></value>\n") + dispatch[TupleType] = dump_array + dispatch[ListType] = dump_array + + def dump_struct(self, value): + self.container(value) + write = self.write + write("<value><struct>\n") + for k, v in value.items(): + write("<member>\n") + if type(k) is not StringType: + raise TypeError, "dictionary key must be string" + write("<name>%s</name>\n" % escape(k)) + self.__dump(v) + write("</member>\n") + write("</struct></value>\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._encoding = "utf-8" + self.append = self._stack.append + + def close(self): + # return response tuple and target method + 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 xml(self, encoding, standalone): + self._encoding = encoding + # FIXME: assert standalone == 1 ??? + + def start(self, tag, attrs): + # prepare to handle this element + if tag == "array" or tag == "struct": + self._marks.append(len(self._stack)) + self._data = [] + self._value = (tag == "value") + + def data(self, text): + self._data.append(text) + + def end(self, tag): + # call the appropriate end tag handler + try: + f = self.dispatch[tag] + except KeyError: + pass # unknown tag ? + else: + return f(self, self._data) + + # + # accelerator support + + def end_dispatch(self, tag, data): + # dispatch data + try: + f = self.dispatch[tag] + except KeyError: + pass # unknown tag ? + else: + return f(self, data) + + # + # element decoders + + dispatch = {} + + def end_boolean(self, data, join=string.join): + data = join(data, "") + if data == "0": + self.append(False) + elif data == "1": + self.append(True) + else: + raise TypeError, "bad boolean value" + self._value = 0 + dispatch["boolean"] = end_boolean + + def end_int(self, data, join=string.join): + self.append(int(join(data, ""))) + self._value = 0 + dispatch["i4"] = end_int + dispatch["int"] = end_int + + def end_double(self, data, join=string.join): + self.append(float(join(data, ""))) + self._value = 0 + dispatch["double"] = end_double + + def end_string(self, data, join=string.join): + data = join(data, "") + if self._encoding: + data = _decode(data, self._encoding) + self.append(_stringify(data)) + self._value = 0 + dispatch["string"] = end_string + dispatch["name"] = end_string # struct keys are always strings + + def end_array(self, data): + 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, data): + 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[_stringify(items[i])] = items[i+1] + self._stack[mark:] = [dict] + self._value = 0 + dispatch["struct"] = end_struct + + def end_base64(self, data, join=string.join): + value = Binary() + value.decode(join(data, "")) + self.append(value) + self._value = 0 + dispatch["base64"] = end_base64 + + def end_dateTime(self, data, join=string.join): + value = DateTime() + value.decode(join(data, "")) + self.append(value) + dispatch["dateTime.iso8601"] = end_dateTime + + def end_value(self, data): + # if we stumble upon an value element with no internal + # elements, treat it as a string element + if self._value: + self.end_string(data) + dispatch["value"] = end_value + + def end_params(self, data): + self._type = "params" + dispatch["params"] = end_params + + def end_fault(self, data): + self._type = "fault" + dispatch["fault"] = end_fault + + def end_methodName(self, data, join=string.join): + data = join(data, "") + if self._encoding: + data = _decode(data, self._encoding) + self._methodname = data + self._type = "methodName" # no params + dispatch["methodName"] = end_methodName + + +# -------------------------------------------------------------------- +# convenience functions + +def getparser(): + """getparser() -> parser, unmarshaller + + Create an instance of the fastest available parser, and attach + it to an unmarshalling object. Return both objects. + """ + if FastParser and FastUnmarshaller: + target = FastUnmarshaller(True, False, binary, datetime) + parser = FastParser(target) + else: + target = Unmarshaller() + if FastParser: + parser = FastParser(target) + elif SgmlopParser: + parser = SgmlopParser(target) + elif ExpatParser: + parser = ExpatParser(target) + else: + parser = SlowParser(target) + return parser, target + +def dumps(params, methodname=None, methodresponse=None, encoding=None): + """data [,options] -> marshalled data + + Convert an argument tuple or a Fault instance to an XML-RPC + request (or response, if the methodresponse option is used). + + In addition to the data object, the following options can be + given as keyword arguments: + + methodname: the method name for a methodCall packet + + methodresponse: true to create a methodResponse packet. + If this option is used with a tuple, the tuple must be + a singleton (i.e. it can contain only one element). + + encoding: the packet encoding (default is UTF-8) + + All 8-bit strings in the data structure are assumed to use the + packet encoding. Unicode strings are automatically converted, + as necessary. + """ + + assert isinstance(params, TupleType) or isinstance(params, Fault),\ + "argument must be tuple or Fault instance" + + if isinstance(params, Fault): + methodresponse = 1 + elif methodresponse and isinstance(params, TupleType): + assert len(params) == 1, "response tuple must be a singleton" + + if not encoding: + encoding = "utf-8" + + m = Marshaller(encoding) + data = m.dumps(params) + + if encoding != "utf-8": + xmlheader = "<?xml version='1.0' encoding=%s?>\n" % repr(encoding) + else: + xmlheader = "<?xml version='1.0'?>\n" # utf-8 is default + + # standard XML-RPC wrappings + if methodname: + # a method call + if not isinstance(methodname, StringType): + methodname = methodname.encode(encoding) + data = ( + xmlheader, + "<methodCall>\n" + "<methodName>", methodname, "</methodName>\n", + data, + "</methodCall>\n" + ) + elif methodresponse: + # a method response, or a fault structure + data = ( + xmlheader, + "<methodResponse>\n", + data, + "</methodResponse>\n" + ) + else: + return data # return as is + return string.join(data, "") + +def loads(data): + """data -> unmarshalled data, method name + + Convert an XML-RPC packet to unmarshalled 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, verbose=0): + # issue XML-RPC request + + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_content(h, request_body) + + errcode, errmsg, headers = h.getreply() + + if errcode != 200: + raise ProtocolError( + host + handler, + errcode, errmsg, + headers + ) + + self.verbose = verbose + + return self.parse_response(h.getfile()) + + def make_connection(self, host): + # create a HTTP connection object from a host descriptor + import httplib + return httplib.HTTP(host) + + def send_request(self, connection, handler, request_body): + connection.putrequest("POST", handler) + + def send_host(self, connection, host): + connection.putheader("Host", host) + + def send_user_agent(self, connection): + connection.putheader("User-Agent", self.user_agent) + + def send_content(self, connection, request_body): + connection.putheader("Content-Type", "text/xml") + connection.putheader("Content-Length", str(len(request_body))) + connection.endheaders() + if request_body: + connection.send(request_body) + + 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 + if self.verbose: + print "body:", repr(response) + p.feed(response) + + f.close() + p.close() + + return u.close() + +class SafeTransport(Transport): + """Handles an HTTPS transaction to an XML-RPC server""" + + def make_connection(self, host): + # create a HTTPS connection object from a host descriptor + # host may be a string, or a (host, x509-dict) tuple + import httplib + if isinstance(host, TupleType): + host, x509 = host + else: + x509 = {} + try: + HTTPS = httplib.HTTPS + except AttributeError: + raise NotImplementedError,\ + "your version of httplib doesn't support HTTPS" + else: + return apply(HTTPS, (host, None), x509) + + def send_host(self, connection, host): + if isinstance(host, TupleType): + host, x509 = host + connection.putheader("Host", host) + +class ServerProxy: + """uri [,options] -> a logical connection to an XML-RPC server + + uri is the connection point on the server, given as + scheme://host/target. + + The standard implementation always supports the "http" scheme. If + SSL socket support is available (Python 2.0), it also supports + "https". + + If the target part and the slash preceding it are both omitted, + "/RPC2" is assumed. + + The following options can be given as keyword arguments: + + transport: a transport factory + encoding: the request encoding (default is UTF-8) + + All 8-bit strings passed to the server proxy are assumed to use + the given encoding. + """ + + def __init__(self, uri, transport=None, encoding=None, verbose=0): + # establish a "logical" server connection + + # get the url + type, uri = urllib.splittype(uri) + if type not in ("http", "https"): + raise IOError, "unsupported XML-RPC protocol" + self.__host, self.__handler = urllib.splithost(uri) + if not self.__handler: + self.__handler = "/RPC2" + + if transport is None: + if type == "https": + transport = SafeTransport() + else: + transport = Transport() + self.__transport = transport + + self.__encoding = encoding + self.__verbose = verbose + + def __request(self, methodname, params): + # call a method on the remote server + + request = dumps(params, methodname, encoding=self.__encoding) + + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + + if len(response) == 1: + response = response[0] + + 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) + + # note: to call a remote object with an non-standard name, use + # result getattr(server, "strange-python-name")(args) + +Server = ServerProxy + +# -------------------------------------------------------------------- +# test code + +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 |