Package wtf :: Package app :: Module response
[hide private]
[frames] | no frames]

Source Code for Module wtf.app.response

  1  # -*- coding: ascii -*- 
  2  # 
  3  # Copyright 2006-2012 
  4  # Andr\xe9 Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  Response object 
 19  =============== 
 20   
 21  This module implements a response object. 
 22  """ 
 23  __author__ = u"Andr\xe9 Malo" 
 24  __docformat__ = "restructuredtext en" 
 25   
 26  import datetime as _datetime 
 27  import weakref as _weakref 
 28   
 29  from wtf.app import http_response as http 
 30  from wtf import httputil as _httputil 
 31   
 32   
33 -class Done(http.HTTPResponse):
34 """ 35 Request Done exception 36 37 Use this is cases where return is not applicable (e.g. decorators). 38 """
39 40
41 -class HeaderCollection(object):
42 """ 43 Response header collection representation 44 45 Note that all header names are treated case insensitive (by lowering them) 46 47 :IVariables: 48 - `_headers`: The headers (``{'name': ['value', ...], ...}``) 49 50 :Types: 51 - `_headers`: ``dict`` 52 """ 53
54 - def __init__(self):
55 """ Initialization """ 56 self._headers = {}
57
58 - def __contains__(self, name):
59 """ 60 Check if header is already set 61 62 :Parameters: 63 - `name`: Header name 64 65 :Types: 66 - `name`: ``str`` 67 68 :return: Does the header already exist? 69 :rtype: ``bool`` 70 """ 71 return name.lower() in self._headers
72
73 - def __iter__(self):
74 """ Header tuple iterator """ 75 for name, values in self._headers.iteritems(): 76 for value in values: 77 yield (name, value)
78
79 - def get(self, name):
80 """ 81 Determine the value list of a header 82 83 :Parameters: 84 - `name`: The header name 85 86 :Types: 87 - `name`: ``str`` 88 89 :return: The value list or ``None`` 90 :rtype: ``list`` 91 """ 92 return self._headers.get(name.lower())
93
94 - def set(self, name, *values):
95 """ 96 Set a header, replacing any same-named header previously set 97 98 :Parameters: 99 - `name`: The header name 100 - `values`: List of values (``('value', ...)``) 101 102 :Types: 103 - `name`: ``str`` 104 - `values`: ``tuple`` 105 """ 106 self._headers[name.lower()] = list(values)
107
108 - def add(self, name, *values):
109 """ 110 Add a value list to a header 111 112 The old values are preserved 113 114 :Parameters: 115 - `name`: Header name 116 - `values`: Values to add (``('value', ...)``) 117 118 :Types: 119 - `name`: ``str`` 120 - `values`: ``tuple`` 121 """ 122 self._headers.setdefault(name.lower(), []).extend(list(values))
123
124 - def remove(self, name, value=None):
125 """ 126 Remove a header by name (plus optionally by value) 127 128 If the header does not exist alrady, it is not an error. 129 130 :Parameters: 131 - `name`: Header name 132 - `value`: Particular value to remove 133 134 :Types: 135 - `name`: ``str`` 136 - `value`: ``str`` 137 """ 138 name = name.lower() 139 if name in self._headers: 140 if value is None: 141 del self._headers[name] 142 else: 143 try: 144 while True: 145 self._headers[name].remove(value) 146 except ValueError: 147 pass
148 149
150 -class Response(object):
151 """ 152 Main response object 153 154 :IVariables: 155 - `write`: Response body writer. You are not forced to use it. In fact, 156 it's recommended to not use it but return an iterator over the 157 response body instead (as WSGI takes it) 158 - `headers`: Response header collection. The headers are flushed 159 autmatically before the fist `write` call or the returned iterable 160 is passed to the WSGI layer below. The headers can't be changed 161 anymore after that (the collection object will emit a warning 162 in case you attempt to do that) 163 - `_status`: Tuple of status and reason phrase (``(int, 'reason')``) 164 165 :Types: 166 - `write`: ``callable`` 167 - `headers`: `HeaderCollection` 168 - `_status`: ``tuple`` 169 """ 170 _status = None 171
172 - def __init__(self, request, start_response):
173 """ 174 Initialization 175 176 :Parameters: 177 - `request`: Request object 178 - `start_response`: WSGI start_response callable 179 180 :Types: 181 - `request`: `wtf.app.request.Request` 182 - `start_response`: ``callable`` 183 """ 184 self.request = _weakref.proxy(request) 185 self.http = http 186 self.status(200) 187 self.headers = HeaderCollection() 188 self.content_type('text/html') 189 def first_write(towrite): 190 """ First write flushes all """ 191 # ensure to do the right thing[tm] in case someone stored 192 # a reference to the write method 193 if self.write == first_write: 194 resp_code = "%03d %s" % self._status 195 headers = list(self.headers) 196 self.write = start_response(resp_code, headers) 197 return self.write(towrite)
198 self.write = first_write
199
200 - def __getattr__(self, name):
201 """ 202 Resolve unknown attributes 203 204 We're looking for special env variables inserted by the middleware 205 stack: ``wtf.response.<name>``. These are expected to be factories, 206 which are lazily initialized with the response object and return 207 the actual attribute, which is cached in the response object for 208 further use. 209 210 :Parameters: 211 - `name`: The name to look up 212 213 :Types: 214 - `name`: ``str`` 215 216 :return: The attribute in question 217 :rtype: any 218 219 :Exceptions: 220 - `AttributeError`: Attribute could not be resolved 221 """ 222 try: 223 factory = self.request.env['wtf.response.%s' % name] 224 except KeyError: 225 pass 226 else: 227 setattr(self, name, factory(_weakref.proxy(self))) 228 return super(Response, self).__getattribute__(name)
229
230 - def status(self, status=None, reason=None):
231 """ 232 Set/get response status 233 234 :Parameters: 235 - `status`: Response status code 236 - `reason`: Reason phrase 237 238 :Types: 239 - `status`: ``int`` 240 - `reason`: ``str`` 241 242 :return: Tuple of previous status and reason phrase 243 (``(int, 'reason')``) 244 :rtype: ``tuple`` 245 """ 246 oldstatus = self._status 247 if status is not None: 248 if reason is None: 249 reason = http.reasons.get(status) or "Status %d" % status 250 status = int(status), reason 251 elif reason is not None: 252 status = oldstatus[0], reason 253 self._status = status 254 return oldstatus
255
256 - def cookie(self, name, value, path='/', expires=None, max_age=None, 257 domain=None, secure=None, comment=None, version=None, 258 codec=None):
259 """ 260 Set response cookie 261 262 :Parameters: 263 - `name`: Cookie name 264 - `value`: Cookie value (if a codec is given, the type should be 265 applicable for the codec encoder). 266 - `path`: Valid URL base path for the cookie. It should always be set 267 to a reasonable path (at least ``/``), otherwise the cookie will 268 only be valid for the current URL and below. 269 - `expires`: Expire time of the cookie. If unset or ``None`` the 270 cookie is dropped when the browser is closed. See also the 271 `max_age` parameter. 272 - `max_age`: Max age of the cookie in seconds. If set, make sure it 273 matches the expiry time. The difference is that expires will be 274 transformed to a HTTP date, while max-age will stay an integer. 275 The expires parameter is the older one and better understood by 276 the clients out there. For that reason if you set max_age only, 277 expires will be set automatically to ``now + max_age``. If unset 278 or ``None`` the cookie will be dropped when the browser is closed. 279 - `domain`: Valid domain 280 - `secure`: Whether this is an SSL-only cookie or not 281 - `comment`: Cookie comment 282 - `version`: Cookie spec version. See `RFC 2965`_ 283 - `codec`: Cookie codec to apply. If unset or ``None``, the codec 284 specified in the application configuration is applied. 285 286 .. _RFC 2965: http://www.ietf.org/rfc/rfc2965.txt 287 288 :Types: 289 - `name`: ``str`` 290 - `value`: ``str`` 291 - `path`: ``str`` 292 - `expires`: ``datetime.datetime`` 293 - `max_age`: ``int`` 294 - `domain`: ``str`` 295 - `secure`: ``bool`` 296 - `comment`: ``str`` 297 - `version`: ``int`` 298 - `codec`: `CookieCodecInterface` 299 """ 300 # pylint: disable = R0913 301 302 if codec is None: 303 codec = self.request.env.get('wtf.codec.cookie') 304 cstring = _httputil.make_cookie(name, value, codec, 305 path=path, 306 expires=expires, 307 max_age=max_age, 308 domain=domain, 309 secure=secure, 310 comment=comment, 311 version=version 312 ) 313 self.headers.add('Set-Cookie', cstring)
314
315 - def content_type(self, ctype=None, charset=None):
316 """ 317 Set/get response content type 318 319 :Parameters: 320 - `ctype`: Content-Type 321 - `charset`: Charset 322 323 :Types: 324 - `ctype`: ``str`` 325 - `charset`: ``str`` 326 327 :return: Full previous content type header (maybe ``None``) 328 :rtype: ``str`` 329 """ 330 oldtype = (self.headers.get('content-type') or [None])[0] 331 if charset is not None: 332 charset = '"%s"' % charset.replace('"', '\\"') 333 if ctype is None: 334 ctype = oldtype or 'text/plain' 335 pos = ctype.find(';') 336 if pos > 0: 337 ctype = ctype[:pos] 338 ctype = ctype.strip() 339 ctype = "%s; charset=%s" % (ctype, charset) 340 if ctype is not None: 341 self.headers.set('content-type', ctype) 342 return oldtype
343
344 - def content_length(self, length):
345 """ 346 Add content length information 347 348 :Parameters: 349 - `length`: The expected length in octets 350 351 :Types: 352 - `length`: ``int`` 353 """ 354 self.headers.set('Content-Length', str(length))
355
356 - def last_modified(self, last_modified):
357 """ 358 Add last-modified information 359 360 :Parameters: 361 - `last_modified`: Last modification date (UTC) 362 363 :Types: 364 - `last_modified`: ``datetime.datetime`` 365 """ 366 self.headers.set('Last-Modified', _httputil.make_date(last_modified))
367
368 - def cache(self, expiry, audience=None):
369 """ 370 Add cache information 371 372 :Parameters: 373 - `expiry`: Expiry time in seconds from now 374 - `audience`: Caching audience; ``private`` or ``public`` 375 376 :Types: 377 - `expiry`: ``int`` 378 - `audience`: ``str`` 379 """ 380 expiry = max(0, expiry) 381 self.headers.set('Expires', _httputil.make_date( 382 _datetime.datetime.utcnow() + _datetime.timedelta(seconds=expiry) 383 )) 384 fields = ['max-age=%s' % expiry] 385 if audience in ('private', 'public'): 386 fields.append(audience) 387 self.headers.set('Cache-Control', ', '.join(fields)) 388 if expiry == 0: 389 self.headers.set('Pragma', 'no-cache')
390
391 - def raise_error(self, status, **param):
392 """ 393 Raise an HTTP error 394 395 :Parameters: 396 - `status`: Status code 397 - `param`: Additional parameters for the accompanying class in 398 `http_response`. The request object is passed automagically. 399 400 :Types: 401 - `status`: ``int`` 402 - `param`: ``dict`` 403 404 :Exceptions: 405 - `KeyError`: The status code is not available 406 - `HTTPResponse`: The requested HTTP exception 407 """ 408 cls = http.classes[status] 409 raise cls(self.request, **param)
410
411 - def raise_redirect(self, location, status=302):
412 """ 413 Raise HTTP redirect 414 415 :Parameters: 416 - `location`: URL to redirect to 417 - `status`: Response code 418 419 :Types: 420 - `location`: ``str`` 421 - `status`: ``int`` 422 423 :Exceptions: 424 - `http.HTTPRedirectResponse`: The redirect exception 425 """ 426 cls = http.classes[status] 427 assert issubclass(cls, http.HTTPRedirectResponse) 428 raise cls(self.request, location=location)
429
430 - def raise_basic_auth(self, realm, message=None):
431 """ 432 Raise a 401 error for HTTP Basic authentication 433 434 :Parameters: 435 - `realm`: The realm to authenticate 436 - `message`: Optional default overriding message 437 438 :Types: 439 - `realm`: ``str`` 440 - `message`: ``str`` 441 442 :Exceptions: 443 - `http.AuthorizationRequired`: The 401 exception 444 """ 445 self.raise_error(401, message=message, auth_type='Basic', realm=realm)
446