Chris Craik | b2cbf15 | 2015-07-28 16:26:29 -0700 | [diff] [blame^] | 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@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 | |
| 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@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 | |
| 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' |