Package wsgiwapi :: Module wsgisupport
[frames] | no frames]

Source Code for Module wsgiwapi.wsgisupport

  1  # Copyright (c) 2009 Richard Boulton 
  2  # 
  3  # Permission is hereby granted, free of charge, to any person obtaining a copy 
  4  # of this software and associated documentation files (the "Software"), to deal 
  5  # in the Software without restriction, including without limitation the rights 
  6  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
  7  # copies of the Software, and to permit persons to whom the Software is 
  8  # furnished to do so, subject to the following conditions: 
  9  # 
 10  # The above copyright notice and this permission notice shall be included in 
 11  # all copies or substantial portions of the Software. 
 12  # 
 13  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 14  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 15  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 16  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 17  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 18  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 19  # SOFTWARE. 
 20  r"""Support utilities for building a WSGI application. 
 21   
 22  """ 
 23  __docformat__ = "restructuredtext en" 
 24   
 25  import cgi 
 26  import re 
 27  import StringIO 
 28  import pathinfo 
 29  import urllib 
 30   
 31  import reason_phrases 
 32   
33 -def to_uni(text):
34 """Convert text into unicode, if it's not already unicode. 35 36 """ 37 if isinstance(text, str): 38 return text.decode('utf-8') 39 return text
40
41 -def method_known(method):
42 """Return True iff the method string is one of the known HTTP methods. 43 44 """ 45 return method in ( 46 'OPTIONS', 47 'GET', 48 'HEAD', 49 'POST', 50 'PUT', 51 'DELETE', 52 'TRACE', 53 'CONNECT', 54 )
55
56 -class Request(object):
57 """Request object, used to represent a request via WSGI. 58 59 """
60 - def __init__(self, environ):
61 self.path = to_uni(environ.get('PATH_INFO', u'/')) 62 if not self.path: 63 self.path = u'/' 64 self.path_components = self.path.split(u'/')[1:] 65 66 # FIXME - set method 67 self.method = environ['REQUEST_METHOD'].upper() 68 69 self.content_length = 0 70 self.input = None 71 if self.method in ('POST', 'PUT'): 72 # FIXME - probably need to do something with chunked encoding here 73 self.content_length = int(environ.get('CONTENT_LENGTH', 0)) 74 if self.content_length > 0: 75 self.input = environ['wsgi.input'] 76 self.content_type = environ.get('CONTENT_TYPE', '') 77 78 self.GET = cgi.parse_qs(environ.get('QUERY_STRING', '')) 79 self.params = self.GET
80
81 - def _set_handler_props(self, handler_props):
82 """Set the WSGIWAPI properties found on the handler. 83 84 This is used to warn when the properties on the handler do not match 85 those used by the decorator - this is usually due to a second decorator 86 dropping the properties. 87 88 """ 89 self._handler_props = handler_props
90
91 - def _set_pathinfo(self, components):
92 """Set the path info to a list of components. 93 94 """ 95 self.pathinfo = pathinfo.PathInfo(components)
96
97 - def __str__(self):
98 return u"Request(%s, \"%s\", %r)" % ( 99 self.method, 100 self.path, 101 self.GET 102 )
103
104 -class WSGIResponse(object):
105 """Object satisfying the WSGI protocol for making a response. 106 107 This object should be passed the start_reponse parameter (as supplied by 108 the WSGI protocol), and the Response object for the response. The status 109 code, headers, and response body will be read from the Response object. 110 111 """
112 - def __init__(self, start_response, response):
113 self.start_response = start_response 114 self.response = response
115
116 - def __iter__(self):
117 self.start_response(self.response.status, 118 self.response.headers.items()) 119 yield self.response.body
120
121 - def __len__(self):
122 return len(self.response.body)
123
124 -def _string_to_ascii(value, description):
125 """Convert a string to a byte string encoded in us-ascii. 126 127 If the input is a byte string, this simply checks that all characters in it 128 are us-ascii. 129 130 - `value` is the value to convert. 131 - `description` is a description of the string, used in error messages. 132 133 """ 134 if isinstance(value, unicode): 135 try: 136 value = value.encode('us-ascii') 137 except UnicodeError, e: 138 e.reason += ", %s must be encodable as US-ASCII" % description 139 raise 140 else: 141 try: 142 value.decode('us-ascii') 143 except UnicodeError, e: 144 e.reason += ", %s must be encodable as US-ASCII" % description 145 raise 146 return value
147 148 # tspecials are defined in RFC 2045 as any of: ()<>@,;:\"/[]?= 149 _tspecials_pattern = re.compile(r'[\(\)<>@,;:\\"/\[\]\?=]') 150 # tokens are composed of characters in usascii range 33-127 (inclusive). 151
152 -def _validate_token(token):
153 """Check that a token only contains characters which are in us-ascii range 154 33-127 inclusive, and are not in tspecials. 155 156 """ 157 for char in token: 158 if ord(char) < 33 or ord(char) > 127: 159 return False 160 if _tspecials_pattern.match(char): 161 return False 162 return True
163
164 -def _encode_with_rfc2231(paramname, paramvalue):
165 # FIXME - allow the language to be set, somehow. 166 167 paramvalue = paramvalue.encode('utf-8') 168 169 # percent encode all parameter values other than 170 # ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "!" / "$" / "&" / "+" 171 paramvalue = urllib.quote(paramvalue, safe='-._~:!$&+') 172 173 return paramname + "*", "utf-8''" + paramvalue
174
175 -class Headers(object):
176 - def __init__(self):
177 self._headers = []
178
179 - def set(self, header, value, params={}, **kwargs):
180 """Set the value of a header. 181 182 If any values for the header already exist in the list of headers, they 183 are first removed. The comparison of header names is performed case 184 insensitively. 185 186 """ 187 self.remove(header) 188 self.add(header, value, params, **kwargs)
189
190 - def add(self, header, value, params={}, **kwargs):
191 """Add a header value. 192 193 FIXME - document 194 195 """ 196 header = _string_to_ascii(header, "header name") 197 value = _string_to_ascii(value, "header value") 198 199 # Get parameter values, quote if possible, or encode using 200 formatted_params = [] 201 def iteritems(a, b): 202 for k, v in a.iteritems(): 203 yield k, v 204 for k, v in b.iteritems(): 205 yield k, v
206 for paramname, paramvalue in iteritems(params, kwargs): 207 paramname = _string_to_ascii(paramname, "parameter name") 208 # Parameter names must only contain ascii characters which are 209 # not in tspecials, SPACE or CTLs. 210 if not _validate_token(paramname): 211 raise InvalidArgumentError("Parameter name contained " 212 "invalid characters") 213 214 # Parameter values should be unquoted if they don't contain any 215 # tspecials characters, quoted if they do, and encoded 216 # according to RFC2231 if they contain non-US-ASCII characters. 217 try: 218 paramvalue = _string_to_ascii(paramvalue, "parameter value") 219 except UnicodeError, e: 220 if not isinstance(paramvalue, unicode): 221 raise 222 # Encode according to RFC2231, if it's unicode 223 # FIXME - implement 224 paramname, paramvalue = _encode_with_rfc2231(paramname, paramvalue) 225 else: 226 if not _validate_token(paramvalue): 227 # Quote it 228 paramvalue = '"' + paramvalue.replace('\\', '\\\\').\ 229 replace('"', '\\"') + '"' 230 formatted_params.append("%s=%s" % (paramname, paramvalue)) 231 if len(formatted_params): 232 value = value + '; ' + '; '.join(formatted_params) 233 self._headers.append((header, value))
234
235 - def get_first(self, header, default=None):
236 """Get the first value of a named header. 237 238 Returns `default` if no values of the named header exist. 239 240 """ 241 header = _string_to_ascii(header, "header name").lower() 242 for key, value in self._headers: 243 if key.lower() == header: 244 return value 245 return default
246
247 - def get_all(self, header):
248 """Get all values of a named header. 249 250 Returns the values in the order in which they were added. 251 252 Returns an empty list if no values of the named header are present. 253 254 """ 255 header = _string_to_ascii(header, "header name").lower() 256 return [value for (key, value) in self._headers if key.lower() == header]
257
258 - def remove(self, header):
259 """Remove any occurrences of the named header. 260 261 The comparison of header names is performed case insensitively. 262 263 """ 264 self._headers = filter(lambda x: x[0].lower() != header.lower(), self._headers)
265
266 - def items(self):
267 """Get the list of headers. 268 269 This returns a list of tuple pairs, ``(header, value)``, one for each 270 header, in the order added. The strings in the tuples are byte 271 strings, encoded appropriately for HTTP transmission. 272 273 """ 274 return self._headers
275
276 - def __str__(self):
277 """Get a string representation of the headers. 278 279 """ 280 return str(self.items())
281
282 -class Response(object):
283 """Response object, used to return stuff via WSGI protocol. 284 285 The Response object is a container fr the details of the response. It 286 contains three significant members: 287 288 - status: The status code (as a string, with code and reason phrase) for 289 the reponse. 290 - headers: The headers to return about the request. 291 - body: The body of the page to return. 292 293 """
294 - def __init__(self, body=u'', status=200, content_type=u'text/plain'):
295 """Create a new Response object. 296 297 The body defaults to being empty, the status defaults to "200 OK", and 298 the content_type defaults to 'text/plain'. 299 300 - body: The value to store in the body member. Defaults to ''. 301 - status: The status to set for the response. May be specified as a 302 number, or as a string (optionally, with a reason phrase). Defaults 303 to 200. 304 - content_type: The content type to set for the response (as specified 305 for the set_content_type() method). Defaults to 'text/plain'. 306 307 """ 308 self.body = body 309 self.status = status 310 self.headers = Headers() 311 self.set_content_type(content_type)
312 313 VALID_STATUS_RE = re.compile(r'[12345][0-9][0-9]')
314 - def _set_status(self, status):
315 if isinstance(status, basestring): 316 if len(status) == 3: 317 pass # Fall through to string processing. 318 elif len(status) <= 4: 319 raise ValueError(u"Supplied status (%r) is not valid" % status) 320 elif status[3] == ' ': 321 if not Response.VALID_STATUS_RE.match(status[:3]): 322 raise ValueError(u"Supplied status (%r) is not valid" % 323 status) 324 self._status = _string_to_ascii(status, 'HTTP status line') 325 return 326 327 try: 328 statusint = int(status) 329 except ValueError: 330 raise ValueError(u"Supplied status (%r) is not a valid " 331 "status code" % status) 332 333 if statusint < 100 or statusint >= 600: 334 raise ValueError(u"Supplied status (%r) is not in valid range" % 335 status) 336 337 try: 338 self._status = reason_phrases.phrase_dict[statusint] 339 except KeyError: 340 raise ValueError(u"Supplied status (%r) is not known" % 341 status)
342
343 - def _get_status(self):
344 return self._status
345 status = property(_get_status, _set_status, doc= 346 """The status line to return. 347 348 This may be set to either a string or a number. If a string, it may 349 either contain only the status code, or may contain a reason phrase 350 following the status code (separated by a space). 351 352 If there is no reason phrase, or the status code is a number, an 353 appropriate reason phrase will be used, as long as the status code is 354 one of the standard HTTP 1.1 codes. For non-standard codes, the reason 355 phrase must be supplied. 356 357 If `status` is a unicode string, it must contain only characters which 358 can be encoded in the US-ASCII character set. Any other characters 359 will cause an exception to be thrown. 360 361 """) 362
363 - def set_content_type(self, content_type):
364 """Set the content type to return. 365 366 """ 367 self.headers.set(u'Content-Type', content_type)
368
369 - def __str__(self):
370 return "Response(%s, %s, %s)" % ( 371 self.status, 372 self.headers, 373 self.body 374 )
375
376 -class HTTPError(Exception, Response):
377 - def __init__(self, status=500, message=None):
378 Exception.__init__(self) 379 Response.__init__(self, status=status) 380 if message is None: 381 self.body = self.status 382 else: 383 self.body = self.status + "\n" + message
384
385 -class HTTPNotFound(HTTPError):
386 """Raise this exception if a requested resource is not found. 387 388 """
389 - def __init__(self, path=None):
390 self.path = path 391 HTTPError.__init__(self, 404)
392
393 -class HTTPMethodNotAllowed(HTTPError):
394 """Raise this exception if a method which is not allowed was used. 395 396 """
397 - def __init__(self, request_method, allowed_methods):
398 if method_known(request_method): 399 HTTPError.__init__(self, 405) 400 # Return the list of allowed methods in sorted order 401 allow = list(allowed_methods) 402 allow.sort() 403 self.headers.set(u'Allow', u', '.join(allow)) 404 else: 405 raise HTTPError(501, "Request method %s is not implemented" % 406 request_method)
407
408 -class HTTPServerError(HTTPError):
409 """Raise this exception if a server error occurs. 410 411 """
412 - def __init__(self, body):
413 HTTPError.__init__(self, 500, body)
414 415 # vim: set fileencoding=utf-8 : 416