Fredrik Håård's Blaag

I'm a programmer, consultant, developer, occasional teacher and speaker. Among my least disliked programming languages are Python, and a majority of these posts are related to Python in one way or another.
RSS Feed

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/<resource>), 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': <module>}, '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.

Blaag created 121214 16:24
blog comments powered by Disqus