Implementing Web Services
=========================

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
>>> server = 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.util import xmlrpc

    _q_exports = ['rpc']

    def rpc (request):
        return xmlrpc(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.


$Id: web-services.txt,v 1.1 2002/05/16 14:39:51 akuchlin Exp $
