0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003"""Routines to generate WSGI responses"""
0004
0005############################################################
0006## Headers
0007############################################################
0008import warnings
0009
0010class HeaderDict(dict):
0011
0012 """
0013 This represents response headers. It handles the headers as a
0014 dictionary, with case-insensitive keys.
0015
0016 Also there is an ``.add(key, value)`` method, which sets the key,
0017 or adds the value to the current value (turning it into a list if
0018 necessary).
0019
0020 For passing to WSGI there is a ``.headeritems()`` method which is
0021 like ``.items()`` but unpacks value that are lists. It also
0022 handles encoding -- all headers are encoded in ASCII (if they are
0023 unicode).
0024
0025 @@: Should that encoding be ISO-8859-1 or UTF-8? I'm not sure
0026 what the spec says.
0027 """
0028
0029 def __getitem__(self, key):
0030 return dict.__getitem__(self, self.normalize(key))
0031
0032 def __setitem__(self, key, value):
0033 dict.__setitem__(self, self.normalize(key), value)
0034
0035 def __delitem__(self, key):
0036 dict.__delitem__(self, self.normalize(key))
0037
0038 def __contains__(self, key):
0039 return dict.__contains__(self, self.normalize(key))
0040
0041 has_key = __contains__
0042
0043 def get(self, key, failobj=None):
0044 return dict.get(self, self.normalize(key), failobj)
0045
0046 def setdefault(self, key, failobj=None):
0047 return dict.setdefault(self, self.normalize(key), failobj)
0048
0049 def pop(self, key):
0050 return dict.pop(self, self.normalize(key))
0051
0052 def update(self, other):
0053 for key in other:
0054 self[self.normalize(key)] = other[key]
0055
0056 def normalize(self, key):
0057 return str(key).lower().strip()
0058
0059 def add(self, key, value):
0060 key = self.normalize(key)
0061 if key in self:
0062 if isinstance(self[key], list):
0063 self[key].append(value)
0064 else:
0065 self[key] = [self[key], value]
0066 else:
0067 self[key] = value
0068
0069 def headeritems(self):
0070 result = []
0071 for key in self:
0072 if isinstance(self[key], list):
0073 for v in self[key]:
0074 result.append((key, str(v)))
0075 else:
0076 result.append((key, str(self[key])))
0077 return result
0078
0079 #@classmethod
0080 def fromlist(cls, seq):
0081 self = cls()
0082 for name, value in seq:
0083 self.add(name, value)
0084 return self
0085
0086 fromlist = classmethod(fromlist)
0087
0088def has_header(headers, name):
0089 """
0090 Is header named ``name`` present in headers?
0091 """
0092 name = name.lower()
0093 for header, value in headers:
0094 if header.lower() == name:
0095 return True
0096 return False
0097
0098def header_value(headers, name):
0099 """
0100 Returns the header's value, or None if no such header. If a
0101 header appears more than once, all the values of the headers
0102 are joined with ','. Note that this is consistent /w RFC 2616
0103 section 4.2 which states:
0104
0105 It MUST be possible to combine the multiple header fields
0106 into one "field-name: field-value" pair, without changing
0107 the semantics of the message, by appending each subsequent
0108 field-value to the first, each separated by a comma.
0109
0110 However, note that the original netscape usage of 'Set-Cookie',
0111 especially in MSIE which contains an 'expires' date will is not
0112 compatible with this particular concatination method.
0113 """
0114 name = name.lower()
0115 result = [value for header, value in headers
0116 if header.lower() == name]
0117 if result:
0118 return ','.join(result)
0119 else:
0120 return None
0121
0122def remove_header(headers, name):
0123 """
0124 Removes the named header from the list of headers. Returns the
0125 value of that header, or None if no header found. If multiple
0126 headers are found, only the last one is returned.
0127 """
0128 name = name.lower()
0129 i = 0
0130 result = None
0131 while i < len(headers):
0132 if headers[i][0].lower() == name:
0133 result = headers[i][1]
0134 del headers[i]
0135 continue
0136 i += 1
0137 return result
0138
0139def replace_header(headers, name, value):
0140 """
0141 Updates the headers replacing the first occurance of the given name
0142 with the value provided; asserting that no further occurances
0143 happen. Note that this is _not_ the same as remove_header and then
0144 append, as two distinct operations (del followed by an append) are
0145 not atomic in a threaded environment. Returns the previous header
0146 value for the provided name, if any. Clearly one should not use
0147 this function with ``set-cookie`` or other names that may have more
0148 than one occurance in the headers.
0149 """
0150 name = name.lower()
0151 i = 0
0152 result = None
0153 while i < len(headers):
0154 if headers[i][0].lower() == name:
0155 assert not result, "two values for the header '%s' found" % name
0156 result = headers[i][1]
0157 headers[i] = (name, value)
0158 i += 1
0159 if not result:
0160 headers.append((name, value))
0161 return result
0162
0163
0164############################################################
0165## Deprecated methods
0166############################################################
0167
0168def error_body_response(error_code, message, __warn=True):
0169 """
0170 Returns a standard HTML response page for an HTTP error.
0171 **Note:** Deprecated
0172 """
0173 if __warn:
0174 warnings.warn(
0175 'wsgilib.error_body_response is deprecated; use the '
0176 'wsgi_application method on an HTTPException object '
0177 'instead', DeprecationWarning, 2)
0178 return '''\
0179<html>
0180 <head>
0181 <title>%(error_code)s</title>
0182 </head>
0183 <body>
0184 <h1>%(error_code)s</h1>
0185 %(message)s
0186 </body>
0187</html>''' % {
0188 'error_code': error_code,
0189 'message': message,
0190 }
0191
0192
0193def error_response(environ, error_code, message,
0194 debug_message=None, __warn=True):
0195 """
0196 Returns the status, headers, and body of an error response.
0197
0198 Use like:
0199
0200 .. code-block:: Python
0201
0202 status, headers, body = wsgilib.error_response(
0203 '301 Moved Permanently', 'Moved to <a href="%s">%s</a>'
0204 % (url, url))
0205 start_response(status, headers)
0206 return [body]
0207
0208 **Note:** Deprecated
0209 """
0210 if __warn:
0211 warnings.warn(
0212 'wsgilib.error_response is deprecated; use the '
0213 'wsgi_application method on an HTTPException object '
0214 'instead', DeprecationWarning, 2)
0215 if debug_message and environ.get('paste.config', {}).get('debug'):
0216 message += '\n\n<!-- %s -->' % debug_message
0217 body = error_body_response(error_code, message, __warn=False)
0218 headers = [('content-type', 'text/html'),
0219 ('content-length', str(len(body)))]
0220 return error_code, headers, body
0221
0222def error_response_app(error_code, message, debug_message=None,
0223 __warn=True):
0224 """
0225 An application that emits the given error response.
0226
0227 **Note:** Deprecated
0228 """
0229 if __warn:
0230 warnings.warn(
0231 'wsgilib.error_response_app is deprecated; use the '
0232 'wsgi_application method on an HTTPException object '
0233 'instead', DeprecationWarning, 2)
0234 def application(environ, start_response):
0235 status, headers, body = error_response(
0236 environ, error_code, message,
0237 debug_message=debug_message, __warn=False)
0238 start_response(status, headers)
0239 return [body]
0240 return application