167 lines
5.9 KiB
Plaintext
167 lines
5.9 KiB
Plaintext
Implementing Web Services with Quixote
|
|
======================================
|
|
|
|
This document will show you how to implement Web services using
|
|
Quixote.
|
|
|
|
|
|
An XML-RPC Service
|
|
------------------
|
|
|
|
XML-RPC is the simplest protocol commonly used to expose a Web
|
|
service. In XML-RPC, there are a few basic data types such as
|
|
integers, floats, strings, and dates, and a few aggregate types such
|
|
as arrays and structs. The xmlrpclib module, part of the Python 2.2
|
|
standard library and available separately from
|
|
http://www.pythonware.com/products/xmlrpc/, converts between Python's
|
|
standard data types and the XML-RPC data types.
|
|
|
|
============== =====================
|
|
XML-RPC Type Python Type or Class
|
|
-------------- ---------------------
|
|
<int> int
|
|
<double> float
|
|
<string> string
|
|
<array> list
|
|
<struct> dict
|
|
<boolean> xmlrpclib.Boolean
|
|
<base64> xmlrpclib.Binary
|
|
<dateTime> xmlrpclib.DateTime
|
|
============== =====================
|
|
|
|
|
|
Making XML-RPC Calls
|
|
--------------------
|
|
|
|
Making an XML-RPC call using xmlrpclib is easy. An XML-RPC server
|
|
lives at a particular URL, so the first step is to create an
|
|
xmlrpclib.ServerProxy object pointing at that URL. ::
|
|
|
|
>>> import xmlrpclib
|
|
>>> s = xmlrpclib.ServerProxy(
|
|
'http://www.stuffeddog.com/speller/speller-rpc.cgi')
|
|
|
|
Now you can simply make a call to the spell-checking service offered
|
|
by this server::
|
|
|
|
>>> s.speller.spellCheck('my speling isnt gud', {})
|
|
[{'word': 'speling', 'suggestions': ['apeling', 'spelding',
|
|
'spelling', 'sperling', 'spewing', 'spiling'], 'location': 4},
|
|
{'word': 'isnt', 'suggestions': [``isn't'', 'ist'], 'location': 12}]
|
|
>>>
|
|
|
|
This call results in the following XML being sent::
|
|
|
|
<?xml version='1.0'?>
|
|
<methodCall>
|
|
<methodName>speller.spellCheck</methodName>
|
|
<params>
|
|
<param>
|
|
<value><string>my speling isnt gud</string></value>
|
|
</param>
|
|
<param>
|
|
<value><struct></struct></value>
|
|
</param>
|
|
</params>
|
|
</methodCall>
|
|
|
|
|
|
Writing a Quixote Service
|
|
-------------------------
|
|
|
|
In the quixote.util module, Quixote provides a function,
|
|
``xmlrpc(request, func)``, that processes the body of an XML-RPC
|
|
request. ``request`` is the HTTPRequest object that Quixote passes to
|
|
every function it invokes. ``func`` is a user-supplied function that
|
|
receives the name of the XML-RPC method being called and a tuple
|
|
containing the method's parameters. If there's a bug in the function
|
|
you supply and it raises an exception, the ``xmlrpc()`` function will
|
|
catch the exception and return a ``Fault`` to the remote caller.
|
|
|
|
Here's an example of implementing a simple XML-RPC handler with a
|
|
single method, ``get_time()``, that simply returns the current
|
|
time. The first task is to expose a URL for accessing the service. ::
|
|
|
|
from quixote.directory import Directory
|
|
from quixote.util import xmlrpc
|
|
from quixote import get_request
|
|
|
|
class RPCDirectory(Directory):
|
|
|
|
_q_exports = ['rpc']
|
|
|
|
def rpc (self):
|
|
return xmlrpc(get_request(), rpc_process)
|
|
|
|
def rpc_process (meth, params):
|
|
...
|
|
|
|
When the above code is placed in the __init__.py file for the Python
|
|
package corresponding to your Quixote application, it exposes the URL
|
|
``http://<hostname>/rpc`` as the access point for the XML-RPC service.
|
|
|
|
Next, we need to fill in the contents of the ``rpc_process()``
|
|
function::
|
|
|
|
import time
|
|
|
|
def rpc_process (meth, params):
|
|
if meth == 'get_time':
|
|
# params is ignored
|
|
now = time.gmtime(time.time())
|
|
return xmlrpclib.DateTime(now)
|
|
else:
|
|
raise RuntimeError, "Unknown XML-RPC method: %r" % meth
|
|
|
|
``rpc_process()`` receives the method name and the parameters, and its
|
|
job is to run the right code for the method, returning a result that
|
|
will be marshalled into XML-RPC. The body of ``rpc_process()`` will
|
|
therefore usually be an ``if`` statement that checks the name of the
|
|
method, and calls another function to do the actual work. In this case,
|
|
``get_time()`` is very simple so the two lines of code it requires are
|
|
simply included in the body of ``rpc_process()``.
|
|
|
|
If the method name doesn't belong to a supported method, execution
|
|
will fall through to the ``else`` clause, which will raise a
|
|
RuntimeError exception. Quixote's ``xmlrpc()`` will catch this
|
|
exception and report it to the caller as an XML-RPC fault, with the
|
|
error code set to 1.
|
|
|
|
As you add additional XML-RPC services, the ``if`` statement in
|
|
``rpc_process()`` will grow more branches. You might be tempted to pass
|
|
the method name to ``getattr()`` to select a method from a module or
|
|
class. That would work, too, and avoids having a continually growing
|
|
set of branches, but you should be careful with this and be sure that
|
|
there are no private methods that a remote caller could access. I
|
|
generally prefer to have the ``if... elif... elif... else`` blocks, for
|
|
three reasons: 1) adding another branch isn't much work, 2) it's
|
|
explicit about the supported method names, and 3) there won't be any
|
|
security holes in doing so.
|
|
|
|
An alternative approach is to have a dictionary mapping method names
|
|
to the corresponding functions and restrict the legal method names
|
|
to the keys of this dictionary::
|
|
|
|
def echo (*params):
|
|
# Just returns the parameters it's passed
|
|
return params
|
|
|
|
def get_time ():
|
|
now = time.gmtime(time.time())
|
|
return xmlrpclib.DateTime(now)
|
|
|
|
methods = {'echo' : echo,
|
|
'get_time' : get_time}
|
|
|
|
def rpc_process (meth, params):
|
|
func = methods.get[meth]
|
|
if methods.has_key(meth):
|
|
# params is ignored
|
|
now = time.gmtime(time.time())
|
|
return xmlrpclib.DateTime(now)
|
|
else:
|
|
raise RuntimeError, "Unknown XML-RPC method: %r" % meth
|
|
|
|
This approach works nicely when there are many methods and the
|
|
``if...elif...else`` statement would be unworkably long.
|