blob: dffd426b3101c68a466d09bf26bf994f54b27c3d [file] [log] [blame]
Chris Craikb2cbf152015-07-28 16:26:29 -07001WebOb Reference
2+++++++++++++++
3
4.. contents::
5
6.. comment:
7
8 >>> from doctest import ELLIPSIS
9
10Introduction
11============
12
13This document covers all the details of the Request and Response
14objects. It is written to be testable with `doctest
15<http://python.org/doc/current/lib/module-doctest.html>`_ -- this
16affects the flavor of the documentation, perhaps to its detriment.
17But it also means you can feel confident that the documentation is
18correct.
19
20This is a somewhat different approach to reference documentation
21compared to the extracted documentation for the `request
22<class-webob.Request.html>`_ and `response
23<class-webob.Response.html>`_.
24
25Request
26=======
27
28The primary object in WebOb is ``webob.Request``, a wrapper around a
29`WSGI environment <http://www.python.org/dev/peps/pep-0333/>`_.
30
31The basic way you create a request object is simple enough:
32
33.. code-block:: python
34
35 >>> from webob import Request
36 >>> environ = {'wsgi.url_scheme': 'http', ...} #doctest: +SKIP
37 >>> req = Request(environ) #doctest: +SKIP
38
39(Note that the WSGI environment is a dictionary with a dozen required
40keys, so it's a bit lengthly to show a complete example of what it
41would look like -- usually your WSGI server will create it.)
42
43The request object *wraps* the environment; it has very little
44internal state of its own. Instead attributes you access read and
45write to the environment dictionary.
46
47You don't have to understand the details of WSGI to use this library;
48this library handles those details for you. You also don't have to
49use this exclusively of other libraries. If those other libraries
50also keep their state in the environment, multiple wrappers can
51coexist. Examples of libraries that can coexist include
52`paste.wsgiwrappers.Request
53<http://pythonpaste.org/class-paste.wsgiwrappers.WSGIRequest.html>`_
54(used by Pylons) and `yaro.Request
55<http://lukearno.com/projects/yaro/>`_.
56
57The WSGI environment has a number of required variables. To make it
58easier to test and play around with, the ``Request`` class has a
59constructor that will fill in a minimal environment:
60
61.. code-block:: python
62
63 >>> req = Request.blank('/article?id=1')
64 >>> from pprint import pprint
65 >>> pprint(req.environ)
66 {'HTTP_HOST': 'localhost:80',
67 'PATH_INFO': '/article',
68 'QUERY_STRING': 'id=1',
69 'REQUEST_METHOD': 'GET',
70 'SCRIPT_NAME': '',
71 'SERVER_NAME': 'localhost',
72 'SERVER_PORT': '80',
73 'SERVER_PROTOCOL': 'HTTP/1.0',
74 'wsgi.errors': <open file '<stderr>', mode 'w' at ...>,
75 'wsgi.input': <...IO... object at ...>,
76 'wsgi.multiprocess': False,
77 'wsgi.multithread': False,
78 'wsgi.run_once': False,
79 'wsgi.url_scheme': 'http',
80 'wsgi.version': (1, 0)}
81
82Request Body
83------------
84
85``req.body`` is a file-like object that gives the body of the request
86(e.g., a POST form, the body of a PUT, etc). It's kind of boring to
87start, but you can set it to a string and that will be turned into a
88file-like object. You can read the entire body with
89``req.body``.
90
91.. code-block:: python
92
93 >>> hasattr(req.body_file, 'read')
94 True
95 >>> req.body
96 ''
97 >>> req.method = 'PUT'
98 >>> req.body = 'test'
99 >>> hasattr(req.body_file, 'read')
100 True
101 >>> req.body
102 'test'
103
104Method & URL
105------------
106
107All the normal parts of a request are also accessible through the
108request object:
109
110.. code-block:: python
111
112 >>> req.method
113 'PUT'
114 >>> req.scheme
115 'http'
116 >>> req.script_name # The base of the URL
117 ''
118 >>> req.script_name = '/blog' # make it more interesting
119 >>> req.path_info # The yet-to-be-consumed part of the URL
120 '/article'
121 >>> req.content_type # Content-Type of the request body
122 ''
123 >>> print req.remote_user # The authenticated user (there is none set)
124 None
125 >>> print req.remote_addr # The remote IP
126 None
127 >>> req.host
128 'localhost:80'
129 >>> req.host_url
130 'http://localhost'
131 >>> req.application_url
132 'http://localhost/blog'
133 >>> req.path_url
134 'http://localhost/blog/article'
135 >>> req.url
136 'http://localhost/blog/article?id=1'
137 >>> req.path
138 '/blog/article'
139 >>> req.path_qs
140 '/blog/article?id=1'
141 >>> req.query_string
142 'id=1'
143
144You can make new URLs:
145
146.. code-block:: python
147
148 >>> req.relative_url('archive')
149 'http://localhost/blog/archive'
150
151For parsing the URLs, it is often useful to deal with just the next
152path segment on PATH_INFO:
153
154.. code-block:: python
155
156 >>> req.path_info_peek() # Doesn't change request
157 'article'
158 >>> req.path_info_pop() # Does change request!
159 'article'
160 >>> req.script_name
161 '/blog/article'
162 >>> req.path_info
163 ''
164
165Headers
166-------
167
168All request headers are available through a dictionary-like object
169``req.headers``. Keys are case-insensitive.
170
171.. code-block:: python
172
173 >>> req.headers['Content-Type'] = 'application/x-www-urlencoded'
174 >>> sorted(req.headers.items())
175 [('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80')]
176 >>> req.environ['CONTENT_TYPE']
177 'application/x-www-urlencoded'
178
179Query & POST variables
180----------------------
181
182Requests can have variables in one of two locations: the query string
183(``?id=1``), or in the body of the request (generally a POST form).
184Note that even POST requests can have a query string, so both kinds of
185variables can exist at the same time. Also, a variable can show up
186more than once, as in ``?check=a&check=b``.
187
188For these variables WebOb uses a `MultiDict
189<class-webob.multidict.MultiDict.html>`_, which is basically a
190dictionary wrapper on a list of key/value pairs. It looks like a
191single-valued dictionary, but you can access all the values of a key
192with ``.getall(key)`` (which always returns a list, possibly an empty
193list). You also get all key/value pairs when using ``.items()`` and
194all values with ``.values()``.
195
196Some examples:
197
198.. code-block:: python
199
200 >>> req = Request.blank('/test?check=a&check=b&name=Bob')
201 >>> req.GET
202 MultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')])
203 >>> req.GET['check']
204 u'b'
205 >>> req.GET.getall('check')
206 [u'a', u'b']
207 >>> req.GET.items()
208 [(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')]
209
210We'll have to create a request body and change the method to get
211POST. Until we do that, the variables are boring:
212
213.. code-block:: python
214
215 >>> req.POST
216 <NoVars: Not a form request>
217 >>> req.POST.items() # NoVars can be read like a dict, but not written
218 []
219 >>> req.method = 'POST'
220 >>> req.body = 'name=Joe&email=joe@example.com'
221 >>> req.POST
222 MultiDict([(u'name', u'Joe'), (u'email', u'joe@example.com')])
223 >>> req.POST['name']
224 u'Joe'
225
226Often you won't care where the variables come from. (Even if you care
227about the method, the location of the variables might not be
228important.) There is a dictionary called ``req.params`` that
229contains variables from both sources:
230
231.. code-block:: python
232
233 >>> req.params
234 NestedMultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob'), (u'name', u'Joe'), (u'email', u'joe@example.com')])
235 >>> req.params['name']
236 u'Bob'
237 >>> req.params.getall('name')
238 [u'Bob', u'Joe']
239 >>> for name, value in req.params.items():
240 ... print '%s: %r' % (name, value)
241 check: u'a'
242 check: u'b'
243 name: u'Bob'
244 name: u'Joe'
245 email: u'joe@example.com'
246
247The ``POST`` and ``GET`` nomenclature is historical -- ``req.GET`` can
248be used for non-GET requests to access query parameters, and
249``req.POST`` can also be used for PUT requests with the appropriate
250Content-Type.
251
252 >>> req = Request.blank('/test?check=a&check=b&name=Bob')
253 >>> req.method = 'PUT'
254 >>> req.body = body = 'var1=value1&var2=value2&rep=1&rep=2'
255 >>> req.environ['CONTENT_LENGTH'] = str(len(req.body))
256 >>> req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
257 >>> req.GET
258 MultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')])
259 >>> req.POST
260 MultiDict([(u'var1', u'value1'), (u'var2', u'value2'), (u'rep', u'1'), (u'rep', u'2')])
261
262Unicode Variables
263~~~~~~~~~~~~~~~~~
264
265Submissions are non-unicode (``str``) strings, unless some character
266set is indicated. A client can indicate the character set with
267``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but
268very few clients actually do this (sometimes XMLHttpRequest requests
269will do this, as JSON is always UTF8 even when a page is served with a
270different character set). You can force a charset, which will affect
271all the variables:
272
273.. code-block:: python
274
275 >>> req.charset = 'utf8'
276 >>> req.GET
277 MultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')])
278
279Cookies
280-------
281
282Cookies are presented in a simple dictionary. Like other variables,
283they will be decoded into Unicode strings if you set the charset.
284
285.. code-block:: python
286
287 >>> req.headers['Cookie'] = 'test=value'
288 >>> req.cookies
289 MultiDict([(u'test', u'value')])
290
291Modifying the request
292---------------------
293
294The headers are all modifiable, as are other environmental variables
295(like ``req.remote_user``, which maps to
296``request.environ['REMOTE_USER']``).
297
298If you want to copy the request you can use ``req.copy()``; this
299copies the ``environ`` dictionary, and the request body from
300``environ['wsgi.input']``.
301
302The method ``req.remove_conditional_headers(remove_encoding=True)``
303can be used to remove headers that might result in a ``304 Not
304Modified`` response. If you are writing some intermediary it can be
305useful to avoid these headers. Also if ``remove_encoding`` is true
306(the default) then any ``Accept-Encoding`` header will be removed,
307which can result in gzipped responses.
308
309Header Getters
310--------------
311
312In addition to ``req.headers``, there are attributes for most of the
313request headers defined by the HTTP 1.1 specification. These
314attributes often return parsed forms of the headers.
315
316Accept-* headers
317~~~~~~~~~~~~~~~~
318
319There are several request headers that tell the server what the client
320accepts. These are ``accept`` (the Content-Type that is accepted),
321``accept_charset`` (the charset accepted), ``accept_encoding``
322(the Content-Encoding, like gzip, that is accepted), and
323``accept_language`` (generally the preferred language of the client).
324
325The objects returned support containment to test for acceptability.
326E.g.:
327
328.. code-block:: python
329
330 >>> 'text/html' in req.accept
331 True
332
333Because no header means anything is potentially acceptable, this is
334returning True. We can set it to see more interesting behavior (the
335example means that ``text/html`` is okay, but
336``application/xhtml+xml`` is preferred):
337
338.. code-block:: python
339
340 >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1'
341 >>> req.accept
342 <MIMEAccept('text/html;q=0.5, application/xhtml+xml')>
343 >>> 'text/html' in req.accept
344 True
345
346There are a few methods for different strategies of finding a match.
347
348.. code-block:: python
349
350 >>> req.accept.best_match(['text/html', 'application/xhtml+xml'])
351 'application/xhtml+xml'
352
353If we just want to know everything the client prefers, in the order it
354is preferred:
355
356.. code-block:: python
357
358 >>> list(req.accept)
359 ['application/xhtml+xml', 'text/html']
360
361For languages you'll often have a "fallback" language. E.g., if there's
362nothing better then use ``en-US`` (and if ``en-US`` is okay, ignore
363any less preferrable languages):
364
365.. code-block:: python
366
367 >>> req.accept_language = 'es, pt-BR'
368 >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US')
369 'en-US'
370 >>> req.accept_language.best_match(['es', 'en-US'], default_match='en-US')
371 'es'
372
373Your fallback language must appear both in the ``offers`` and as the
374``default_match`` to insure that it is returned as a best match if the
375client specified a preference for it.
376
377.. code-block:: python
378
379 >>> req.accept_language = 'en-US;q=0.5, en-GB;q=0.2'
380 >>> req.accept_language.best_match(['en-GB'], default_match='en-US')
381 'en-GB'
382 >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US')
383 'en-US'
384
385Conditional Requests
386~~~~~~~~~~~~~~~~~~~~
387
388There a number of ways to make a conditional request. A conditional
389request is made when the client has a document, but it is not sure if
390the document is up to date. If it is not, it wants a new version. If
391the document is up to date then it doesn't want to waste the
392bandwidth, and expects a ``304 Not Modified`` response.
393
394ETags are generally the best technique for these kinds of requests;
395this is an opaque string that indicates the identity of the object.
396For instance, it's common to use the mtime (last modified) of the file,
397plus the number of bytes, and maybe a hash of the filename (if there's
398a possibility that the same URL could point to two different
399server-side filenames based on other variables). To test if a 304
400response is appropriate, you can use:
401
402.. code-block:: python
403
404 >>> server_token = 'opaque-token'
405 >>> server_token in req.if_none_match # You shouldn't return 304
406 False
407 >>> req.if_none_match = server_token
408 >>> req.if_none_match
409 <ETag opaque-token>
410 >>> server_token in req.if_none_match # You *should* return 304
411 True
412
413For date-based comparisons If-Modified-Since is used:
414
415.. code-block:: python
416
417 >>> from webob import UTC
418 >>> from datetime import datetime
419 >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
420 >>> req.headers['If-Modified-Since']
421 'Sun, 01 Jan 2006 12:00:00 GMT'
422 >>> server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC)
423 >>> req.if_modified_since and req.if_modified_since >= server_modified
424 True
425
426For range requests there are two important headers, If-Range (which is
427form of conditional request) and Range (which requests a range). If
428the If-Range header fails to match then the full response (not a
429range) should be returned:
430
431.. code-block:: python
432
433 >>> req.if_range
434 <Empty If-Range>
435 >>> req.if_range.match(etag='some-etag', last_modified=datetime(2005, 1, 1, 12, 0))
436 True
437 >>> req.if_range = 'opaque-etag'
438 >>> req.if_range.match(etag='other-etag')
439 False
440 >>> req.if_range.match(etag='opaque-etag')
441 True
442
443You can also pass in a response object with:
444
445.. code-block:: python
446
447 >>> from webob import Response
448 >>> res = Response(etag='opaque-etag')
449 >>> req.if_range.match_response(res)
450 True
451
452To get the range information:
453
454 >>> req.range = 'bytes=0-100'
455 >>> req.range
456 <Range ranges=(0, 101)>
457 >>> cr = req.range.content_range(length=1000)
458 >>> cr.start, cr.stop, cr.length
459 (0, 101, 1000)
460
461Note that the range headers use *inclusive* ranges (the last byte
462indexed is included), where Python always uses a range where the last
463index is excluded from the range. The ``.stop`` index is in the
464Python form.
465
466Another kind of conditional request is a request (typically PUT) that
467includes If-Match or If-Unmodified-Since. In this case you are saying
468"here is an update to a resource, but don't apply it if someone else
469has done something since I last got the resource". If-Match means "do
470this if the current ETag matches the ETag I'm giving".
471If-Unmodified-Since means "do this if the resource has remained
472unchanged".
473
474.. code-block:: python
475
476 >>> server_token in req.if_match # No If-Match means everything is ok
477 True
478 >>> req.if_match = server_token
479 >>> server_token in req.if_match # Still OK
480 True
481 >>> req.if_match = 'other-token'
482 >>> # Not OK, should return 412 Precondition Failed:
483 >>> server_token in req.if_match
484 False
485
486For more on this kind of conditional request, see `Detecting the Lost
487Update Problem Using Unreserved Checkout
488<http://www.w3.org/1999/04/Editing/>`_.
489
490Calling WSGI Applications
491-------------------------
492
493The request object can be used to make handy subrequests or test
494requests against WSGI applications. If you want to make subrequests,
495you should copy the request (with ``req.copy()``) before sending it to
496multiple applications, since applications might modify the request
497when they are run.
498
499There's two forms of the subrequest. The more primitive form is
500this:
501
502.. code-block:: python
503
504 >>> req = Request.blank('/')
505 >>> def wsgi_app(environ, start_response):
506 ... start_response('200 OK', [('Content-type', 'text/plain')])
507 ... return ['Hi!']
508 >>> req.call_application(wsgi_app)
509 ('200 OK', [('Content-type', 'text/plain')], ['Hi!'])
510
511Note it returns ``(status_string, header_list, app_iter)``. If
512``app_iter.close()`` exists, it is your responsibility to call it.
513
514A handier response can be had with:
515
516.. code-block:: python
517
518 >>> res = req.get_response(wsgi_app)
519 >>> res
520 <Response ... 200 OK>
521 >>> res.status
522 '200 OK'
523 >>> res.headers
524 ResponseHeaders([('Content-type', 'text/plain')])
525 >>> res.body
526 'Hi!'
527
528You can learn more about this response object in the Response_ section.
529
530Ad-Hoc Attributes
531-----------------
532
533You can assign attributes to your request objects. They will all go
534in ``environ['webob.adhoc_attrs']`` (a dictionary).
535
536.. code-block:: python
537
538 >>> req = Request.blank('/')
539 >>> req.some_attr = 'blah blah blah'
540 >>> new_req = Request(req.environ)
541 >>> new_req.some_attr
542 'blah blah blah'
543 >>> req.environ['webob.adhoc_attrs']
544 {'some_attr': 'blah blah blah'}
545
546Response
547========
548
549The ``webob.Response`` object contains everything necessary to make a
550WSGI response. Instances of it are in fact WSGI applications, but it
551can also represent the result of calling a WSGI application (as noted
552in `Calling WSGI Applications`_). It can also be a way of
553accumulating a response in your WSGI application.
554
555A WSGI response is made up of a status (like ``200 OK``), a list of
556headers, and a body (or iterator that will produce a body).
557
558Core Attributes
559---------------
560
561The core attributes are unsurprising:
562
563.. code-block:: python
564
565 >>> from webob import Response
566 >>> res = Response()
567 >>> res.status
568 '200 OK'
569 >>> res.headerlist
570 [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')]
571 >>> res.body
572 ''
573
574You can set any of these attributes, e.g.:
575
576.. code-block:: python
577
578 >>> res.status = 404
579 >>> res.status
580 '404 Not Found'
581 >>> res.status_code
582 404
583 >>> res.headerlist = [('Content-type', 'text/html')]
584 >>> res.body = 'test'
585 >>> print res
586 404 Not Found
587 Content-type: text/html
588 Content-Length: 4
589 <BLANKLINE>
590 test
591 >>> res.body = u"test"
592 Traceback (most recent call last):
593 ...
594 TypeError: You cannot set Response.body to a unicode object (use Response.text)
595 >>> res.text = u"test"
596 Traceback (most recent call last):
597 ...
598 AttributeError: You cannot access Response.text unless charset is set
599 >>> res.charset = 'utf8'
600 >>> res.text = u"test"
601 >>> res.body
602 'test'
603
604You can set any attribute with the constructor, like
605``Response(charset='utf8')``
606
607Headers
608-------
609
610In addition to ``res.headerlist``, there is dictionary-like view on
611the list in ``res.headers``:
612
613.. code-block:: python
614
615 >>> res.headers
616 ResponseHeaders([('Content-Type', 'text/html; charset=utf8'), ('Content-Length', '4')])
617
618This is case-insensitive. It can support multiple values for a key,
619though only if you use ``res.headers.add(key, value)`` or read them
620with ``res.headers.getall(key)``.
621
622Body & app_iter
623---------------
624
625The ``res.body`` attribute represents the entire body of the request
626as a single string (not unicode, though you can set it to unicode if
627you have a charset defined). There is also a ``res.app_iter``
628attribute that reprsents the body as an iterator. WSGI applications
629return these ``app_iter`` iterators instead of strings, and sometimes
630it can be problematic to load the entire iterator at once (for
631instance, if it returns the contents of a very large file). Generally
632it is not a problem, and often the iterator is something simple like a
633one-item list containing a string with the entire body.
634
635If you set the body then Content-Length will also be set, and an
636``res.app_iter`` will be created for you. If you set ``res.app_iter``
637then Content-Length will be cleared, but it won't be set for you.
638
639There is also a file-like object you can access, which will update the
640app_iter in-place (turning the app_iter into a list if necessary):
641
642.. code-block:: python
643
644 >>> res = Response(content_type='text/plain', charset=None)
645 >>> f = res.body_file
646 >>> f.write('hey')
647 >>> f.write(u'test')
648 Traceback (most recent call last):
649 . . .
650 TypeError: You can only write unicode to Response if charset has been set
651 >>> f.encoding
652 >>> res.charset = 'utf8'
653 >>> f.encoding
654 'utf8'
655 >>> f.write(u'test')
656 >>> res.app_iter
657 ['', 'hey', 'test']
658 >>> res.body
659 'heytest'
660
661Header Getters
662--------------
663
664Like Request, HTTP response headers are also available as individual
665properties. These represent parsed forms of the headers.
666
667Content-Type is a special case, as the type and the charset are
668handled through two separate properties:
669
670.. code-block:: python
671
672 >>> res = Response()
673 >>> res.content_type = 'text/html'
674 >>> res.charset = 'utf8'
675 >>> res.content_type
676 'text/html'
677 >>> res.headers['content-type']
678 'text/html; charset=utf8'
679 >>> res.content_type = 'application/atom+xml'
680 >>> res.content_type_params
681 {'charset': 'utf8'}
682 >>> res.content_type_params = {'type': 'entry', 'charset': 'utf8'}
683 >>> res.headers['content-type']
684 'application/atom+xml; charset=utf8; type=entry'
685
686Other headers:
687
688.. code-block:: python
689
690 >>> # Used with a redirect:
691 >>> res.location = 'http://localhost/foo'
692
693 >>> # Indicates that the server accepts Range requests:
694 >>> res.accept_ranges = 'bytes'
695
696 >>> # Used by caching proxies to tell the client how old the
697 >>> # response is:
698 >>> res.age = 120
699
700 >>> # Show what methods the client can do; typically used in
701 >>> # a 405 Method Not Allowed response:
702 >>> res.allow = ['GET', 'PUT']
703
704 >>> # Set the cache-control header:
705 >>> res.cache_control.max_age = 360
706 >>> res.cache_control.no_transform = True
707
708 >>> # Tell the browser to treat the response as an attachment:
709 >>> res.content_disposition = 'attachment; filename=foo.xml'
710
711 >>> # Used if you had gzipped the body:
712 >>> res.content_encoding = 'gzip'
713
714 >>> # What language(s) are in the content:
715 >>> res.content_language = ['en']
716
717 >>> # Seldom used header that tells the client where the content
718 >>> # is from:
719 >>> res.content_location = 'http://localhost/foo'
720
721 >>> # Seldom used header that gives a hash of the body:
722 >>> res.content_md5 = 'big-hash'
723
724 >>> # Means we are serving bytes 0-500 inclusive, out of 1000 bytes total:
725 >>> # you can also use the range setter shown earlier
726 >>> res.content_range = (0, 501, 1000)
727
728 >>> # The length of the content; set automatically if you set
729 >>> # res.body:
730 >>> res.content_length = 4
731
732 >>> # Used to indicate the current date as the server understands
733 >>> # it:
734 >>> res.date = datetime.now()
735
736 >>> # The etag:
737 >>> res.etag = 'opaque-token'
738 >>> # You can generate it from the body too:
739 >>> res.md5_etag()
740 >>> res.etag
741 '1B2M2Y8AsgTpgAmY7PhCfg'
742
743 >>> # When this page should expire from a cache (Cache-Control
744 >>> # often works better):
745 >>> import time
746 >>> res.expires = time.time() + 60*60 # 1 hour
747
748 >>> # When this was last modified, of course:
749 >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC)
750
751 >>> # Used with 503 Service Unavailable to hint the client when to
752 >>> # try again:
753 >>> res.retry_after = 160
754
755 >>> # Indicate the server software:
756 >>> res.server = 'WebOb/1.0'
757
758 >>> # Give a list of headers that the cache should vary on:
759 >>> res.vary = ['Cookie']
760
761Note in each case you can general set the header to a string to avoid
762any parsing, and set it to None to remove the header (or do something
763like ``del res.vary``).
764
765In the case of date-related headers you can set the value to a
766``datetime`` instance (ideally with a UTC timezone), a time tuple, an
767integer timestamp, or a properly-formatted string.
768
769After setting all these headers, here's the result:
770
771.. code-block:: python
772
773 >>> for name, value in res.headerlist:
774 ... print '%s: %s' % (name, value)
775 Content-Type: application/atom+xml; charset=utf8; type=entry
776 Location: http://localhost/foo
777 Accept-Ranges: bytes
778 Age: 120
779 Allow: GET, PUT
780 Cache-Control: max-age=360, no-transform
781 Content-Disposition: attachment; filename=foo.xml
782 Content-Encoding: gzip
783 Content-Language: en
784 Content-Location: http://localhost/foo
785 Content-MD5: big-hash
786 Content-Range: bytes 0-500/1000
787 Content-Length: 4
788 Date: ... GMT
789 ETag: ...
790 Expires: ... GMT
791 Last-Modified: Mon, 01 Jan 2007 12:00:00 GMT
792 Retry-After: 160
793 Server: WebOb/1.0
794 Vary: Cookie
795
796You can also set Cache-Control related attributes with
797``req.cache_expires(seconds, **attrs)``, like:
798
799.. code-block:: python
800
801 >>> res = Response()
802 >>> res.cache_expires(10)
803 >>> res.headers['Cache-Control']
804 'max-age=10'
805 >>> res.cache_expires(0)
806 >>> res.headers['Cache-Control']
807 'max-age=0, must-revalidate, no-cache, no-store'
808 >>> res.headers['Expires']
809 '... GMT'
810
811You can also use the `timedelta
812<http://python.org/doc/current/lib/datetime-timedelta.html>`_
813constants defined, e.g.:
814
815.. code-block:: python
816
817 >>> from webob import *
818 >>> res = Response()
819 >>> res.cache_expires(2*day+4*hour)
820 >>> res.headers['Cache-Control']
821 'max-age=187200'
822
823Cookies
824-------
825
826Cookies (and the Set-Cookie header) are handled with a couple
827methods. Most importantly:
828
829.. code-block:: python
830
831 >>> res.set_cookie('key', 'value', max_age=360, path='/',
832 ... domain='example.org', secure=True)
833 >>> res.headers['Set-Cookie']
834 'key=value; Domain=example.org; Max-Age=360; Path=/; expires=... GMT; secure'
835 >>> # To delete a cookie previously set in the client:
836 >>> res.delete_cookie('bad_cookie')
837 >>> res.headers['Set-Cookie']
838 'bad_cookie=; Max-Age=0; Path=/; expires=... GMT'
839
840The only other real method of note (note that this does *not* delete
841the cookie from clients, only from the response object):
842
843.. code-block:: python
844
845 >>> res.unset_cookie('key')
846 >>> res.unset_cookie('bad_cookie')
847 >>> print res.headers.get('Set-Cookie')
848 None
849
850Binding a Request
851-----------------
852
853You can bind a request (or request WSGI environ) to the response
854object. This is available through ``res.request`` or
855``res.environ``. This is currently only used in setting
856``res.location``, to make the location absolute if necessary.
857
858Response as a WSGI application
859------------------------------
860
861A response is a WSGI application, in that you can do:
862
863.. code-block:: python
864
865 >>> req = Request.blank('/')
866 >>> status, headers, app_iter = req.call_application(res)
867
868A possible pattern for your application might be:
869
870.. code-block:: python
871
872 >>> def my_app(environ, start_response):
873 ... req = Request(environ)
874 ... res = Response()
875 ... res.content_type = 'text/plain'
876 ... parts = []
877 ... for name, value in sorted(req.environ.items()):
878 ... parts.append('%s: %r' % (name, value))
879 ... res.body = '\n'.join(parts)
880 ... return res(environ, start_response)
881 >>> req = Request.blank('/')
882 >>> res = req.get_response(my_app)
883 >>> print res
884 200 OK
885 Content-Type: text/plain; charset=UTF-8
886 Content-Length: ...
887 <BLANKLINE>
888 HTTP_HOST: 'localhost:80'
889 PATH_INFO: '/'
890 QUERY_STRING: ''
891 REQUEST_METHOD: 'GET'
892 SCRIPT_NAME: ''
893 SERVER_NAME: 'localhost'
894 SERVER_PORT: '80'
895 SERVER_PROTOCOL: 'HTTP/1.0'
896 wsgi.errors: <open file '<stderr>', mode 'w' at ...>
897 wsgi.input: <...IO... object at ...>
898 wsgi.multiprocess: False
899 wsgi.multithread: False
900 wsgi.run_once: False
901 wsgi.url_scheme: 'http'
902 wsgi.version: (1, 0)
903
904Exceptions
905==========
906
907In addition to Request and Response objects, there are a set of Python
908exceptions for different HTTP responses (3xx, 4xx, 5xx codes).
909
910These provide a simple way to provide these non-200 response. A very
911simple body is provided.
912
913.. code-block:: python
914
915 >>> from webob.exc import *
916 >>> exc = HTTPTemporaryRedirect(location='foo')
917 >>> req = Request.blank('/path/to/something')
918 >>> print str(req.get_response(exc)).strip()
919 307 Temporary Redirect
920 Location: http://localhost/path/to/foo
921 Content-Length: 126
922 Content-Type: text/plain; charset=UTF-8
923 <BLANKLINE>
924 307 Temporary Redirect
925 <BLANKLINE>
926 The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically.
927
928Note that only if there's an ``Accept: text/html`` header in the
929request will an HTML response be given:
930
931.. code-block:: python
932
933 >>> req.accept += 'text/html'
934 >>> print str(req.get_response(exc)).strip()
935 307 Temporary Redirect
936 Location: http://localhost/path/to/foo
937 Content-Length: 270
938 Content-Type: text/html; charset=UTF-8
939 <BLANKLINE>
940 <html>
941 <head>
942 <title>307 Temporary Redirect</title>
943 </head>
944 <body>
945 <h1>307 Temporary Redirect</h1>
946 The resource has been moved to <a href="http://localhost/path/to/foo">http://localhost/path/to/foo</a>;
947 you should be redirected automatically.
948 <BLANKLINE>
949 <BLANKLINE>
950 </body>
951 </html>
952
953
954This is taken from `paste.httpexceptions
955<http://pythonpaste.org/modules/httpexceptions.html#module-paste.httpexceptions>`_, and if
956you have Paste installed then these exceptions will be subclasses of
957the Paste exceptions.
958
959
960Conditional WSGI Application
961----------------------------
962
963The Response object can handle your conditional responses for you,
964checking If-None-Match, If-Modified-Since, and Range/If-Range.
965
966To enable this you must create the response like
967``Response(conditional_response=True)``, or make a subclass like:
968
969.. code-block:: python
970
971 >>> class AppResponse(Response):
972 ... default_content_type = 'text/html'
973 ... default_conditional_response = True
974 >>> res = AppResponse(body='0123456789',
975 ... last_modified=datetime(2005, 1, 1, 12, 0, tzinfo=UTC))
976 >>> req = Request.blank('/')
977 >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC)
978 >>> req.get_response(res)
979 <Response ... 304 Not Modified>
980 >>> del req.if_modified_since
981 >>> res.etag = 'opaque-tag'
982 >>> req.if_none_match = 'opaque-tag'
983 >>> req.get_response(res)
984 <Response ... 304 Not Modified>
985
986 >>> req.if_none_match = '*'
987 >>> 'x' in req.if_none_match
988 True
989 >>> req.if_none_match = req.if_none_match
990 >>> 'x' in req.if_none_match
991 True
992 >>> req.if_none_match = None
993 >>> 'x' in req.if_none_match
994 False
995 >>> req.if_match = None
996 >>> 'x' in req.if_match
997 True
998 >>> req.if_match = req.if_match
999 >>> 'x' in req.if_match
1000 True
1001 >>> req.headers.get('If-Match')
1002 '*'
1003
1004 >>> del req.if_none_match
1005
1006 >>> req.range = (1, 5)
1007 >>> result = req.get_response(res)
1008 >>> result.headers['content-range']
1009 'bytes 1-4/10'
1010 >>> result.body
1011 '1234'