1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
55 """ Initialization """
56 self._headers = {}
57
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
74 """ Header tuple iterator """
75 for name, values in self._headers.iteritems():
76 for value in values:
77 yield (name, value)
78
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
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
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
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
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
192
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
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
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
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
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
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
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