Skip to main content

pyRest part 3: Routing and responsibilities

·3 mins

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.