1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3
4"""
5Cascades through several applications, so long as applications
6return ``404 Not Found``.
7"""
8from paste import httpexceptions
9from paste.util import converters
10import tempfile
11from cStringIO import StringIO
12
13__all__ = ['Cascade']
14
15def make_cascade(loader, global_conf, catch='404', **local_conf):
16    """
17    Entry point for Paste Deploy configuration
18
19    Expects configuration like::
20
21        [composit:cascade]
22        use = egg:Paste#cascade
23        # all start with 'app' and are sorted alphabetically
24        app1 = foo
25        app2 = bar
26        ...
27        catch = 404 500 ...
28    """
29    catch = map(int, converters.aslist(catch))
30    apps = []
31    for name, value in local_conf.items():
32        if not name.startswith('app'):
33            raise ValueError(
34                "Bad configuration key %r (=%r); all configuration keys "
35                "must start with 'app'"
36                % (name, value))
37        app = loader.get_app(value, global_conf=global_conf)
38        apps.append((name, app))
39    apps.sort()
40    apps = [app for name, app in apps]
41    return Cascade(apps, catch=catch)
42
43class Cascade(object):
44
45    """
46    Passed a list of applications, ``Cascade`` will try each of them
47    in turn.  If one returns a status code listed in ``catch`` (by
48    default just ``404 Not Found``) then the next application is
49    tried.
50
51    If all applications fail, then the last application's failure
52    response is used.
53
54    Instances of this class are WSGI applications.
55    """
56
57    def __init__(self, applications, catch=(404,)):
58        self.apps = applications
59        self.catch_codes = {}
60        self.catch_exceptions = []
61        for error in catch:
62            if isinstance(error, str):
63                error = int(error.split(None, 1)[0])
64            if isinstance(error, httpexceptions.HTTPException):
65                exc = error
66                code = error.code
67            else:
68                exc = httpexceptions.get_exception(error)
69                code = error
70            self.catch_codes[code] = exc
71            self.catch_exceptions.append(exc)
72        self.catch_exceptions = tuple(self.catch_exceptions)
73
74    def __call__(self, environ, start_response):
75        """
76        WSGI application interface
77        """
78        failed = []
79        def repl_start_response(status, headers, exc_info=None):
80            code = int(status.split(None, 1)[0])
81            if code in self.catch_codes:
82                failed.append(None)
83                return _consuming_writer
84            return start_response(status, headers, exc_info)
85
86        try:
87            length = int(environ.get('CONTENT_LENGTH', 0) or 0)
88        except ValueError:
89            length = 0
90        if length > 0:
91            # We have to copy wsgi.input
92            copy_wsgi_input = True
93            if length > 4096 or length < 0:
94                f = tempfile.TemporaryFile()
95                if length < 0:
96                    f.write(environ['wsgi.input'].read())
97                else:
98                    copy_len = length
99                    while copy_len > 0:
100                        chunk = environ['wsgi.input'].read(min(copy_len, 4096))
101                        if not chunk:
102                            raise IOError("Request body truncated")
103                        f.write(chunk)
104                        copy_len -= len(chunk)
105                f.seek(0)
106            else:
107                f = StringIO(environ['wsgi.input'].read(length))
108            environ['wsgi.input'] = f
109        else:
110            copy_wsgi_input = False
111        for app in self.apps[:-1]:
112            environ_copy = environ.copy()
113            if copy_wsgi_input:
114                environ_copy['wsgi.input'].seek(0)
115            failed = []
116            try:
117                v = app(environ_copy, repl_start_response)
118                if not failed:
119                    return v
120                else:
121                    if hasattr(v, 'close'):
122                        # Exhaust the iterator first:
123                        list(v)
124                        # then close:
125                        v.close()
126            except self.catch_exceptions:
127                pass
128        if copy_wsgi_input:
129            environ['wsgi.input'].seek(0)
130        return self.apps[-1](environ, start_response)
131
132def _consuming_writer(s):
133    pass
134