import cherrypy from cherrypy._cpdispatch import PageHandler, LateParamPageHandler import routes import logging import yaml REST_METHODS = ('GET','PUT','POST','DELETE') def is_form_post(environ): """Determine whether the request is a POSTed html form""" if environ['REQUEST_METHOD'] != 'POST': return False content_type = environ.get('CONTENT_TYPE', '').lower() if ';' in content_type: content_type = content_type.split(';', 1)[0] return content_type in ('application/x-www-form-urlencoded','multipart/form-data') class ResourceDispatcher(object): """ A Routes-based dispatcher for CherryPy that maps RESTful resources. The dispatcher is meant to follow most of the conventions pioneers by the Ruby on Rails framework's router. In particular, if you do not predefine the keys for the controllers prior to configuring the routes, ResourceDispatcher.resource() will try to add a CamelCase version of the given collection_name + the "Controller" suffix. For example mapper.resource("item","items") will try to add {"items": ItemsController()} to the controllers dict. Otherwise the connect() and resource() methods are passed directly to routes.Mapper() and work in the same manner. """ def __init__(self,controllers={}): """ Resource dispatcher RESTful Routes-based dispatcher for CherryPy. Provide a dict of {collection_name: Controller()} to initialize the set of controllers available. Otherwise, set the controllers attr to this hash after creation. """ import routes self.controllers = controllers self.mapper = routes.Mapper() self.mapper.controller_scan = self.controllers.keys def connect(self, *args, **kwargs): """Create and connect a new Route to the dispatcher.""" self.mapper.connect(*args, **kwargs) def resource(self,member_name,collection_name): """Maps a resource, given the member_name and collection_name of that resource. If the resource does not yet exist in the set of defined controllers, this method will attempt the add it, following a standard naming convention pioneered by Ruby on Rails, where the plural snake_cased collection name is turned to CamelCase and suffixed with "Controller" for the class' name. For example mapper.resource('blog_comment','blog_comments') would map to the controller BlogCommentsController.""" if collection_name not in self.controllers.keys(): self.controllers[collection_name] = \ eval("%s()" % collection_name.title().replace("_","")) self.mapper.resource(member_name,collection_name) def redirect(self, url): """A wrapper for CherryPy's HTTPRedirect method""" raise cherrypy.HTTPRedirect(url) def __call__(self, path_info): """Set handler and config for the current request.""" func = self.find_handler(path_info) if func: cherrypy.response.headers['Allow'] = ", ".join(REST_METHODS) cherrypy.request.handler = LateParamPageHandler(func) else: cherrypy.request.handler = cherrypy.NotFound() def find_handler(self,path_info): """Find the right page handler, and set request.config.""" request = cherrypy.request environ = cherrypy.request.wsgi_environ # account for HTTP REQUEST_METHOD overrides old_method = None if '_method' in environ.get('QUERY_STRING', '') and \ request.params.get('_method','').upper() in REST_METHODS: old_method = environ['REQUEST_METHOD'] environ['REQUEST_METHOD'] = request.params['_method'].upper() logging.debug("_method found in QUERY_STRING, altering request" " method to %s", environ['REQUEST_METHOD']) elif is_form_post(environ): # must parse the request body to get the method param request.process_body() m = request.params.get('_method',None) if m is not None and m.upper() in REST_METHODS: old_method = environ['REQUEST_METHOD'] environ['REQUEST_METHOD'] = m.upper() logging.debug("_method found in POST data, altering request " "method to %s", environ['REQUEST_METHOD']) config = routes.request_config() # Hook up the routes variables for this request config.mapper = self.mapper config.environ = environ config.host = request.headers.get('Host',None) config.protocol = request.scheme config.redirect = self.redirect result = self.mapper.match(path_info) m = self.mapper.match(path_info) config.mapper_dict = result if old_method: environ['REQUEST_METHOD'] = old_method # also pop out the _method request param, if it exists request.params.pop('_method', None) params = {} if result: params = result.copy() params.pop('controller', None) params.pop('action', None) request.params.update(params) # Get config for the root object/path. request.config = base = cherrypy.config.copy() curpath = "" def merge(nodeconf): if 'tools.staticdir.dir' in nodeconf: nodeconf['tools.staticdir.section'] = curpath or "/" base.update(nodeconf) app = request.app root = app.root if hasattr(root, "_cp_config"): merge(root._cp_config) if "/" in app.config: merge(app.config["/"]) # Mix in values from app.config. atoms = [x for x in path_info.split("/") if x] if atoms: last = atoms.pop() else: last = None for atom in atoms: curpath = "/".join((curpath, atom)) if curpath in app.config: merge(app.config[curpath]) handler = None if result: controller = result.get('controller', None) controller = self.controllers.get(controller) if controller: # Get config from the controller. if hasattr(controller, "_cp_config"): merge(controller._cp_config) action = result.get('action', None) if action is not None: handler = getattr(controller, action, None) # Get config from the handler if hasattr(handler, "_cp_config"): merge(handler._cp_config) # Do the last path atom here so it can # override the controller's _cp_config. if last: curpath = "/".join((curpath, last)) if curpath in app.config: merge(app.config[curpath]) return handler