def __call__(self, environ, start_response): ''' Implements the WSGI specification for Pecan applications, utilizing ``WebOb``. ''' # create the request and response object req = self.request_cls(environ) resp = self.response_cls() state = RoutingState(req, resp, self) environ['pecan.locals'] = {'request': req, 'response': resp} controller = None # handle the request try: # add context and environment to the request req.context = environ.get('pecan.recursive.context', {}) req.pecan = dict(content_type=None) controller, args, kwargs = self.find_controller(state) self.invoke_controller(controller, args, kwargs, state) except Exception as e: # if this is an HTTP Exception, set it as the response if isinstance(e, exc.HTTPException): # if the client asked for JSON, do our best to provide it best_match = acceptparse.MIMEAccept( getattr(req.accept, 'header_value', '*/*')).best_match( ('text/plain', 'text/html', 'application/json')) state.response = e if best_match == 'application/json': json_body = dumps({ 'code': e.status_int, 'title': e.title, 'description': e.detail }) if isinstance(json_body, six.text_type): e.text = json_body else: e.body = json_body state.response.content_type = best_match environ['pecan.original_exception'] = e # if this is not an internal redirect, run error hooks on_error_result = None if not isinstance(e, ForwardRequestException): on_error_result = self.handle_hooks( self.determine_hooks(state.controller), 'on_error', state, e) # if the on_error handler returned a Response, use it. if isinstance(on_error_result, WebObResponse): state.response = on_error_result else: if not isinstance(e, exc.HTTPException): raise # if this is an HTTP 405, attempt to specify an Allow header if isinstance(e, exc.HTTPMethodNotAllowed) and controller: allowed_methods = _cfg(controller).get('allowed_methods', []) if allowed_methods: state.response.allow = sorted(allowed_methods) finally: # handle "after" hooks self.handle_hooks(self.determine_hooks(state.controller), 'after', state) self._handle_empty_response_body(state) # get the response return state.response(environ, start_response)
def find_controller(self, state): ''' The main request handler for Pecan applications. ''' # get a sorted list of hooks, by priority (no controller hooks yet) req = state.request pecan_state = req.pecan # store the routing path for the current application to allow hooks to # modify it pecan_state['routing_path'] = path = req.path_info # handle "on_route" hooks self.handle_hooks(self.hooks, 'on_route', state) # lookup the controller, respecting content-type as requested # by the file extension on the URI pecan_state['extension'] = None # attempt to guess the content type based on the file extension if self.guess_content_type_from_ext \ and not pecan_state['content_type'] \ and '.' in path: _, extension = splitext(path.rstrip('/')) # preface with a letter to ensure compat for 2.5 potential_type = guess_type('x' + extension)[0] if extension and potential_type is not None: path = ''.join(path.rsplit(extension, 1)) pecan_state['extension'] = extension pecan_state['content_type'] = potential_type controller, remainder = self.route(req, self.root, path) cfg = _cfg(controller) if cfg.get('generic_handler'): raise exc.HTTPNotFound # handle generic controllers im_self = None if cfg.get('generic'): im_self = six.get_method_self(controller) handlers = cfg['generic_handlers'] controller = handlers.get(req.method, handlers['DEFAULT']) handle_security(controller, im_self) cfg = _cfg(controller) # add the controller to the state so that hooks can use it state.controller = controller # if unsure ask the controller for the default content type content_types = cfg.get('content_types', {}) if not pecan_state['content_type']: # attempt to find a best match based on accept headers (if they # exist) accept = getattr(req.accept, 'header_value', '*/*') if accept == '*/*' or (accept.startswith('text/html,') and list( content_types.keys()) in self.SIMPLEST_CONTENT_TYPES): pecan_state['content_type'] = cfg.get('content_type', 'text/html') else: best_default = acceptparse.MIMEAccept(accept).best_match( content_types.keys()) if best_default is None: msg = "Controller '%s' defined does not support " + \ "content_type '%s'. Supported type(s): %s" logger.error( msg % (controller.__name__, pecan_state['content_type'], content_types.keys())) raise exc.HTTPNotAcceptable() pecan_state['content_type'] = best_default elif cfg.get('content_type') is not None and \ pecan_state['content_type'] not in content_types: msg = "Controller '%s' defined does not support content_type " + \ "'%s'. Supported type(s): %s" logger.error(msg % (controller.__name__, pecan_state['content_type'], content_types.keys())) raise exc.HTTPNotFound # fetch any parameters if req.method == 'GET': params = req.GET elif req.content_type in ('application/json', 'application/javascript'): try: if not isinstance(req.json, dict): raise TypeError('%s is not a dict' % req.json) params = NestedMultiDict(req.GET, req.json) except (TypeError, ValueError): params = req.params else: params = req.params # fetch the arguments for the controller args, varargs, kwargs = self.get_args(state, params.mixed(), remainder, cfg['argspec'], im_self) state.arguments = Arguments(args, varargs, kwargs) # handle "before" hooks self.handle_hooks(self.determine_hooks(controller), 'before', state) return controller, args + varargs, kwargs