cascade.py 4.4 KB

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