summaryrefslogtreecommitdiff
path: root/paste/cascade.py
diff options
context:
space:
mode:
Diffstat (limited to 'paste/cascade.py')
-rw-r--r--paste/cascade.py133
1 files changed, 133 insertions, 0 deletions
diff --git a/paste/cascade.py b/paste/cascade.py
new file mode 100644
index 0000000..8207ae3
--- /dev/null
+++ b/paste/cascade.py
@@ -0,0 +1,133 @@
+# (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