Producing and Consuming XML-RPC in Python¶
Walk through the following exercise to get an idea of how XML-RPC services can be generated and consumed from within a Python ecosystem.
A Simple Function-Based Service¶
Begin by creating a file, xmlrpc_server.py
.
Into it, type the following code:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from SimpleXMLRPCServer import SimpleXMLRPCServer
address = ('localhost', 50000)
server = SimpleXMLRPCServer(address)
def multiply(a, b):
"""return the product of two numbers"""
return a * b
server.register_function(multiply)
if __name__ == '__main__':
try:
print "Server running on %s:%s" % address
print "Use Ctrl-C to Exit"
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
print "Exiting"
The call to server.register_function tells the server that we want this
multiply
function to be available to the network.
We can run a client from a terminal. First, open one terminal and run the xmlrpc_server.py script:
$ python xmlrpc_server.py
Then, open another terminal and start up python:
>>> import xmlrpclib
>>> proxy = xmlrpclib.ServerProxy('http://localhost:50000', verbose=True)
>>> proxy.multiply(3, 24)
...
72
>>>
By passing the optional verbose
argument to our ServerProxy, we have
indicated that we want to see the traffic that goes back and forth.
If you format the HTTP request you sent to the service, it looks like this:
POST /RPC2 HTTP/1.0
Host: localhost:50000
User-Agent: xmlrpclib.py/1.0.1 (by www.pythonware.com)
Content-Type: text/xml
Content-Length: 192
<?xml version='1.0'?>
<methodCall>
<methodName>multiply</methodName>
<params>
<param>
<value><int>3</int></value>
</param>
<param>
<value><int>24</int></value>
</param>
</params>
</methodCall>
And the response that was returned by the service is also a simple HTTP response with an XML body:
HTTP/1.0 200 OK
Server: BaseHTTP/0.3 Python/2.6.1
Date: Sun, 13 Jan 2013 03:38:00 GMT
Content-type: text/xml
Content-length: 121
<?xml version='1.0'?>
<methodResponse>
<params>
<param>
<value><int>72</int></value>
</param>
</params>
</methodResponse>
Class-Based Service¶
It is also possible to register methods defined on a Python class as a service.
In your xmlrpc_server.py
file, add the following class definition:
class SimpleMathService(object):
"""provide simple mathematical operations"""
def __init__(self):
self.usage = {}
def divide(self, a, b):
"""return the quotient of two numeric values"""
self.usage.setdefault('divide', 0)
self.usage['divide'] += 1
return a / b
def get_usage(self):
"""show usage statistics"""
stats = ""
for op, ct in self.usage:
stats += "%s: used %d times\n"
if not stats:
stats = "No operations have yet been used"
return stats
def _private_method(self):
"""this method is private because it starts with '_'
"""
return "This method is not callable on the published service"
Then, comment out the earlier multiply
function and the line that registers
it with the server:
# def multiply(a, b):
# return a * b
# server.register_function(multiply)
Finally, register the new class with the server instead:
server.register_instance(SimpleMathService())
Back in your server terminal, quit the server and then restart it.
Then, try calling multiply
again:
>>> proxy.multiply(3, 24)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1224, in __call__
return self.__send(self.__name, args)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1578, in __request
verbose=self.__verbose
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1264, in request
return self.single_request(host, handler, request_body, verbose)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1297, in single_request
return self.parse_response(response)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1473, in parse_response
return u.close()
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 793, in close
raise Fault(\*\*self._stack[0])
xmlrpclib.Fault: <Fault 1: '<type \'exceptions.Exception\'>:method "multiply" is not supported'>
>>>
Note that the client has raised an error, but the server is still happily running.
Now, call the new method we defined:
>>> proxy.divide(24, 3)
send: "POST /RPC2 HTTP/1.1\r\nHost: localhost:50000\r\nAccept-Encoding: gzip\r\nUser-Agent: xmlrpclib.py/1.0.1 (by www.pythonware.com)\r\nContent-Type: text/xml\r\nContent-Length: 191\r\n\r\n<?xml version='1.0'?>\n<methodCall>\n<methodName>divide</methodName>\n<params>\n<param>\n<value><int>24</int></value>\n</param>\n<param>\n<value><int>3</int></value>\n</param>\n</params>\n</methodCall>\n"
reply: 'HTTP/1.0 200 OK\r\n'
header: Server: BaseHTTP/0.3 Python/2.7.5
header: Date: Sun, 23 Feb 2014 00:28:29 GMT
header: Content-type: text/xml
header: Content-length: 121
body: "<?xml version='1.0'?>\n<methodResponse>\n<params>\n<param>\n<value><int>8</int></value>\n</param>\n</params>\n</methodResponse>\n"
8
>>>
Try to call the method we created as private:
>>> proxy._private_method()
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1224, in __call__
return self.__send(self.__name, args)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1578, in __request
verbose=self.__verbose
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1264, in request
return self.single_request(host, handler, request_body, verbose)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1297, in single_request
return self.parse_response(response)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 1473, in parse_response
return u.close()
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/xmlrpclib.py", line 793, in close
raise Fault(\*\*self._stack[0])
xmlrpclib.Fault: <Fault 1: '<type \'exceptions.Exception\'>:method "_private_method" is not supported'>
Again, note that the client raises the error, the server responded just fine and continues to run.
Finally, try calling the other method we defined, to show usage statistics. What does it give us?
>>> proxy.get_usage()
...
{'divide': 1}
>>>
Try setting up another proxy in a third terminal. Call both methods a few times from each proxy and notice how the usage statistics increment. What does this tell you about a class-based service?
Service Introspection¶
XML-RPC supports introspection of a service, allowing you to get information about the service and how it functions.
The introspection methods that are available for a Python XML-RPC server are
listMethods
, methodHelp
, and methodSignature
.
To add support for a given introspection method to your class-based service,
you have to implement a corresponding private method on the class. Add the
following two methods to your SimpleMathService
class in
xmlrpc_server.py
:
def _listMethods(self):
"""custom logic for presenting method names to users
list_public_methods is a convenience function from the Python
library, but you can make your own logic if you wish.
"""
return list_public_methods(self)
def _methodHelp(self, method):
"""provide help text for an individual method
"""
f = getattr(self, method)
return f.__doc__
In addition, make sure to import the list_public_methods
method from the
SimpleXMLRPCServer
module at the top of the server script:
from SimpleXMLRPCServer import list_public_methods
Finally, just after creating the server, set it up to provide the public
introspection functions (still in xmlrpc_server.py
):
address = ('localhost', 50000)
server = SimpleXMLRPCServer(address)
server.register_introspection_functions() # this line is new
Now, restart your server and kick the tires. The introspection functions are
available as attributes of the system
attribute of your proxy:
>>> proxy.system.listMethods()
...
['divide', 'get_usage', 'system.listMethods', 'system.methodHelp', 'system.methodSignature']
>>> proxy.system.methodHelp('divide')
...
'return the quotient of two numeric values'
>>> proxy.system.methodSignature('divide')
...
'signatures not supported'
>>>
Warning
It is included here for completeness, and you should try it, but I’ve been
unable to get system.methodSignature
to work on OS X.