1 WebOb Reference 2 +++++++++++++++ 3 4 .. contents:: 5 6 .. comment: 7 8 >>> from doctest import ELLIPSIS 9 10 Introduction 11 ============ 12 13 This document covers all the details of the Request and Response 14 objects. It is written to be testable with `doctest 15 <http://python.org/doc/current/lib/module-doctest.html>`_ -- this 16 affects the flavor of the documentation, perhaps to its detriment. 17 But it also means you can feel confident that the documentation is 18 correct. 19 20 This is a somewhat different approach to reference documentation 21 compared to the extracted documentation for the `request 22 <class-webob.Request.html>`_ and `response 23 <class-webob.Response.html>`_. 24 25 Request 26 ======= 27 28 The primary object in WebOb is ``webob.Request``, a wrapper around a 29 `WSGI environment <http://www.python.org/dev/peps/pep-0333/>`_. 30 31 The 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 40 keys, so it's a bit lengthly to show a complete example of what it 41 would look like -- usually your WSGI server will create it.) 42 43 The request object *wraps* the environment; it has very little 44 internal state of its own. Instead attributes you access read and 45 write to the environment dictionary. 46 47 You don't have to understand the details of WSGI to use this library; 48 this library handles those details for you. You also don't have to 49 use this exclusively of other libraries. If those other libraries 50 also keep their state in the environment, multiple wrappers can 51 coexist. 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 57 The WSGI environment has a number of required variables. To make it 58 easier to test and play around with, the ``Request`` class has a 59 constructor 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 82 Request 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 87 start, but you can set it to a string and that will be turned into a 88 file-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 104 Method & URL 105 ------------ 106 107 All the normal parts of a request are also accessible through the 108 request 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 144 You can make new URLs: 145 146 .. code-block:: python 147 148 >>> req.relative_url('archive') 149 'http://localhost/blog/archive' 150 151 For parsing the URLs, it is often useful to deal with just the next 152 path 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 165 Headers 166 ------- 167 168 All 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 179 Query & POST variables 180 ---------------------- 181 182 Requests 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). 184 Note that even POST requests can have a query string, so both kinds of 185 variables can exist at the same time. Also, a variable can show up 186 more than once, as in ``?check=a&check=b``. 187 188 For these variables WebOb uses a `MultiDict 189 <class-webob.multidict.MultiDict.html>`_, which is basically a 190 dictionary wrapper on a list of key/value pairs. It looks like a 191 single-valued dictionary, but you can access all the values of a key 192 with ``.getall(key)`` (which always returns a list, possibly an empty 193 list). You also get all key/value pairs when using ``.items()`` and 194 all values with ``.values()``. 195 196 Some 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 210 We'll have to create a request body and change the method to get 211 POST. 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 (a] example.com' 221 >>> req.POST 222 MultiDict([(u'name', u'Joe'), (u'email', u'joe (a] example.com')]) 223 >>> req.POST['name'] 224 u'Joe' 225 226 Often you won't care where the variables come from. (Even if you care 227 about the method, the location of the variables might not be 228 important.) There is a dictionary called ``req.params`` that 229 contains 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 (a] 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 (a] example.com' 246 247 The ``POST`` and ``GET`` nomenclature is historical -- ``req.GET`` can 248 be used for non-GET requests to access query parameters, and 249 ``req.POST`` can also be used for PUT requests with the appropriate 250 Content-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 262 Unicode Variables 263 ~~~~~~~~~~~~~~~~~ 264 265 Submissions are non-unicode (``str``) strings, unless some character 266 set is indicated. A client can indicate the character set with 267 ``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but 268 very few clients actually do this (sometimes XMLHttpRequest requests 269 will do this, as JSON is always UTF8 even when a page is served with a 270 different character set). You can force a charset, which will affect 271 all 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 279 Cookies 280 ------- 281 282 Cookies are presented in a simple dictionary. Like other variables, 283 they 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 291 Modifying the request 292 --------------------- 293 294 The headers are all modifiable, as are other environmental variables 295 (like ``req.remote_user``, which maps to 296 ``request.environ['REMOTE_USER']``). 297 298 If you want to copy the request you can use ``req.copy()``; this 299 copies the ``environ`` dictionary, and the request body from 300 ``environ['wsgi.input']``. 301 302 The method ``req.remove_conditional_headers(remove_encoding=True)`` 303 can be used to remove headers that might result in a ``304 Not 304 Modified`` response. If you are writing some intermediary it can be 305 useful to avoid these headers. Also if ``remove_encoding`` is true 306 (the default) then any ``Accept-Encoding`` header will be removed, 307 which can result in gzipped responses. 308 309 Header Getters 310 -------------- 311 312 In addition to ``req.headers``, there are attributes for most of the 313 request headers defined by the HTTP 1.1 specification. These 314 attributes often return parsed forms of the headers. 315 316 Accept-* headers 317 ~~~~~~~~~~~~~~~~ 318 319 There are several request headers that tell the server what the client 320 accepts. 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 325 The objects returned support containment to test for acceptability. 326 E.g.: 327 328 .. code-block:: python 329 330 >>> 'text/html' in req.accept 331 True 332 333 Because no header means anything is potentially acceptable, this is 334 returning True. We can set it to see more interesting behavior (the 335 example 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 346 There 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 353 If we just want to know everything the client prefers, in the order it 354 is preferred: 355 356 .. code-block:: python 357 358 >>> list(req.accept) 359 ['application/xhtml+xml', 'text/html'] 360 361 For languages you'll often have a "fallback" language. E.g., if there's 362 nothing better then use ``en-US`` (and if ``en-US`` is okay, ignore 363 any 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 373 Your 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 375 client 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 385 Conditional Requests 386 ~~~~~~~~~~~~~~~~~~~~ 387 388 There a number of ways to make a conditional request. A conditional 389 request is made when the client has a document, but it is not sure if 390 the document is up to date. If it is not, it wants a new version. If 391 the document is up to date then it doesn't want to waste the 392 bandwidth, and expects a ``304 Not Modified`` response. 393 394 ETags are generally the best technique for these kinds of requests; 395 this is an opaque string that indicates the identity of the object. 396 For instance, it's common to use the mtime (last modified) of the file, 397 plus the number of bytes, and maybe a hash of the filename (if there's 398 a possibility that the same URL could point to two different 399 server-side filenames based on other variables). To test if a 304 400 response 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 413 For 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 426 For range requests there are two important headers, If-Range (which is 427 form of conditional request) and Range (which requests a range). If 428 the If-Range header fails to match then the full response (not a 429 range) 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 443 You 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 452 To 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 461 Note that the range headers use *inclusive* ranges (the last byte 462 indexed is included), where Python always uses a range where the last 463 index is excluded from the range. The ``.stop`` index is in the 464 Python form. 465 466 Another kind of conditional request is a request (typically PUT) that 467 includes 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 469 has done something since I last got the resource". If-Match means "do 470 this if the current ETag matches the ETag I'm giving". 471 If-Unmodified-Since means "do this if the resource has remained 472 unchanged". 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 486 For more on this kind of conditional request, see `Detecting the Lost 487 Update Problem Using Unreserved Checkout 488 <http://www.w3.org/1999/04/Editing/>`_. 489 490 Calling WSGI Applications 491 ------------------------- 492 493 The request object can be used to make handy subrequests or test 494 requests against WSGI applications. If you want to make subrequests, 495 you should copy the request (with ``req.copy()``) before sending it to 496 multiple applications, since applications might modify the request 497 when they are run. 498 499 There's two forms of the subrequest. The more primitive form is 500 this: 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 511 Note it returns ``(status_string, header_list, app_iter)``. If 512 ``app_iter.close()`` exists, it is your responsibility to call it. 513 514 A 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 528 You can learn more about this response object in the Response_ section. 529 530 Ad-Hoc Attributes 531 ----------------- 532 533 You can assign attributes to your request objects. They will all go 534 in ``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 546 Response 547 ======== 548 549 The ``webob.Response`` object contains everything necessary to make a 550 WSGI response. Instances of it are in fact WSGI applications, but it 551 can also represent the result of calling a WSGI application (as noted 552 in `Calling WSGI Applications`_). It can also be a way of 553 accumulating a response in your WSGI application. 554 555 A WSGI response is made up of a status (like ``200 OK``), a list of 556 headers, and a body (or iterator that will produce a body). 557 558 Core Attributes 559 --------------- 560 561 The 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 574 You 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 604 You can set any attribute with the constructor, like 605 ``Response(charset='utf8')`` 606 607 Headers 608 ------- 609 610 In addition to ``res.headerlist``, there is dictionary-like view on 611 the 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 618 This is case-insensitive. It can support multiple values for a key, 619 though only if you use ``res.headers.add(key, value)`` or read them 620 with ``res.headers.getall(key)``. 621 622 Body & app_iter 623 --------------- 624 625 The ``res.body`` attribute represents the entire body of the request 626 as a single string (not unicode, though you can set it to unicode if 627 you have a charset defined). There is also a ``res.app_iter`` 628 attribute that reprsents the body as an iterator. WSGI applications 629 return these ``app_iter`` iterators instead of strings, and sometimes 630 it can be problematic to load the entire iterator at once (for 631 instance, if it returns the contents of a very large file). Generally 632 it is not a problem, and often the iterator is something simple like a 633 one-item list containing a string with the entire body. 634 635 If 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`` 637 then Content-Length will be cleared, but it won't be set for you. 638 639 There is also a file-like object you can access, which will update the 640 app_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 661 Header Getters 662 -------------- 663 664 Like Request, HTTP response headers are also available as individual 665 properties. These represent parsed forms of the headers. 666 667 Content-Type is a special case, as the type and the charset are 668 handled 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 686 Other 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 761 Note in each case you can general set the header to a string to avoid 762 any parsing, and set it to None to remove the header (or do something 763 like ``del res.vary``). 764 765 In 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 767 integer timestamp, or a properly-formatted string. 768 769 After 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 796 You 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 811 You can also use the `timedelta 812 <http://python.org/doc/current/lib/datetime-timedelta.html>`_ 813 constants 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 823 Cookies 824 ------- 825 826 Cookies (and the Set-Cookie header) are handled with a couple 827 methods. 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 840 The only other real method of note (note that this does *not* delete 841 the 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 850 Binding a Request 851 ----------------- 852 853 You can bind a request (or request WSGI environ) to the response 854 object. 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 858 Response as a WSGI application 859 ------------------------------ 860 861 A 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 868 A 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 904 Exceptions 905 ========== 906 907 In addition to Request and Response objects, there are a set of Python 908 exceptions for different HTTP responses (3xx, 4xx, 5xx codes). 909 910 These provide a simple way to provide these non-200 response. A very 911 simple 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 928 Note that only if there's an ``Accept: text/html`` header in the 929 request 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 954 This is taken from `paste.httpexceptions 955 <http://pythonpaste.org/modules/httpexceptions.html#module-paste.httpexceptions>`_, and if 956 you have Paste installed then these exceptions will be subclasses of 957 the Paste exceptions. 958 959 960 Conditional WSGI Application 961 ---------------------------- 962 963 The Response object can handle your conditional responses for you, 964 checking If-None-Match, If-Modified-Since, and Range/If-Range. 965 966 To 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