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.