Using ElementTrees to Generate XML-RPC Messages
July 11, 2002 | Fredrik Lundh
This is a work in progress.The XML-RPC protocol provides a simple way to call remote services across a network. The protocol is based on well-known and widely used standards like HTTP and XML, and it simply adds a standard way to format requests and response packages, and to convert parameter values to XML and back again.
To call a remote service, create an XML document describing your request according to the instructions in the XML-RPC specification, and use HTTP to post it to the server. If everything goes well, the server will return an XML document containing the response.
Here’s a simple request example (right out of the XML-RPC specification):
<?xml version="1.0"?> <methodCall> <methodName>examples.getStateName</methodName> <params> <param> <value><i4>41</i4></value> </param> </params> </methodCall>
The request consists of a toplevel methodCall element, which contains two subelements, methodName and params.
As the name implies, the methodName element contains the name of the remote procedure to invoke. In this case, the name is examples.getStateName, which can be interpreted as a getStateName method provided by the examples service.
The params element, finally, contains parameters to pass to the procedure. It should contain a number of param subelements, each containing a value. The params element is optional; if the procedure takes no arguments, you can leave it out.
The following script uses the httplib module to send this request to a userland.com server:
host = "betty.userland.com" handler = "/RPC2" body = """\ <?xml version="1.0"?> <methodCall> <methodName>examples.getStateName</methodName> <params> <param> <value><i4>41</i4></value> </param> </params> </methodCall> """ def do_request(host, handler, body): from httplib import HTTP # send xml-rpc request h = HTTP(host) h.putrequest("POST", handler) h.putheader("User-Agent", "element-xmlrpc") h.putheader("Host", host) h.putheader("Content-Type", "text/xml") h.putheader("Content-Length", str(len(body))) h.endheaders() h.send(body) # fetch the reply errcode, errmsg, headers = h.getreply() if errcode != 200: raise Exception(errcode, errmsg) return h.getfile() print do_request(host, handler, body).read()
Assuming that betty.userland.com is up and running, the above script produces the following output (or something very similar):
<?xml version="1.0"?> <methodResponse> <params> <param> <value>South Dakota</value> </param> </params> </methodResponse>
The response is similar in structure. The toplevel element tells us that this is methodResponse, and the return values are stored in a params element.
Parsing the Response
Instead of printing the response, we can parse the it into an ElementTree, and use standard element methods to access the response contents:
file = do_request(host, handler, body) import ElementTree tree = ElementTree.parse(file) methodResponse = tree.getroot() for param in methodResponse.find("params"): print repr(param[0].text)
If you run this script, it’ll print ‘South Dakota’ (unless someone’s moved things around, of course).
Encoding Parameters and Return Values
Both parameters and return values are always stored in value elements, using a subelement to specify both the type (in the tag field) and the value (as text). For strings, you can leave out the subelement and store the string value in the value element itself.
XML-RPC supports the following type elements:
- i4 or int
-
A 32-bit signed integer.
- boolean
-
A boolean value, or flag: 0 for false, 1 for true.
- string
-
An string of XML characters (also see XML-RPC and the ASCII Limitation).
- double
-
A double-precision (64-bit) signed floating point number.
- dateTime.iso8601
-
Date/time given as a 17-character ISO 8601 string: “yyyymmddThh:mm:dd”. Note that the value is a given as a “naive time”; it does not include a time zone.
- base64
-
Binary data, stored as base64-encoded text
- array
-
An ordered collection of values (similar to a Python list). The array element should have a subelement named data, which can contain any number of value subelements. Arrays can be nested.
- struct
-
An unordered collection of string/value pairs (similar to a Python dictionary). The struct element can contain any number of member subelements, each containing a name element with the key string, and a value element containing the value. Structs can be nested.
To be continued…
Notes:
The http_xml module contains code to send and receive element trees over HTTP.
The following piece of code decodes a value argument, recursively:
from base64 import decodestring unmarshallers = { "int": lambda x: int(x.text), "i4": lambda x: int(x.text), "boolean": lambda x: bool(int(x.text)), "string": lambda x: x.text or "", "double": lambda x: float(x.text), "dateTime.iso8601": lambda x: datetime(x.text), "array": lambda x: [unmarshal(v) for v in x[0]], "struct": lambda x: dict([(k.text or "", unmarshal(v)) for k, v in x]), "base64": lambda x: decodestring(x.text or "") } def unmarshal(elem): if elem: value = elem[0] return unmarshallers[value.tag](value) return elem.text or ""
Here’s an alternative version of the struct line, which works also under Python 2.1:
"struct": lambda x: ([d for d in [{}]], [d.setdefault(k.text or "", unmarshal(v)) for (k, v) in x], d)[2],
Hmm. Maybe I should start with a slightly more readable version, using separate arrayfixup and structfixup functions…
Here’s a snippet that builds an XML-RPC request, with parameters given as a sequence:
def marshal_method_call(method_name, params): method_call_elem = Element("methodCall") SubElement(method_call_elem, "methodName").text = method_name if params: params_elem = SubElement(method_call_elem, "params") for value in params: elem = SubElement(params_elem, "param") elem.append(marshal(value)) return method_call_elem
And here’s a snippet that builds an XML-RPC response. Note that a method can only return a single value; to return multiple values, put them in a tuple.
def marshal_method_response(params): method_response_elem = Element("methodResponse") param_elem = SubElement( SubElement(method_response_elem, "params"), "param" ) param_elem.append(marshal(params)) return method_response_elem
Here’s a stub version of marshal. This version only supports a few data types:
marshallers = { type(0): lambda x, v: (SubElement(x, "i4"), str(v)), type(0.0): lambda x, v: (SubElement(x, "double"), str(v)), type(""): lambda x, v: (SubElement(x, "string"), v) } def marshal(value): value_elem = Element("value") elem, elem.text = marshallers[type(value)](value_elem, value) return value_elem