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

Source Code for Module wsgiwebapi.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 overlaydict 
 29  import pathinfo 
 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.GET = cgi.parse_qs(environ.get('QUERY_STRING', '')) 70 self.POST = {} 71 72 if self.method == 'POST': 73 try: 74 content_length = int(environ.get('CONTENT_LENGTH', 0)) 75 except (ValueError, TypeError): 76 content_length = 0 77 if content_length > 0: 78 fd = environ['wsgi.input'] 79 buf = StringIO.StringIO() 80 while content_length > 0: 81 chunk = fd.read(min(content_length, 65536)) 82 if not chunk: 83 break 84 buf.write(chunk) 85 content_length -= len(chunk) 86 self.raw_post_data = buf.getvalue() 87 buf.close() 88 else: 89 self.raw_post_data = '' 90 self.POST = cgi.parse_qs(self.raw_post_data) 91 92 self.params = overlaydict.OverlayDict(self.POST, self.GET)
93
94 - def _set_handler_props(self, handler_props):
95 """Set the WSGIWebAPI properties found on the handler. 96 97 This is used to warn when the properties on the handler do not match 98 those used by the decorator - this is usually due to a second decorator 99 dropping the properties. 100 101 """ 102 self._handler_props = handler_props
103
104 - def _set_pathinfo(self, components):
105 """Set the path info to a list of components. 106 107 """ 108 self.pathinfo = pathinfo.PathInfo(components)
109
110 - def __str__(self):
111 return u"Request(%s, \"%s\", %r)" % ( 112 self.method, 113 self.path, 114 self.GET 115 )
116
117 -class WSGIResponse(object):
118 """Object satisfying the WSGI protocol for making a response. 119 120 This object should be passed the start_reponse parameter (as supplied by 121 the WSGI protocol), and the Response object for the response. The status 122 code, headers, and response body will be read from the Response object. 123 124 """
125 - def __init__(self, start_response, response):
126 self.start_response = start_response 127 self.response = response
128
129 - def __iter__(self):
130 self.start_response(self.response.status, 131 self.response.get_encoded_header_list()) 132 yield self.response.body
133
134 - def __len__(self):
135 return len(self.response.body)
136
137 -class Response(object):
138 """Response object, used to return stuff via WSGI protocol. 139 140 The Response object is a container fr the details of the response. It 141 contains three significant members: 142 143 - status: The status code (as a string, with code and reason phrase) for 144 the reponse. 145 - headers: The headers to return about the request. 146 - body: The body of the page to return. 147 148 """
149 - def __init__(self, body=u'', status=200, content_type=u'text/plain'):
150 """Create a new Response object. 151 152 The body defaults to being empty, the status defaults to "200 OK", and 153 the content_type defaults to 'text/plain'. 154 155 - body: The value to store in the body member. Defaults to ''. 156 - status: The status to set for the response. May be specified as a 157 number, or as a string (optionally, with a reason phrase). Defaults 158 to 200. 159 - content_type: The content type to set for the response (as specified 160 for the set_content_type() method). Defaults to 'text/plain'. 161 162 """ 163 self.body = body 164 self.status = status 165 self.headers = [] 166 self.set_content_type(content_type)
167
168 - def get_encoded_header_list(self):
169 """Get the list of headers, encoded suitably for HTTP. 170 171 This converts the unicode headers to byte strings, suitable for passing 172 through HTTP. 173 174 """ 175 result = [] 176 for header, value in self.headers: 177 if not isinstance(header, str): 178 header = header.encode('iso-8859-1') 179 if not isinstance(value, str): 180 # FIXME - if the header value is not valid iso-8859-1, encode 181 # it according to rfc 2231. 182 # See: email.utils.encode_rfc2231 183 value = value.encode('iso-8859-1') 184 result.append((header, value)) 185 return result
186 187 VALID_STATUS_RE = re.compile(r'[12345][0-9][0-9]')
188 - def _set_status(self, status):
189 if isinstance(status, basestring): 190 if len(status) == 3: 191 pass # Fall through to string processing. 192 elif len(status) <= 4: 193 raise ValueError(u"Supplied status (%r) is not valid" % status) 194 elif status[3] == ' ': 195 if not Response.VALID_STATUS_RE.match(status[:3]): 196 raise ValueError(u"Supplied status (%r) is not valid" % 197 status) 198 if isinstance(status, unicode): 199 try: 200 self._status = status.encode('us-ascii') 201 except UnicodeError, e: 202 e.reason += ", HTTP status line must be " \ 203 "encodable as US-ASCII" 204 raise 205 else: 206 self._status = status 207 return 208 209 try: 210 statusint = int(status) 211 except ValueError: 212 raise ValueError(u"Supplied status (%r) is not a valid " 213 "status code" % status) 214 215 if statusint < 100 or statusint >= 600: 216 raise ValueError(u"Supplied status (%r) is not in valid range" % 217 status) 218 219 try: 220 self._status = reason_phrases.phrase_dict[statusint] 221 except KeyError: 222 raise ValueError(u"Supplied status (%r) is not known" % 223 status)
224
225 - def _get_status(self):
226 return self._status
227 status = property(_get_status, _set_status, doc= 228 """The status line to return. 229 230 This may be set to either a string or a number. If a string, it may 231 either contain only the status code, or may contain a reason phrase 232 following the status code (separated by a space). 233 234 If there is no reason phrase, or the status code is a number, an 235 appropriate reason phrase will be used, as long as the status code is 236 one of the standard HTTP 1.1 codes. For non-standard codes, the reason 237 phrase must be supplied. 238 239 If `status` is a unicode string, it must contain only characters which 240 can be encoded in the US-ASCII character set. Any other characters 241 will cause an exception to be thrown. 242 243 """) 244
245 - def set_unique_header(self, header, value):
246 """Set the value of a header which should only occur once. 247 248 If any values for the header already exist in the list of headers, they 249 are first removed. The comparison of header names is performed case 250 insensitively. 251 252 """ 253 self.headers = filter(lambda x: x[0].lower() != header.lower(), self.headers) 254 self.headers.append((header, value))
255
256 - def set_content_type(self, content_type):
257 """Set the content type to return. 258 259 """ 260 self.set_unique_header(u'Content-Type', content_type)
261
262 - def __str__(self):
263 return "Response(%s, %s, %s)" % ( 264 self.status, 265 self.headers, 266 self.body 267 )
268
269 -class HTTPError(Exception, Response):
270 - def __init__(self, status=500, message=None):
271 Exception.__init__(self) 272 Response.__init__(self, status=status) 273 if message is None: 274 self.body = self.status 275 else: 276 self.body = self.status + "\n" + message
277
278 -class HTTPNotFound(HTTPError):
279 """Raise this exception if a requested resource is not found. 280 281 """
282 - def __init__(self, path):
283 HTTPError.__init__(self, 404, 'Path \'%s\' not found' % path)
284
285 -class HTTPMethodNotAllowed(HTTPError):
286 """Raise this exception if a method which is not allowed was used. 287 288 """
289 - def __init__(self, request_method, allowed_methods):
290 if method_known(request_method): 291 HTTPError.__init__(self, 405) 292 # Return the list of allowed methods in sorted order 293 allow = list(allowed_methods) 294 allow.sort() 295 self.set_unique_header(u'Allow', u', '.join(allow)) 296 else: 297 raise HTTPError(501, "Request method %s is not implemented" % 298 request_method)
299
300 -class HTTPServerError(HTTPError):
301 """Raise this exception if a server error occurs. 302 303 """
304 - def __init__(self, body):
305 HTTPError.__init__(self, 500, body)
306 307 # vim: set fileencoding=utf-8 : 308