# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ Cascades through several applications, so long as applications return ``404 Not Found``. """ from paste import httpexceptions from paste.util import converters import tempfile from cStringIO import StringIO __all__ = ['Cascade'] def make_cascade(loader, global_conf, catch='404', **local_conf): """ Entry point for Paste Deploy configuration Expects configuration like:: [composit:cascade] use = egg:Paste#cascade # all start with 'app' and are sorted alphabetically app1 = foo app2 = bar ... catch = 404 500 ... """ catch = map(int, converters.aslist(catch)) apps = [] for name, value in local_conf.items(): if not name.startswith('app'): raise ValueError( "Bad configuration key %r (=%r); all configuration keys " "must start with 'app'" % (name, value)) app = loader.get_app(value, global_conf=global_conf) apps.append((name, app)) apps.sort() apps = [app for name, app in apps] return Cascade(apps, catch=catch) class Cascade(object): """ Passed a list of applications, ``Cascade`` will try each of them in turn. If one returns a status code listed in ``catch`` (by default just ``404 Not Found``) then the next application is tried. If all applications fail, then the last application's failure response is used. Instances of this class are WSGI applications. """ def __init__(self, applications, catch=(404,)): self.apps = applications self.catch_codes = {} self.catch_exceptions = [] for error in catch: if isinstance(error, str): error = int(error.split(None, 1)[0]) if isinstance(error, httpexceptions.HTTPException): exc = error code = error.code else: exc = httpexceptions.get_exception(error) code = error self.catch_codes[code] = exc self.catch_exceptions.append(exc) self.catch_exceptions = tuple(self.catch_exceptions) def __call__(self, environ, start_response): """ WSGI application interface """ failed = [] def repl_start_response(status, headers, exc_info=None): code = int(status.split(None, 1)[0]) if code in self.catch_codes: failed.append(None) return _consuming_writer return start_response(status, headers, exc_info) try: length = int(environ.get('CONTENT_LENGTH', 0) or 0) except ValueError: length = 0 if length > 0: # We have to copy wsgi.input copy_wsgi_input = True if length > 4096 or length < 0: f = tempfile.TemporaryFile() if length < 0: f.write(environ['wsgi.input'].read()) else: copy_len = length while copy_len > 0: chunk = environ['wsgi.input'].read(min(copy_len, 4096)) if not chunk: raise IOError("Request body truncated") f.write(chunk) copy_len -= len(chunk) f.seek(0) else: f = StringIO(environ['wsgi.input'].read(length)) environ['wsgi.input'] = f else: copy_wsgi_input = False for app in self.apps[:-1]: environ_copy = environ.copy() if copy_wsgi_input: environ_copy['wsgi.input'].seek(0) failed = [] try: v = app(environ_copy, repl_start_response) if not failed: return v else: if hasattr(v, 'close'): # Exhaust the iterator first: list(v) # then close: v.close() except self.catch_exceptions: pass if copy_wsgi_input: environ['wsgi.input'].seek(0) return self.apps[-1](environ, start_response) def _consuming_writer(s): pass