WebOb Reference+++++++++++++++.. contents::.. comment: >>> from dtopt import ELLIPSISIntroduction============This document covers all the details of the Request and Responseobjects. It is written to be testable with `doctest<http://python.org/doc/current/lib/module-doctest.html>`_ -- thiseffects the flavor of the documentation, perhaps to its detriment.But it also means you can feel confident that the documentation iscorrect.This is a somewhat different approach to reference documentationcompared to the extracted documentation for the `request<class-webob.Request.html>`_ and `response<class-webob.Response.html>`_.Request=======The primary object in WebOb is ``webob.Request``, a wrapper around a`WSGI environment <http://www.python.org/dev/peps/pep-0333/>`_.The basic way you create a request object is simple enough:.. code-block:: >>> from webob import Request >>> environ = {} >>> req = Request(environ)The request object *wraps* the environment; it has very littleinternal state of its own. Instead attributes you access read andwrite to the environment dictionary.You don't have to understand the details of WSGI to use this library;this library handles those details for you. You also don't have touse this exclusively of other libraries. If those other librariesalso keep their state in the environment, multiple wrappers cancoexist. Examples of libraries that can coexist include`paste.wsgiwrappers.Request<http://pythonpaste.org/class-paste.wsgiwrappers.WSGIRequest.html>`_(used by Pylons) and `yaro.Request<http://lukearno.com/projects/yaro/>`_.The WSGI environment has a number of required variables. To make iteasier to test and play around with, the ``Request`` class has aconstructor that will fill in a minimal environment:.. code-block:: >>> req = Request.blank('/article?id=1') >>> from pprint import pprint >>> pprint(req.environ) {'HTTP_HOST': 'localhost:80', 'PATH_INFO': '/article', 'QUERY_STRING': 'id=1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.0', 'wsgi.errors': <open file '<stderr>', mode 'w' at ...>, 'wsgi.input': <cStringIO.StringI object at ...>, 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0)}Request Body------------``req.body`` is a file-like object that gives the body of the request(e.g., a POST form, the body of a PUT, etc). It's kind of boring tostart, but you can set it to a string and that will be turned into afile-like object. You can read the entire body with``req.body``... code-block:: >>> req.body_file <cStringIO.StringI object at ...> >>> req.body '' >>> req.body = 'test' >>> req.body_file <cStringIO.StringI object at ...> >>> req.body 'test'Method & URL------------All the normal parts of a request are also accessible through therequest object:.. code-block:: >>> req.method 'GET' >>> req.scheme 'http' >>> req.script_name # The base of the URL '' >>> req.script_name = '/blog' # make it more interesting >>> req.path_info # The yet-to-be-consumed part of the URL '/article' >>> req.content_type # Content-Type of the request body '' >>> print req.remote_user # The authenticated user (there is none set) None >>> print req.remote_addr # The remote IP None >>> req.host 'localhost:80' >>> req.host_url 'http://localhost' >>> req.application_url 'http://localhost/blog' >>> req.path_url 'http://localhost/blog/article' >>> req.url 'http://localhost/blog/article?id=1' >>> req.path '/blog/article' >>> req.path_qs '/blog/article?id=1' >>> req.query_string 'id=1'You can make new URLs:.. code-block:: >>> req.relative_url('archive') 'http://localhost/blog/archive'For parsing the URLs, it is often useful to deal with just the nextpath segment on PATH_INFO:.. code-block:: >>> req.path_info_peek() # Doesn't change request 'article' >>> req.path_info_pop() # Does change request! 'article' >>> req.script_name '/blog/article' >>> req.path_info ''Headers-------All request headers are available through a dictionary-like object``req.headers``. Keys are case-insensitive... code-block:: >>> req.headers['content-type'] = 'application/x-www-urlencoded' >>> req.headers {'Content-Length': '4', 'Content-Type': 'application/x-www-urlencoded', 'Host': 'localhost:80'} >>> req.environ['CONTENT_TYPE'] 'application/x-www-urlencoded'Query & POST variables----------------------Requests can have variables in one of two locations: the query string(``?id=1``), or in the body of the request (generally a POST form).Note that even POST requests can have a query string, so both kinds ofvariables can exist at the same time. Also, a variable can show upmore than once, as in ``?check=a&check=b``.For these variables WebOb uses a `MultiDict<class-webob.multidict.MultiDict.html>`_, which is basically adictionary wrapper on a list of key/value pairs. It looks like asingle-valued dictionary, but you can access all the values of a keywith ``.getall(key)`` (which always returns a list, possibly an emptylist). You also get all key/value pairs when using ``.items()`` andall values with ``.values()``.Some examples:.. code-block:: >>> req = Request.blank('/test?check=a&check=b&name=Bob') >>> req.GET MultiDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) >>> req.GET['check'] 'b' >>> req.GET.getall('check') ['a', 'b'] >>> req.GET.items() [('check', 'a'), ('check', 'b'), ('name', 'Bob')]We'll have to create a request body and change the method to getPOST. Until we do that, the variables are boring:.. code-block:: >>> req.POST <NoVars: Not a POST request> >>> req.POST.items() # NoVars can be read like a dict, but not written [] >>> req.method = 'POST' >>> req.body = 'name=Joe&email=joe@example.com' >>> req.POST MultiDict([('name', 'Joe'), ('email', 'joe@example.com')]) >>> req.POST['name'] 'Joe'Often you won't care where the variables come from. (Even if you careabout the method, the location of the variables might not beimportant.) There is a dictionary called ``req.params`` thatcontains variables from both sources:.. code-block:: >>> req.params NestedMultiDict([('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Joe'), ('email', 'joe@example.com')]) >>> req.params['name'] 'Bob' >>> req.params.getall('name') ['Bob', 'Joe'] >>> for name, value in req.params.items(): ... print '%s: %r' % (name, value) check: 'a' check: 'b' name: 'Bob' name: 'Joe' email: 'joe@example.com'Unicode Variables~~~~~~~~~~~~~~~~~Submissions are non-unicode (``str``) strings, unless some characterset is indicated. A client can indicate the character set with``Content-Type: application/x-www-form-urlencoded; charset=utf8``, butvery few clients actually do this (sometimes XMLHttpRequest requestswill do this, as JSON is always UTF8 even when a page is served with adifferent character set). You can force a charset, which will effectall the variables:.. code-block:: >>> req.charset = 'utf8' >>> req.GET UnicodeMultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')])If you always want ``str`` values, you can use ``req.str_GET``and ``str_POST``.Cookies-------Cookies are presented in a simple dictionary. Like other variables,they will be decoded into Unicode strings if you set the charset... code-block:: >>> req.headers['Cookie'] = 'test=value' >>> req.cookies UnicodeMultiDict([(u'test', u'value')]) >>> req.charset = None >>> req.cookies {'test': 'value'}Modifying the request---------------------The headers are all modifiable, as are other environmental variables(like ``req.remote_user``, which maps to``request.environ['REMOTE_USER']``).If you want to copy the request you can use ``req.copy()``; thiscopies the ``environ`` dictionary, and the request body from``environ['wsgi.input']``.The method ``req.remove_conditional_headers(remove_encoding=True)``can be used to remove headers that might result in a ``304 NotModified`` response. If you are writing some intermediary it can beuseful to avoid these headers. Also if ``remove_encoding`` is true(the default) then any ``Accept-Encoding`` header will be removed,which can result in gzipped responses.Header Getters--------------In addition to ``req.headers``, there are attributes for most of therequest headers defined by the HTTP 1.1 specification. Theseattributes often return parsed forms of the headers.Accept-* headers~~~~~~~~~~~~~~~~There are several request headers that tell the server what the clientaccepts. These are ``accept`` (the Content-Type that is accepted),``accept_charset`` (the charset accepted), ``accept_encoding``(the Content-Encoding, like gzip, that is accepted), and``accept_language`` (generally the preferred language of the client).The objects returned support containment to test for acceptability.E.g.:.. code-block:: >>> 'text/html' in req.accept TrueBecause no header means anything is potentially acceptable, this isreturning True. We can set it to see more interesting behavior (theexample means that ``text/html`` is okay, but``application/xhtml+xml`` is preferred):.. code-block:: >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1' >>> req.accept <MIMEAccept at ... Accept: text/html;q=0.5, application/xhtml+xml> >>> 'text/html' in req.accept TrueThere's three methods for different strategies of finding a match.First, when you trust the server's preference over the client (a goodidea for Accept):.. code-block:: >>> req.accept.first_match(['text/html', 'application/xhtml+xml']) 'text/html'Because ``text/html`` is at least *somewhat* acceptible, it isreturned, even if the client says it prefers``application/xhtml+xml``. If we trust the client more:.. code-block:: >>> req.accept.best_match(['text/html', 'application/xhtml+xml']) 'application/xhtml+xml'If we just want to know everything the client prefers, in the order itis preferred:.. code-block:: >>> req.accept.best_matches() ['application/xhtml+xml', 'text/html']For languages you'll often have a "fallback" language. E.g., if there'snothing better then use ``en-US`` (and if ``en-US`` is okay, ignoreany less preferrable languages):.. code-block:: >>> req.accept_language = 'es, pt-BR' >>> req.accept_language.best_matches('en-US') ['es', 'pt-BR', 'en-US'] >>> req.accept_language.best_matches('es') ['es']Conditional Requests~~~~~~~~~~~~~~~~~~~~There a number of ways to make a conditional request. A conditionalrequest is made when the client has a document, but it is not sure ifthe document is up to date. If it is not, it wants a new version. Ifthe document is up to date then it doesn't want to waste thebandwidth, and expects a ``304 Not Modified`` response.ETags are generally the best technique for these kinds of requests;this is an opaque string that indicates the identity of the object.For instance, it's common to use the mtime (last modified) of the file,plus the number of bytes, and maybe a hash of the filename (if there'sa possibility that the same URL could point to two differentserver-side filenames based on other variables). To test if a 304response is appropriate, you can use:.. code-block:: >>> server_token = 'opaque-token' >>> server_token in req.if_none_match # You shouldn't return 304 False >>> req.if_none_match = server_token >>> req.if_none_match <ETag opaque-token> >>> server_token in req.if_none_match # You *should* return 304 TrueFor date-based comparisons If-Modified-Since is used:.. code-block:: >>> from webob import UTC >>> from datetime import datetime >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.headers['If-Modified-Since'] 'Sun, 01 Jan 2006 12:00:00 GMT' >>> server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> req.if_modified_since and req.if_modified_since >= server_modified TrueFor range requests there are two important headers, If-Range (which isform of conditional request) and Range (which requests a range). Ifthe If-Range header fails to match then the full response (not arange) should be returned:.. code-block:: >>> req.if_range <Empty If-Range> >>> req.if_range.match(etag='some-etag', last_modified=datetime(2005, 1, 1, 12, 0)) True >>> req.if_range = 'opaque-etag' >>> req.if_range.match(etag='other-etag') False >>> req.if_range.match(etag='opaque-etag') TrueYou can also pass in a response object with:.. code-block:: >>> from webob import Response >>> res = Response(etag='opaque-etag') >>> req.if_range.match_response(res) TrueTo get the range information: >>> req.range = 'bytes=0-100' >>> req.range <Range ranges=(0, 99)> >>> cr = req.range.content_range(length=1000) >>> cr.start, cr.stop, cr.length (0, 99, 1000)Note that the range headers use *inclusive* ranges (the last byteindexed is included), where Python always uses a range where the lastindex is excluded from the range. The ``.stop`` index is in thePython form.Another kind of conditional request is a request (typically PUT) thatincludes If-Match or If-Unmodified-Since. In this case you are saying"here is an update to a resource, but don't apply it if someone elsehas done something since I last got the resource". If-Match means "dothis if the current ETag matches the ETag I'm giving".If-Unmodified-Since means "do this if the resource has remainedunchanged"... code-block:: >>> server_token in req.if_match # No If-Match means everything is ok True >>> req.if_match = server_token >>> server_token in req.if_match # Still OK True >>> req.if_match = 'other-token' >>> # Not OK, should return 412 Precondition Failed: >>> server_token in req.if_match FalseFor more on this kind of conditional request, see `Detecting the LostUpdate Problem Using Unreserved Checkout<http://www.w3.org/1999/04/Editing/>`_.Calling WSGI Applications-------------------------The request object can be used to make handy subrequests or testrequests against WSGI applications. If you want to make subrequests,you should copy the request (with ``req.copy()``) before sending it tomultiple applications, since applications might modify the requestwhen they are run.There's two forms of the subrequest. The more primitive form isthis:.. code-block:: >>> req = Request.blank('/') >>> def wsgi_app(environ, start_response): ... start_response('200 OK', [('Content-type', 'text/plain')]) ... return ['Hi!'] >>> req.call_application(wsgi_app) ('200 OK', [('Content-type', 'text/plain')], ['Hi!'])Note it returns ``(status_string, header_list, app_iter)``. If``app_iter.close()`` exists, it is your responsibility to call it.A handier response can be had with:.. code-block:: >>> res = req.get_response(wsgi_app) >>> res <Response ... 200 OK> >>> res.status '200 OK' >>> res.headers HeaderDict([('Content-type', 'text/plain')]) >>> res.body 'Hi!'You can learn more about this response object in the Response_ section.Thread-local Request Wrappers-----------------------------You can also give the ``Request`` object a function to get theenvironment. This can be used to make a single global request objectdynamic. An example:.. code-block:: >>> import threading >>> import webob >>> environments = threading.local() >>> def get_environ(): ... return environments.environ >>> def set_thread_environ(environ): ... environments.environ = environ >>> request = webob.Request(environ_getter=get_environ) >>> set_thread_environ({'SCRIPT_NAME': '/test'}) >>> request.script_name '/test'Ad-Hoc Attributes-----------------You can assign attributes to your request objects. They will all goin ``environ['webob.adhoc_attrs']`` (a dictionary). .. code-block:: >>> req = Request.blank('/') >>> req.some_attr = 'blah blah blah' >>> new_req = Request(req.environ) >>> new_req.some_attr 'blah blah blah' >>> req.environ['webob.adhoc_attrs'] {'some_attr': 'blah blah blah'}Response========The ``webob.Response`` object contains everything necessary to make aWSGI response. Instances of it are in fact WSGI applications, but itcan also represent the result of calling a WSGI application (as notedin `Calling WSGI Applications`_). It can also be a way ofaccumulating a response in your WSGI application.A WSGI response is made up of a status (like ``200 OK``), a list ofheaders, and a body (or iterator that will produce a body).Core Attributes---------------The core attributes are unsurprising:.. code-block:: >>> from webob import Response >>> res = Response() >>> res.status '200 OK' >>> res.headerlist [('Content-Length', '0')] >>> res.body ''You can set any of these attributes, e.g.:.. code-block:: >>> res.status = 404 >>> res.status '404 Not Found' >>> res.status_int 404 >>> res.headerlist = [('Content-type', 'text/html')] >>> res.body = 'test' >>> print res 404 Not Found Content-type: text/html Content-Length: 4 <BLANKLINE> test >>> res.body = u"test" Traceback (most recent call last): ... TypeError: You cannot set Response.body to a unicode object (use Response.unicode_body) >>> res.unicode_body = u"test" Traceback (most recent call last): ... AttributeError: You cannot access Response.unicode_body unless charset is set >>> res.charset = 'utf8' >>> res.unicode_body = u"test" >>> res.body 'test'You can set any attribute with the constructor, like``Response(charset='utf8')``Headers-------In addition to ``res.headerlist``, there is dictionary-like view onthe list in ``res.headers``:.. code-block:: >>> res.headers HeaderDict([('content-type', 'text/html; charset=utf8'), ('Content-Length', '4')])This is case-insensitive. It can support multiple values for a key,though only if you use ``res.headers.add(key, value)`` or read themwith ``res.headers.getall(key)``.Body & app_iter---------------The ``res.body`` attribute represents the entire body of the requestas a single string (not unicode, though you can set it to unicode ifyou have a charset defined). There is also a ``res.app_iter``attribute that reprsents the body as an iterator. WSGI applicationsreturn these ``app_iter`` iterators instead of strings, and sometimesit can be problematic to load the entire iterator at once (forinstance, if it returns the contents of a very large file). Generallyit is not a problem, and often the iterator is something simple like aone-item list containing a string with the entire body.If you set the body then Content-Length will also be set, and an``res.app_iter`` will be created for you. If you set ``res.app_iter``then Content-Length will be cleared, but it won't be set for you.There is also a file-like object you can access, which will update theapp_iter in-place (turning the app_iter into a list if necessary):.. code-block:: >>> res = Response(content_type='text/plain') >>> f = res.body_file >>> f.write('hey') >>> f.write(u'test') Traceback (most recent call last): . . . TypeError: You can only write unicode to Response.body_file if charset has been set >>> f.encoding >>> res.charset = 'utf8' >>> f.encoding 'utf8' >>> f.write(u'test') >>> res.app_iter ['hey', 'test'] >>> res.body 'heytest'Header Getters--------------Like Request, HTTP response headers are also available as individualproperties. These represent parsed forms of the headers.Content-Type is a special case, as the type and the charset arehandled through two separate properties:.. code-block:: >>> res = Response() >>> res.content_type = 'text/html' >>> res.charset = 'utf8' >>> res.content_type 'text/html' >>> res.headers['content-type'] 'text/html; charset=utf8' >>> res.content_type = 'application/atom+xml' >>> res.content_type_params {'charset': 'utf8'} >>> res.content_type_params = {'type': 'entry', 'charset': 'utf8'} >>> res.headers['content-type'] 'application/atom+xml; charset=utf8; type=entry'Other headers:.. code-block:: >>> # Used with a redirect: >>> res.location = 'http://localhost/foo' >>> # Indicates that the server accepts Range requests: >>> res.accept_ranges = 'bytes' >>> # Used by caching proxies to tell the client how old the >>> # response is: >>> res.age = 120 >>> # Show what methods the client can do; typically used in >>> # a 405 Method Not Allowed response: >>> res.allow = ['GET', 'PUT'] >>> # Set the cache-control header: >>> res.cache_control.max_age = 360 >>> res.cache_control.no_transform = True >>> # Used if you had gzipped the body: >>> res.content_encoding = 'gzip' >>> # What language(s) are in the content: >>> res.content_language = ['en'] >>> # Seldom used header that tells the client where the content >>> # is from: >>> res.content_location = 'http://localhost/foo' >>> # Seldom used header that gives a hash of the body: >>> res.content_md5 = 'big-hash' >>> # Means we are serving bytes 0-500 inclusive, out of 1000 bytes total: >>> # you can also use the range setter shown earlier >>> res.content_range = (0, 499, 1000) >>> # The length of the content; set automatically if you set >>> # res.body: >>> res.content_length = 4 >>> # Used to indicate the current date as the server understands >>> # it: >>> res.date = datetime.now() >>> # The etag: >>> res.etag = 'opaque-token' >>> # You can generate it from the body too: >>> res.md5_etag() >>> res.etag '1B2M2Y8AsgTpgAmY7PhCfg' >>> # When this page should expire from a cache (Cache-Control >>> # often works better): >>> import time >>> res.expires = time.time() + 60*60 # 1 hour >>> # When this was last modified, of course: >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC) >>> # Used with 503 Service Unavailable to hint the client when to >>> # try again: >>> res.retry_after = 160 >>> # Indicate the server software: >>> res.server = 'WebOb/1.0' >>> # Give a list of headers that the cache should vary on: >>> res.vary = ['Cookie']Note in each case you can general set the header to a string to avoidany parsing, and set it to None to remove the header (or do somethinglike ``del res.vary``).In the case of date-related headers you can set the value to a``datetime`` instance (ideally with a UTC timezone), a time tuple, aninteger timestamp, or a properly-formatted string.After setting all these headers, here's the result:.. code-block:: >>> for name, value in res.headerlist: ... print '%s: %s' % (name, value) content-type: application/atom+xml; charset=utf8; type=entry location: http://localhost/foo Accept-Ranges: bytes Age: 120 Allow: GET, PUT Cache-Control: max-age=360, no-transform Content-Encoding: gzip Content-Language: en Content-Location: http://localhost/foo Content-MD5: big-hash Content-Range: bytes 0-500/1000 Content-Length: 4 Date: ... GMT ETag: ... Expires: ... GMT Last-Modified: Mon, 01 Jan 2007 12:00:00 GMT Retry-After: 160 Server: WebOb/1.0 Vary: CookieYou can also set Cache-Control related attributes with``req.cache_expires(seconds, **attrs)``, like:.. code-block:: >>> res = Response() >>> res.cache_expires(10) >>> res.headers['Cache-Control'] 'max-age=10' >>> res.cache_expires(0) >>> res.headers['Cache-Control'] 'max-age=0, must-revalidate, no-cache, no-store' >>> res.headers['Expires'] '... GMT'You can also use the `timedelta<http://python.org/doc/current/lib/datetime-timedelta.html>`_constants defined, e.g.:.. code-block:: >>> from webob import * >>> res = Response() >>> res.cache_expires(2*day+4*hour) >>> res.headers['Cache-Control'] 'max-age=187200'Cookies-------Cookies (and the Set-Cookie header) are handled with a couplemethods. Most importantly:.. code-block:: >>> res.set_cookie('key', 'value', max_age=360, path='/', ... domain='example.org', secure=True) >>> res.headers['Set-Cookie'] 'key=value; Domain=example.org; Max-Age=360; Path=/; secure;' >>> # To delete a cookie previously set in the client: >>> res.delete_cookie('bad_cookie') >>> res.headers['Set-Cookie'] 'bad_cookie=; Max-Age=0; Path=/;'The only other real method of note (note that this does *not* deletethe cookie from clients, only from the response object):.. code-block:: >>> res.unset_cookie('key') >>> res.unset_cookie('bad_cookie') >>> print res.headers.get('Set-Cookie') NoneBinding a Request-----------------You can bind a request (or request WSGI environ) to the responseobject. This is available through ``res.request`` or``res.environ``. This is currently only used in setting``res.location``, to make the location absolute if necessary.Response as a WSGI application------------------------------A response is a WSGI application, in that you can do:.. code-block:: >>> req = Request.blank('/') >>> status, headers, app_iter = req.call_application(res)A possible pattern for your application might be:.. code-block:: >>> def my_app(environ, start_response): ... req = Request(environ) ... res = Response() ... res.content_type = 'text/plain' ... parts = [] ... for name, value in sorted(req.environ.items()): ... parts.append('%s: %r' % (name, value)) ... res.body = '\n'.join(parts) ... return res(environ, start_response) >>> req = Request.blank('/') >>> res = req.get_response(my_app) >>> print res 200 OK content-type: text/plain Content-Length: 394 <BLANKLINE> HTTP_HOST: 'localhost:80' PATH_INFO: '/' QUERY_STRING: '' REQUEST_METHOD: 'GET' SCRIPT_NAME: '' SERVER_NAME: 'localhost' SERVER_PORT: '80' SERVER_PROTOCOL: 'HTTP/1.0' wsgi.errors: <open file '<stderr>', mode 'w' at ...> wsgi.input: <cStringIO.StringI object at ...> wsgi.multiprocess: False wsgi.multithread: False wsgi.run_once: False wsgi.url_scheme: 'http' wsgi.version: (1, 0)Exceptions==========In addition to Request and Response objects, there are a set of Pythonexceptions for different HTTP responses (3xx, 4xx, 5xx codes).These provide a simple way to provide these non-200 response. A verysimple body is provided... code-block:: >>> from webob.exc import * >>> exc = HTTPTemporaryRedirect(location='foo') >>> req = Request.blank('/path/to/something') >>> print str(req.get_response(exc)).strip() 307 Temporary Redirect content-type: text/html location: http://localhost/path/to/foo Content-Length: 126 <BLANKLINE> 307 Temporary Redirect <BLANKLINE> The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically.Note that only if there's an ``Accept: text/html`` header in therequest will an HTML response be given:.. code-block:: >>> req.accept += 'text/html' >>> print str(req.get_response(exc)).strip() 307 Temporary Redirect content-type: text/html location: http://localhost/path/to/foo Content-Length: 270 <BLANKLINE> <html> <head> <title>307 Temporary Redirect</title> </head> <body> <h1>307 Temporary Redirect</h1> The resource has been moved to <a href="http://localhost/path/to/foo">http://localhost/path/to/foo</a>; you should be redirected automatically. <BLANKLINE> <BLANKLINE> </body> </html>This is taken from `paste.httpexceptions<http://pythonpaste.org/module-paste.httpexceptions.html>`_, and ifyou have Paste installed then these exceptions will be subclasses ofthe Paste exceptions.Note that on Python 2.4 and before, new-style classes could not beused as exceptions. ``Response`` objects must be new-style classes,so this causes a bit of a conflict. The base class``webob.exc.HTTPException`` *is* an exception, so you can catch that,and it *is* a WSGI application. But you may not be able to useResponse methods. You can always get ``obj.exception`` to get anexception that you can raise, and ``obj.wsgi_response`` to get the``Response`` object that you can use.Conditional WSGI Application----------------------------The Response object can handle your conditional responses for you,checking If-None-Match, If-Modified-Since, and Range/If-Range.To enable this you must create the response like``Response(conditional_request=True)``, or make a subclass like:.. code-block:: >>> class AppResponse(Response): ... default_content_type = 'text/html' ... default_conditional_response = True >>> res = AppResponse(body='0123456789', ... last_modified=datetime(2005, 1, 1, 12, 0, tzinfo=UTC)) >>> req = Request.blank('/') >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.get_response(res) <Response ... 304 Not Modified> >>> del req.if_modified_since >>> res.etag = 'opaque-tag' >>> req.if_none_match = 'opaque-tag' >>> req.get_response(res) <Response ... 304 Not Modified> >>> del req.if_none_match >>> req.range = (1, 5) >>> result = req.get_response(res) >>> result.headers['content-range'] 'bytes 1-6/10' >>> result.body '1234'