It’s a Twister – Now with More XML

Thursday, October 15th, 2009 1:26 am

Holy community code, Batman. Facebook has been launching open source projects like crazy in the past few months. (This would be a Good Thing — I think they launched two today just while I refreshed the page!) Things like Cassandra, Thrift, and Hive are great additions to the community, and hopefully the rewards will go both ways. One of the projects I’ve been playing with is Tornado, FriendFeed’s Python web framework. It’s quite simple to dive into if you are familiar with any of the dozens of web frameworks available for Python (like web.py, Pylons, CherryPy, etc.), and it’s very fast. (But, in the immortal words of LeVar Burton, “don’t take my word for it.“)


So, of course, the first thing I said is that we need to hack on some additional libraries and functionality to slow that sucker down. :) Actually, I use XML-RPC exhaustively in my projects, and it was only fitting that I decided to try tacking on simple (read: not the full specification) XML-RPC to Tornado, just to get my feet wet. Turns out, it’s really simple. Unlike some frameworks, you always have access to the entire request body, so you can parse it to your heart’s content. (Just the thing for a lonely Saturday night, eh?)

Below is my “quick” implementation, free for perusal / critique / a gripping read. Make sure you have the Tornado library installed — it’s really quite simple (unlike some Facebook open source projects), just download the tar and run (Mac / Linux):

tar xvzf tornado-0.2.tar.gz
cd tornado-0.2
sudo python setup.py install

Other than that, my code shouldn’t require any libraries that won’t come with a default Python install. (Tornado’s site does recommend simplejson and PyCurl for full functionality.) Keep in mind, this is a script you found on the internet. It shouldn’t delete your files or leave your oven on, but let’s just say it’s a bad choice for a production environment. :) Also, this has been rigorously tested — ran it once on Linux — so enjoy those bugs.

import tornado.httpserver
import tornado.ioloop
import tornado.web

def private(func):
    # Decorator to make a method, well, private.
    class PrivateMethod(object):
        def __init__(self):
            self.private = True
        __call__ = func
    return PrivateMethod()

class XMLRPCHandler(tornado.web.RequestHandler):
    """
    Subclass this to add methods -- you can treat them
    just like normal methods, this handles the XML formatting.
    """

    def post(self):
        """
        Later we'll make this compatible with "dot" calls like:
        server.namespace.method()
        If you implement this, make sure you do something proper
        with the Exceptions, i.e. follow the XMLRPC spec.
        """
        import xmlrpclib
        try:
            params, method_name = xmlrpclib.loads(self.request.body)
        except:
            # Bad request formatting, bad.
            raise Exception('Deal with how you want.')
        if method_name in dir(tornado.web.RequestHandler):
            # Pre-existing, not an implemented attribute
            raise AttributeError('%s is not implemented.' % method_name)
        try:
            method = getattr(self, method_name)
        except:
            # Attribute doesn't exist
            raise AttributeError('%s is not a valid method.' % method_name)
        if not callable(method):
            # Not callable, so not a method
            raise Exception('Attribute %s is not a method.' % method_name)
        if method_name.startswith('_') or
                ('private' in dir(method) and method.private is True):
            # No, no. That's private.
            raise Exception('Private function %s called.' % method_name)
        response = method(*params)
        response_xml = xmlrpclib.dumps((response,), methodresponse=True)
        self.set_header("Content-Type", "text/xml")
        self.write(response_xml)

class TestXMLRPC(XMLRPCHandler):
    def add(self, x, y):
        return x+y

    def ping(self, x):
        return x

    def _private(self):
        # Shouldn't be called
        return False

    @private
    def private(self):
        # Also shouldn't be called
        return False

if __name__ == "__main__":

    application = tornado.web.Application([
        (r"/", TestXMLRPC),
        (r"/RPC2", TestXMLRPC)
    ])

    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8080)
    tornado.ioloop.IOLoop.instance().start()

To test it, just save the contents as a file (like hello.py), start it up with python, and test it with the following in a Python console:

import xmlrpclib
hello = xmlrpclib.Server('http://localhost:8080/')
hello.add(5, 6)

You should get 11, unless you are passing in exceptionally large values of 5. :) I’ll be updating this script as I go along, making it more robust, more to spec, etc. Random Exceptions, believe it or not, are not the way to fail an XML-RPC connection. Feel free to blast at me with the utter failings of this script and the ineptitude of even starting on this path (that’s why they built Thrift, right?), or, you know, just comment friendly-like.

Update: All these source code offereings might have something to do with that ridiculous patent infringement lawsuit announcement where Facebook was ordered to “show their code” because some dude(s) managed to fool the U.S. Patent Office into giving him a patent that covered, you know, sharing information through the Interweb thingy. This would be a perfect time to rant against the insanity of software patents, but I’ll save that for another day. (Likely tomorrow.)

Leave a Reply