1WebOb 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' 1012