class ServicePublisher(object): def __init__(self, fallbackapp=None, endpoints=None, debug=False, options=None, default_error_handler=None): # Note fallback app needs to be a wsgi-compatible callable if fallbackapp: assert callable(fallbackapp), "Fallback app must be callable" self._fallbackapp = fallbackapp self._debug = debug if self._debug: _log.setLevel(logging.DEBUG) self._endpoints = [] if endpoints: assert isinstance(endpoints, list), "endpoints must be a list" for ep in endpoints: self.add_endpoint(ep) if not default_error_handler: default_error_handler = JsonErrorHandler self._options = Dictomatic({ "default_error_handler": default_error_handler, }) if options: assert isinstance(options, dict), "options must be of type dict" self._options.update(options) self.verify_options() def verify_options(self): msg = "Default exception handler " assert self._options.default_error_handler, msg + "must exist" assert isinstance(self._options.default_error_handler.code, int),\ msg + "http code must be an int" assert self._options.default_error_handler.code in responses,\ msg + "http code not in nudge.publisher.responses" assert isinstance( self._options.default_error_handler.content_type, str),\ msg + "content_type must be a byte string" assert isinstance(self._options.default_error_handler.content, str),\ msg + "content must be a byte string" assert isinstance(self._options.default_error_handler.headers, dict),\ msg + "headers must be a dict" for k, v in self._options.default_error_handler.headers: assert isinstance(k, str),\ msg+ "headers keys and values must be a byte string" assert isinstance(v, str),\ msg + "headers keys and values must be a byte string" # Set default error params here incase of massive failure we fallback # to these. self._options.default_error_response = ( self._options.default_error_handler.code, self._options.default_error_handler.content_type, self._options.default_error_handler.content, self._options.default_error_handler.headers, ) def add_endpoint(self, endpoint): assert isinstance(endpoint, Endpoint) self._endpoints.append(endpoint) def _add_args(self, req): args = req.QUERY_STRING.split('=') def __call__(self, environ, start_response): ''' This is called by each request to the server. This MUST return a valid HTTP response under all circumstances. We expect environ to be a valid wgsi python dictionary. ''' req = WSGIRequest(environ) # if isinstance(environ, types.DictType): # req = WSGIRequest(environ) # else: # req = environ # main exception handler to ensure client gets valid response. # defer any mutation of the request object (incl. writes to the client) # until you're sure all exception prone activities have been performed # successfully (aka: "basic exception guarantee") code = None final_content = "" endpoint = None try: # allow '_method' query arg to override method method = req.method if '_method' in req.arguments: method = req.arguments['_method'][0].upper() del req.arguments['_method'] # find appropriate endpoint reqline = method + urllib.unquote(req.path) match = None for endpoint in self._endpoints: match = endpoint.match(reqline) if match: break if not match: if self._fallbackapp: _log.debug("Using fallback app for request: (%s) (%s)" % \ (method, req.uri)) # Since recreating a stringio from the request body can be # expensive, we only want to do this if we fallback. environ['wsgi.input'] = StringIO.StringIO(req.body) return self._fallbackapp(environ, start_response) else: raise HTTPException(404) # convert all values in req.arguments from lists to scalars, # then combine with path args. arguments = dict((k, v[0]) for k, v in req.arguments.iteritems()\ if isinstance(v, list) and len(v) > 0) inargs = dict(match.groupdict(), **arguments) # compile positional arguments args = [] for arg in endpoint.sequential: args.append(arg.argspec(req, inargs)) # compile keyword arguments kwargs = {} for argname, arg in endpoint.named.iteritems(): r = arg.argspec(req, inargs) if r != None: kwargs[argname] = r # invoke the service endpoint result = endpoint(*args, **kwargs) # TODO make sure this works with unicode _log.debug(_gen_trace_str(endpoint.function, args, kwargs, result)) if isinstance(endpoint.renderer, RequestAwareRenderer): r = endpoint.renderer(req, result) else: r = endpoint.renderer(result) content, content_type, code, extra_headers = \ r.content, r.content_type, r.http_status, r.headers except (Exception), e: error_response = None logged_trace = False # # Try to use this endpoint's exception handler(s) # If the raised exception is not mapped in this endpoint, or # this endpoint raises an exception when trying to handle, # we will then try to the default handler, and ultimately # fallback to the self._options.default_error_response, which # is guaranteed to be valid at app initialization. # if endpoint and endpoint.exceptions: try: error_response = handle_exception( e, endpoint.exceptions, default_handler=self._options.default_error_handler) if not error_response: raise except (Exception), e: _log.exception("Endpoint (%s) failed to handle exception" % endpoint.name) logged_trace = True if not error_response: try: # Try one more time to handle a base exception handler = self._options.default_error_handler() error_response = handler(e) except (Exception), e: _log.error( "Default error handler failed to handle exception") if logged_trace is False: _log.exception(e)
class ServicePublisher(object): def __init__(self, fallbackapp=None, endpoints=None, \ debug=False, options=None, default_error_handler=None): self._debug = debug if self._debug: _log.setLevel(logging.DEBUG) self._endpoints = [] if endpoints: assert isinstance(endpoints, list), "endpoints must be a list" for ep in endpoints: self.add_endpoint(ep) # TODO Fix fallback app here and below self._fallbackapp = fallbackapp if not default_error_handler: default_error_handler = JsonErrorHandler self._options = Dictomatic({ "default_error_handler": default_error_handler, }) if options: assert isinstance(options, dict), "options must be of type dict" self._options.update(options) self.verify_options() def verify_options(self): msg = "Default exception handler " assert self._options.default_error_handler, msg + "must exist" assert isinstance(self._options.default_error_handler.code, int),\ msg + "http code must be an int" assert self._options.default_error_handler.code in responses,\ msg + "http code not in nudge.publisher.responses" assert isinstance( self._options.default_error_handler.content_type, str),\ msg + "content_type must be a byte string" assert isinstance(self._options.default_error_handler.content, str),\ msg + "content must be a byte string" assert isinstance(self._options.default_error_handler.headers, dict),\ msg + "headers must be a dict" for k, v in self._options.default_error_handler.headers: assert isinstance(k, str),\ msg+ "headers keys and values must be a byte string" assert isinstance(v, str),\ msg + "headers keys and values must be a byte string" # Set default error params here incase of massive failure we fallback # to these. self._options.default_error_response = ( self._options.default_error_handler.code, self._options.default_error_handler.content_type, self._options.default_error_handler.content, self._options.default_error_handler.headers, ) def add_endpoint(self, endpoint): assert isinstance(endpoint, Endpoint) self._endpoints.append(endpoint) def _add_args(self, req): args = req.QUERY_STRING.split('=') def __call__(self, environ, start_response): """ This is called by each request to the server. This MUST return a valid HTTP response under all circumstances. """ if isinstance(environ, types.DictType): req = WSGIRequest(environ) else: req = environ # main exception handler to ensure client gets valid response. # defer any mutation of the request object (incl. writes to the client) # until you're sure all exception prone activities have been performed # successfully (aka: "basic exception guarantee") code = None final_content = "" endpoint = None try: # allow '_method' query arg to overide method method = req.method if '_method' in req.arguments: method = req.arguments['_method'][0].upper() del req.arguments['_method'] # find appropriate endpoint reqline = method + urllib.unquote(req.path) match = None for endpoint in self._endpoints: match = endpoint.match(reqline) if match: break if not match: # TODO: Handle HTTPException in new world exceptions raise HTTPException(404) # # Fallback app is untested with WSGI/EVENTLET # FIXTHIS!! # # if self._fallbackapp: # _log.debug("falling through: %s %s" % (method, req.uri)) # return self._fallbackapp(event_req, start_response) # else: # raise HTTPException(404) # convert all values in req.arguments from lists to scalars, # then combine with path args. arguments = dict((k, v[0]) for k, v in req.arguments.iteritems()\ if isinstance(v, list) and len(v) > 0) inargs = dict(match.groupdict(), **arguments) # compile positional arguments args = [] for arg in endpoint.sequential: args.append(arg.argspec(req, inargs)) # compile keyword arguments kwargs = {} for argname, arg in endpoint.named.iteritems(): r = arg.argspec(req, inargs) if r != None: kwargs[argname] = r # invoke the service endpoint result = endpoint(*args, **kwargs) # TODO make sure this works with unicode _log.debug(_gen_trace_str(endpoint.function, args, kwargs, result)) if isinstance(endpoint.renderer, RequestAwareRenderer): r = endpoint.renderer(req, result) else: r = endpoint.renderer(result) content, content_type, code, extra_headers = \ r.content, r.content_type, r.http_status, r.headers except (Exception), e: error_response = None logged_trace = False # # Try to use this endpoint's exception handler(s) # If the raised exception is not mapped in this endpoint, or # this endpoint raises an exception when trying to handle, # we will then try to the default handler, and ultimately # fallback to the self._options.default_error_response, which # is guaranteed to be valid at app initialization. # if endpoint and endpoint.exceptions: try: error_response = handle_exception( e, endpoint.exceptions, default_handler=self._options.default_error_handler ) except (Exception), e: # TODO this may log too loudly _log.exception( "Endpoint %s failed to handle exception" % endpoint.name ) logged_trace = True if not error_response: try: # Try one more time to handle a base exception handler = self._options.default_error_handler() error_response = handler(e) except (Exception), e: _log.error( "Default error handler failed to handle exception") if logged_trace is False: _log.exception(e)