Home | History | Annotate | Download | only in paste
      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 """
      5 Cascades through several applications, so long as applications
      6 return ``404 Not Found``.
      7 """
      8 from paste import httpexceptions
      9 from paste.util import converters
     10 import tempfile
     11 from cStringIO import StringIO
     12 
     13 __all__ = ['Cascade']
     14 
     15 def 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 
     43 class 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 
    132 def _consuming_writer(s):
    133     pass
    134