Apr 11, 2013

CherryPy 101 - Method Dispatcher

Following the previous post about the default dispatcher, where I mention the existence of another kind of dispatchers besides the default dispatcher, here I will cover the Method Dispatcher which already have a couple of tutorials and examples, but for the sake of completion here is a moderately complex example, showing the capabilities.

The Gists object is an implementation of the kind of dispatching that the github gists api is currently using, overlooking the authentication and the validation of the json properties.

import cherrypy

def dict_not_found():
    cherrypy.response.status = 404
    return {"message": "Not found"}


class PublicGists(object):
    exposed = True

    def GET(self):
        return {"message": "This are the public gists"}


class StarredGists(object):
    exposed = True

    def GET(self):
        return {"message": "This are the starred gists"}


class Gists(object):
    exposed = True
    public = PublicGists()    # /gists/public
    starred = StarredGists()  # /gists/starred

    def __init__(self):
        # The properties attribute correspond to the urls in the form:
        # /gists/ID/PROP, mapping: PROP -> METHOD -> FUNC
        self.properties = {'star': {'GET': self._is_starred,
                                    'PUT': self._star,
                                    'DELETE': self._remove_star},
                           'forks': {'POST': self._forks}}

    def _modify_property(self, property, *args, **kwargs):
        method = cherrypy.request.method
        try:
            property_handler = self.properties[property][method]
        except KeyError:
            return dict_not_found()
        else:
            return property_handler(*args, **kwargs)

    def _fetch_gist(self, gid):
        return {"message": "This is the gist {0}".format(gid)}

    def _forks(self, gid):
        response = cherrypy.response
        response.status = 201
        response.headers['Location'] = cherrypy.url('/gists/2')
        return {"message": "The gist %s has been forked" % gid}

    def _is_starred(self, gid):
        if gid == '1':
            cherrypy.response.status = 204
        else:
            cherrypy.response.status = 404

    def _star(self, gid):
        return {"message": "The gist %s has been starred" % gid}

    def _remove_star(self, gid):
        return {"message": "The gist %s has been unstarred" % gid}

    def GET(self, gid=None, prop=None):
        if gid is None:
            return self.public.GET()
        if prop is None:
            return self._fetch_gist(gid)
        return self._modify_property(prop, gid)

    # this decorator indicates that the request may or may not
    # specify that the body is a json document, which is
    # the case when: POST /gists/ID/forks
    @cherrypy.config(**{'tools.json_in.force': False})
    def POST(self, gid=None, prop=None):
        if gid is None:
            response = cherrypy.response
            response.status = 201
            response.headers['Location'] = cherrypy.url('/gists/1')
            return {"message": "Gist created with json %s" %
                    cherrypy.request.json}
        if prop is None:
            return dict_not_found()
        return self._modify_property(prop, gid)

    def PATCH(self, gid):
        return {"message": "Patching gist with json %s" %
                cherrypy.request.json}

    # this decorator disable the processing of the body
    # as json, because for the current implemented properties
    # ("star") is not used.
    @cherrypy.config(**{'tools.json_in.on': False})
    def PUT(self, gid, prop):
        return self._modify_property(prop, gid)

    def DELETE(self, gid, prop):
        return self._modify_property(prop, gid)


class API(object):
    # _cp_config indicates that all the subsequent objects
    # are going to be pre/post processed as json documents
    # serializing and deserializing: JSON -> dict -> JSON
    # unless is "overwritten", which some do with the
    # cherrypy.config decorator.
    _cp_config  = {'tools.json_out.on': True,
                   'tools.json_in.on': True}
    exposed = True
    gists = Gists()

    def GET(self):
        return {'message': 'Nothing to see here, go to /gists'}


if __name__ == '__main__':
    config = {'/':
              {'request.methods_with_bodies': ('POST', 'PUT', 'PATCH'),
               'request.dispatch':
                 cherrypy.dispatch.MethodDispatcher()}}
    cherrypy.quickstart(API(), config=config)

Something peculiar is that 'request.methods_with_bodies': ('POST', 'PUT', 'PATCH') probably in the near future that would not be required, that's just because PATCH is not defined as a method that can have a body, it is relatively new to the HTTP/1.1 spec, but it does demonstrate the capabilities to modify the internals of cherrypy from the application code, the configuration mechanism of cherrypy it is extremely flexible, that probably deserves a post for itself.

Just for the curios this are a bunch of curl calls to test the api:

# Nothing relevant.
curl -i localhost:8080/ ; echo
# Public gists
curl -i localhost:8080/gists/; echo
curl -i localhost:8080/gists/public ; echo
# Starred gists
curl -i localhost:8080/gists/starred ; echo
# Get a gist by id
curl -i localhost:8080/gists/1 ; echo
# Create a gist
curl -i -H "Content-Type: application/json" \
     -d '{"content": "This is the gist content"}' localhost:8080/gists/ ; echo
# Edit a gist
curl -i -X PATCH  -H "Content-Type: application/json" \
     -d '{"content": "This is thew new content"}' localhost:8080/gists/1 ; echo
# Star a gist
curl  -i -X PUT -H "Content-Length: 0" localhost:8080/gists/1/star ; echo
# Unstar a gist
curl -i  -X DELETE localhost:8080/gists/1/star; echo
# Check if a gist is starred
curl -i localhost:8080/gists/1/star ; echo
curl -i localhost:8080/gists/2/star ; echo
# Fork a gist
curl -i -X POST -H "Content-Length: 0" localhost:8080/gists/1/forks; echo
# Delete a gist
curl -i  -X DELETE localhost:8080/gists/2 ; echo

There are a few discrepancies in the Content-Type: application/json requirement but I think that is out of scope to really go on further details.

I hope this post helps you to understand a little bit more the MethodDispacher.

Tags: cherrypy webdev python