pyRest part 3: Routing and responsibilities

In part 2, I hooked up the API to CherryPy in a very crude fashion, and this time we'll look at how we can add handlers for resources in a less clumsy way. I decided to keep handlers on one 'level' only - that is, /sketch/parrot and sketch will both be handled by the /sketch handler. This is because I find that the same sub-resource often is present in several places (what about //props/parrot?) and having handlers like this simplifies stuff and makes the magic more readable.

That magic looks like this - it is passed a package, find all modules that has at least one of get/post/put/delete implemented, and stores them in a name->module dict.


    def `get`_handlers(package):
        handlers = {}
        for `member`_name, `member` in
            [module for module in inspect.getmembers(package)
                    if inspect.ismodule(module[1])]:
            if [fn for name, fn in inspect.getmembers(member)
                   if name in ('get', 'post', 'put', 'delete')]:
                print("Adding handler %s" % `member`_name)
                handlers[`member`_name]  = `member`
        return handlers

Later, when we get a request, we interpret the first part of the path as resource name (although I mounted it at /api, so it becomed /api/), and then use that string to get the correct module, check for a handler for the specific method, and call it if it exists.


    def requesthandler(handlers, method, resource, *pathargs, **kwargs):
        """Main dispatch for calls to PyRest; no framework specific
        code to be present after this point"""

        if not resource in handlers:
            return Response('404 Not Found', 'No such resource')

        if not  hasattr(handlers[resource], method):
            return Response('405 Method Not Allowed',
                            'Unsupported method for resource')

        `return`_data = getattr(handlers[resource],
                              method)(*pathargs, **kwargs)
        `return` Response('200 OK', json.dumps(`return`_data))

Right now, there's nothing exciting going on in the API, so the routing logic just calls hgapi and assumes everything will be in order:


    def get(ref=None):
       rev = hgapi.Repo('.')[ref]

       return {
           'node': rev.node,
           'desc': rev.desc
       }

So, when we GET /api/changeset/1, the requesthandler will be passed this: ({'changeset': }, 'get', 'changeset', ('1',)). It will lookup 'changeset' to get the module, and then retrieve and call 'get' using getattr and pass in the '1'. changeset.get() will then call hgapi, stick it into a map, and requesthandler encodes it as json and returns it. Since none of the parts involved actually cares what the arguments are, you can just as well use /api/changeset/tip or /api/changeset/default.

As it looks now, the next part should probably be adding some tests, but since I'm not totally decided on how I want to write my tests, I'll push ahead with separating the code instead - the current PyRest class and everything that has to do with CherryPy should go into a pyrest.cherrypy package or something similar, the requesthandler and get_handler functions should stay as part of pyRest proper, and the backend package should probably end up in an example package.

Code is as always available at Bitbucket.

comments powered by Disqus