| .. _reference: |
| |
| WebOb Reference |
| +++++++++++++++ |
| |
| .. contents:: |
| |
| Introduction |
| ============ |
| |
| This document covers all the details of the Request and Response |
| objects. It is written to be testable with :py:mod:`doctest` -- this |
| affects the flavor of the documentation, perhaps to its detriment. |
| But it also means you can feel confident that the documentation is |
| correct. |
| |
| .. note:: |
| |
| All of the code samples below are for Python 3, and will not function as-is |
| on Python 2. |
| |
| This is a somewhat different approach to reference documentation compared to |
| the extracted documentation for the :py:mod:`~webob.request` and |
| :py:mod:`~webob.response`. |
| |
| .. _request-reference: |
| |
| Request |
| ======= |
| |
| The primary object in WebOb is :py:class:`webob.Request`, a wrapper around a |
| `WSGI environment <https://www.python.org/dev/peps/pep-0333/>`_. |
| |
| The basic way you create a request object is simple enough: |
| |
| .. code-block:: python |
| |
| >>> from webob import Request |
| >>> environ = {'wsgi.url_scheme': 'http', ...} #doctest: +SKIP |
| >>> req = Request(environ) #doctest: +SKIP |
| |
| (Note that the WSGI environment is a dictionary with a dozen required |
| keys, so it's a bit lengthly to show a complete example of what it |
| would look like -- usually your WSGI server will create it.) |
| |
| The request object *wraps* the environment; it has very little |
| internal state of its own. Instead attributes you access read and |
| write 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 to |
| use this exclusively of other libraries. If those other libraries |
| also keep their state in the environment, multiple wrappers can |
| coexist. Examples of libraries that can coexist include |
| `paste.wsgiwrappers.Request |
| <https://bitbucket.org/ianb/paste/src/0e5a48796ab969d874c6b772c5c33561ac2d1b0d/paste/wsgiwrappers.py?at=default&fileviewer=file-view-default#wsgiwrappers.py-64>`_ |
| (used by Pylons) and `yaro.Request |
| <https://pypi.python.org/pypi/yaro>`_. |
| |
| The WSGI environment has a number of required variables. To make it |
| easier to test and play around with, the ``Request`` class has a |
| constructor that will fill in a minimal environment: |
| |
| .. code-block:: python |
| |
| >>> req = Request.blank('/article?id=1') |
| >>> from pprint import pprint |
| >>> pprint(req.environ) # doctest: +ELLIPSIS |
| {'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': <...TextIOWrapper ...'<stderr>' ...>, |
| 'wsgi.input': <...IO object at 0x...>, |
| 'wsgi.multiprocess': False, |
| 'wsgi.multithread': False, |
| 'wsgi.run_once': False, |
| 'wsgi.url_scheme': 'http', |
| 'wsgi.version': (1, 0)} |
| |
| |
| Request Body |
| ------------ |
| |
| :py:meth:`req.body <webob.request.BaseRequest.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 to start, but you can set it to a string and that will be |
| turned into a file-like object. You can read the entire body with |
| :py:meth:`req.body <webob.request.BaseRequest.body>`. |
| |
| .. code-block:: python |
| |
| >>> hasattr(req.body_file, 'read') |
| True |
| >>> req.body |
| b'' |
| >>> req.method = 'PUT' |
| >>> req.body = b'test' |
| >>> hasattr(req.body_file, 'read') |
| True |
| >>> req.body |
| b'test' |
| |
| Method & URL |
| ------------ |
| |
| All the normal parts of a request are also accessible through the |
| request object: |
| |
| .. code-block:: python |
| |
| >>> req.method |
| 'PUT' |
| >>> 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:: python |
| |
| >>> req.relative_url('archive') |
| 'http://localhost/blog/archive' |
| |
| For parsing the URLs, it is often useful to deal with just the next |
| path segment on PATH_INFO: |
| |
| .. code-block:: python |
| |
| >>> 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 |
| :py:meth:`req.headers <webob.request.BaseRequest.headers>`. Keys are |
| case-insensitive. |
| |
| .. code-block:: python |
| |
| >>> req.headers['Content-Type'] = 'application/x-www-urlencoded' |
| >>> sorted(req.headers.items()) |
| [('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 of |
| variables can exist at the same time. Also, a variable can show up |
| more than once, as in ``?check=a&check=b``. |
| |
| For these variables WebOb uses a :py:class:`~webob.multidict.MultiDict`, which |
| is basically a dictionary wrapper on a list of key/value pairs. It looks like |
| a single-valued dictionary, but you can access all the values of a key with |
| :py:meth:`.getall(key) <webob.multidict.MultiDict.getall>` (which always |
| returns a list, possibly an empty list). You also get all key/value pairs when |
| using :py:meth:`.items() <webob.multidict.MultiDict.items>` and all values with |
| :py:meth:`.values() <webob.multidict.MultiDict.values>`. |
| |
| Some examples: |
| |
| .. code-block:: python |
| |
| >>> req = Request.blank('/test?check=a&check=b&name=Bob') |
| >>> req.GET |
| GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) |
| >>> req.GET['check'] |
| 'b' |
| >>> req.GET.getall('check') |
| ['a', 'b'] |
| >>> list(req.GET.items()) |
| [('check', 'a'), ('check', 'b'), ('name', 'Bob')] |
| |
| We'll have to create a request body and change the method to get ``POST``. |
| Until we do that, the variables are boring: |
| |
| .. code-block:: python |
| |
| >>> req.POST |
| <NoVars: Not a form request> |
| >>> list(req.POST.items()) # NoVars can be read like a dict, but not written |
| [] |
| >>> req.method = 'POST' |
| >>> req.body = b'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 care about |
| the method, the location of the variables might not be important.) There is a |
| dictionary called :py:meth:`req.params <webob.request.BaseRequest.params>` that |
| contains variables from both sources: |
| |
| .. code-block:: python |
| |
| >>> 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' |
| |
| The ``POST`` and ``GET`` nomenclature is historical -- :py:meth:`req.GET |
| <webob.request.BaseRequest.GET>` can be used for non-GET requests to access |
| query parameters, and :py:meth:`req.POST <webob.request.BaseRequest.POST>` can |
| also be used for PUT requests with the appropriate Content-Type. |
| |
| >>> req = Request.blank('/test?check=a&check=b&name=Bob') |
| >>> req.method = 'PUT' |
| >>> req.body = b'var1=value1&var2=value2&rep=1&rep=2' |
| >>> req.environ['CONTENT_LENGTH'] = str(len(req.body)) |
| >>> req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' |
| >>> req.GET |
| GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) |
| >>> req.POST |
| MultiDict([('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')]) |
| |
| Unicode Variables |
| ~~~~~~~~~~~~~~~~~ |
| |
| Submissions are by default UTF-8, you can force a different character set by |
| setting the charset on the ``Request`` object explicitly. A client can indicate |
| the character set with ``Content-Type: application/x-www-form-urlencoded; |
| charset=utf8``, but very few clients actually do this (sometimes XMLHttpRequest |
| requests will do this, as JSON is always UTF8 even when a page is served with a |
| different character set). You can force a charset, which will affect all the |
| variables: |
| |
| .. code-block:: python |
| |
| >>> req.charset = 'utf8' |
| >>> req.GET |
| GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) |
| |
| 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:: python |
| |
| >>> req.headers['Cookie'] = 'test=value' |
| >>> req.cookies |
| <RequestCookies (dict-like) with values {'test': 'value'}> |
| |
| Modifying the request |
| --------------------- |
| |
| The headers are all modifiable, as are other environmental variables (like |
| :py:meth:`req.remote_user <webob.request.BaseRequest.remote_user>`, which maps |
| to ``request.environ['REMOTE_USER']``). |
| |
| If you want to copy the request you can use :py:meth:`req.copy() |
| <webob.request.BaseRequest.copy>`; this copies the ``environ`` dictionary, and |
| the request body from ``environ['wsgi.input']``. |
| |
| The method :py:meth:`req.remove_conditional_headers(remove_encoding=True) |
| <webob.request.BaseRequest.remove_conditional_headers>` can be used to remove |
| headers that might result in a ``304 Not Modified`` response. If you are |
| writing some intermediary it can be useful 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 :py:meth:`req.headers <webob.request.BaseRequest.headers>`, |
| there are attributes for most of the request headers defined by the HTTP 1.1 |
| specification. These attributes often return parsed forms of the headers. |
| |
| Accept-* headers |
| ~~~~~~~~~~~~~~~~ |
| |
| There are several request headers that tell the server what the client |
| accepts. 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:: python |
| |
| >>> 'text/html' in req.accept |
| True |
| |
| Because no header means anything is potentially acceptable, this is |
| returning True. We can set it to see more interesting behavior (the |
| example means that ``text/html`` is okay, but |
| ``application/xhtml+xml`` is preferred): |
| |
| .. code-block:: python |
| |
| >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1' |
| >>> req.accept |
| <AcceptValidHeader ('text/html;q=0.5, application/xhtml+xml')> |
| >>> 'text/html' in req.accept |
| True |
| |
| There are a few methods for different strategies of finding a match. |
| |
| .. code-block:: python |
| |
| >>> 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 it |
| is preferred: |
| |
| .. code-block:: python |
| |
| >>> list(req.accept) |
| ['application/xhtml+xml', 'text/html'] |
| |
| For languages you'll often have a "fallback" language. E.g., if there's |
| nothing better then use ``en-US`` (and if ``en-US`` is okay, ignore |
| any less preferrable languages): |
| |
| .. code-block:: python |
| |
| >>> req.accept_language = 'es, pt-BR' |
| >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US') |
| 'en-US' |
| >>> req.accept_language.best_match(['es', 'en-US'], default_match='en-US') |
| 'es' |
| |
| Your fallback language must appear both in the ``offers`` and as the |
| ``default_match`` to insure that it is returned as a best match if the |
| client specified a preference for it. |
| |
| .. code-block:: python |
| |
| >>> req.accept_language = 'en-US;q=0.5, en-GB;q=0.2' |
| >>> req.accept_language.best_match(['en-GB'], default_match='en-US') |
| 'en-GB' |
| >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US') |
| 'en-US' |
| |
| Conditional Requests |
| ~~~~~~~~~~~~~~~~~~~~ |
| |
| There a number of ways to make a conditional request. A conditional |
| request is made when the client has a document, but it is not sure if |
| the document is up to date. If it is not, it wants a new version. If |
| the document is up to date then it doesn't want to waste the |
| bandwidth, 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's |
| a possibility that the same URL could point to two different |
| server-side filenames based on other variables). To test if a 304 |
| response is appropriate, you can use: |
| |
| .. code-block:: python |
| |
| >>> 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 |
| True |
| |
| For date-based comparisons If-Modified-Since is used: |
| |
| .. code-block:: python |
| |
| >>> 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 |
| True |
| |
| For range requests there are two important headers, If-Range (which is |
| form of conditional request) and Range (which requests a range). If |
| the If-Range header fails to match then the full response (not a |
| range) should be returned: |
| |
| .. code-block:: python |
| |
| >>> req.if_range |
| IfRange(<ETag *>) |
| >>> req.if_range = 'opaque-etag' |
| >>> from webob import Response |
| >>> res = Response(etag='opaque-etag') |
| >>> res in req.if_range |
| True |
| |
| To get the range information: |
| |
| >>> req.range = 'bytes=0-100' |
| >>> req.range |
| <Range bytes 0-101> |
| >>> cr = req.range.content_range(length=1000) |
| >>> cr.start, cr.stop, cr.length |
| (0, 101, 1000) |
| |
| Note that the range headers use *inclusive* ranges (the last byte |
| indexed is included), where Python always uses a range where the last |
| index is excluded from the range. The ``.stop`` index is in the |
| Python form. |
| |
| Another kind of conditional request is a request (typically PUT) that |
| includes 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 else |
| has done something since I last got the resource". If-Match means "do |
| this if the current ETag matches the ETag I'm giving". |
| If-Unmodified-Since means "do this if the resource has remained |
| unchanged". |
| |
| .. code-block:: python |
| |
| >>> 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 |
| False |
| |
| For more on this kind of conditional request, see `Detecting the Lost |
| Update Problem Using Unreserved Checkout |
| <https://www.w3.org/1999/04/Editing/>`_. |
| |
| Calling WSGI Applications |
| ------------------------- |
| |
| The request object can be used to make handy subrequests or test |
| requests against WSGI applications. If you want to make subrequests, |
| you should copy the request (with ``req.copy()``) before sending it to |
| multiple applications, since applications might modify the request |
| when they are run. |
| |
| There's two forms of the subrequest. The more primitive form is |
| this: |
| |
| .. code-block:: python |
| |
| >>> 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:: python |
| |
| >>> res = req.get_response(wsgi_app) |
| >>> res # doctest: +ELLIPSIS |
| <Response ... 200 OK> |
| >>> res.status |
| '200 OK' |
| >>> res.headers |
| ResponseHeaders([('Content-type', 'text/plain')]) |
| >>> res.body |
| 'Hi!' |
| |
| You can learn more about this response object in the Response_ section. |
| |
| Ad-Hoc Attributes |
| ----------------- |
| |
| You can assign attributes to your request objects. They will all go |
| in ``environ['webob.adhoc_attrs']`` (a dictionary). |
| |
| .. code-block:: python |
| |
| >>> 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-reference: |
| |
| Response |
| ======== |
| |
| The :py:class:`webob.Response <webob.response.Response>` object contains |
| everything necessary to make a WSGI response. Instances of it are in fact WSGI |
| applications, but it can also represent the result of calling a WSGI |
| application (as noted in `Calling WSGI Applications`_). It can also be a way |
| of accumulating a response in your WSGI application. |
| |
| A WSGI response is made up of a status (like ``200 OK``), a list of |
| headers, and a body (or iterator that will produce a body). |
| |
| Core Attributes |
| --------------- |
| |
| The core attributes are unsurprising: |
| |
| .. code-block:: python |
| |
| >>> from webob import Response |
| >>> res = Response() |
| >>> res.status |
| '200 OK' |
| >>> res.headerlist |
| [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')] |
| >>> res.body |
| b'' |
| |
| You can set any of these attributes, e.g.: |
| |
| .. code-block:: python |
| |
| >>> res.status = 404 |
| >>> res.status |
| '404 Not Found' |
| >>> res.status_code |
| 404 |
| >>> res.headerlist = [('Content-Type', 'text/html')] |
| >>> res.body = b'test' |
| >>> print(res) |
| 404 Not Found |
| Content-Type: text/html |
| Content-Length: 4 |
| <BLANKLINE> |
| test |
| >>> res.body = "test" # doctest: +ELLIPSIS |
| Traceback (most recent call last): |
| ... |
| TypeError: You cannot set Response.body to a text object (use Response.text) |
| >>> res.text = "test" |
| >>> res.body |
| b'test' |
| |
| You can set any attribute with the constructor, like |
| ``Response(charset='UTF-8')`` |
| |
| Headers |
| ------- |
| |
| In addition to ``res.headerlist``, there is dictionary-like view on |
| the list in ``res.headers``: |
| |
| .. code-block:: python |
| |
| >>> res.headers |
| ResponseHeaders([('Content-Type', 'text/html'), ('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 them |
| with ``res.headers.getall(key)``. |
| |
| Body & app_iter |
| --------------- |
| |
| The ``res.body`` attribute represents the entire body of the request |
| as a single string (not unicode, though you can set it to unicode if |
| you have a charset defined). There is also a ``res.app_iter`` |
| attribute that reprsents the body as an iterator. WSGI applications |
| return these ``app_iter`` iterators instead of strings, and sometimes |
| it can be problematic to load the entire iterator at once (for |
| instance, if it returns the contents of a very large file). Generally |
| it is not a problem, and often the iterator is something simple like a |
| one-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 the |
| app_iter in-place (turning the app_iter into a list if necessary): |
| |
| .. code-block:: python |
| |
| >>> res = Response(content_type='text/plain', charset=None) |
| >>> f = res.body_file |
| >>> f.write('hey') # doctest: +ELLIPSIS |
| Traceback (most recent call last): |
| ... |
| TypeError: You can only write text to Response if charset has been set |
| >>> f.encoding |
| >>> res.charset = 'UTF-8' |
| >>> f.encoding |
| 'UTF-8' |
| >>> f.write('test') |
| >>> res.app_iter |
| [b'', b'test'] |
| >>> res.body |
| b'test' |
| |
| Header Getters |
| -------------- |
| |
| Like Request, HTTP response headers are also available as individual |
| properties. These represent parsed forms of the headers. |
| |
| Content-Type is a special case, as the type and the charset are |
| handled through two separate properties: |
| |
| .. code-block:: python |
| |
| >>> 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': 'UTF-8'} |
| >>> res.content_type_params = {'type': 'entry', 'charset': 'UTF-8'} |
| >>> res.headers['content-type'] |
| 'application/atom+xml; charset=UTF-8; type=entry' |
| |
| Other headers: |
| |
| .. code-block:: python |
| |
| >>> # 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 |
| |
| >>> # Tell the browser to treat the response as an attachment: |
| >>> res.content_disposition = 'attachment; filename=foo.xml' |
| |
| >>> # 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, 501, 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 avoid |
| any parsing, and set it to None to remove the header (or do something |
| like ``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, an |
| integer timestamp, or a properly-formatted string. |
| |
| After setting all these headers, here's the result: |
| |
| .. code-block:: python |
| |
| >>> for name, value in res.headerlist: # doctest: +ELLIPSIS |
| ... print('%s: %s' % (name, value)) # doctest: +ELLIPSIS |
| Content-Type: application/atom+xml; charset=UTF-8; type=entry |
| Location: http://localhost/foo |
| Accept-Ranges: bytes |
| Age: 120 |
| Allow: GET, PUT |
| Cache-Control: max-age=360, no-transform |
| Content-Disposition: attachment; filename=foo.xml |
| 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: Cookie |
| |
| You can also set Cache-Control related attributes with |
| ``req.cache_expires(seconds, **attrs)``, like: |
| |
| .. code-block:: python |
| |
| >>> 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'] # doctest: +ELLIPSIS |
| '... GMT' |
| |
| You can also use the :py:class:`~datetime.timedelta` |
| constants defined, e.g.: |
| |
| .. code-block:: python |
| |
| >>> 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 couple |
| methods. Most importantly: |
| |
| .. code-block:: python |
| |
| >>> res.set_cookie('key', 'value', max_age=360, path='/', |
| ... domain='example.org', secure=True) |
| >>> res.headers['Set-Cookie'] # doctest: +ELLIPSIS |
| 'key=value; Domain=example.org; Max-Age=360; Path=/; expires=... GMT; secure' |
| >>> # To delete a cookie previously set in the client: |
| >>> res.delete_cookie('bad_cookie') |
| >>> res.headers['Set-Cookie'] # doctest: +ELLIPSIS |
| 'bad_cookie=; Max-Age=0; Path=/; expires=... GMT' |
| |
| The only other real method of note (note that this does *not* delete |
| the cookie from clients, only from the response object): |
| |
| .. code-block:: python |
| |
| >>> res.unset_cookie('key') |
| >>> res.unset_cookie('bad_cookie') |
| >>> print(res.headers.get('Set-Cookie')) |
| None |
| |
| Binding a Request |
| ----------------- |
| |
| You can bind a request (or request WSGI environ) to the response |
| object. 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:: python |
| |
| >>> req = Request.blank('/') |
| >>> status, headers, app_iter = req.call_application(res) |
| |
| A possible pattern for your application might be: |
| |
| .. code-block:: python |
| |
| >>> def my_app(environ, start_response): |
| ... req = Request(environ) |
| ... res = Response() |
| ... res.charset = 'UTF-8' |
| ... res.content_type = 'text/plain' |
| ... parts = [] |
| ... for name, value in sorted(req.environ.items()): |
| ... parts.append('%s: %r' % (name, value)) |
| ... res.text = '\n'.join(parts) |
| ... return res(environ, start_response) |
| >>> req = Request.blank('/') |
| >>> res = req.get_response(my_app) |
| >>> print(res) # doctest: +ELLIPSIS |
| 200 OK |
| Content-Type: text/plain; charset=UTF-8 |
| Content-Length: ... |
| <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: <...> |
| wsgi.input: <...IO... 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 Python |
| exceptions for different HTTP responses (3xx, 4xx, 5xx codes). |
| |
| These provide a simple way to provide these non-200 response. A very |
| simple body is provided. |
| |
| .. code-block:: python |
| |
| >>> from webob.exc import * |
| >>> exc = HTTPTemporaryRedirect(location='foo') |
| >>> req = Request.blank('/path/to/something') |
| >>> print(str(req.get_response(exc)).strip()) |
| 307 Temporary Redirect |
| Location: http://localhost/path/to/foo |
| Content-Length: 126 |
| Content-Type: text/plain; charset=UTF-8 |
| \r |
| 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 the |
| request will an HTML response be given: |
| |
| .. code-block:: python |
| |
| >>> req.accept += 'text/html' |
| >>> print(str(req.get_response(exc)).strip()) |
| 307 Temporary Redirect |
| Location: http://localhost/path/to/foo |
| Content-Length: 270 |
| Content-Type: text/html; charset=UTF-8 |
| <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> |
| |
| 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_response=True)``, or make a subclass like: |
| |
| .. code-block:: python |
| |
| >>> 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) # doctest: +ELLIPSIS |
| <Response ... 304 Not Modified> |
| >>> del req.if_modified_since |
| >>> res.etag = 'opaque-tag' |
| >>> req.if_none_match = 'opaque-tag' |
| >>> req.get_response(res) # doctest: +ELLIPSIS |
| <Response ... 304 Not Modified> |
| |
| >>> req.if_none_match = '*' |
| >>> 'x' in req.if_none_match |
| True |
| >>> req.if_none_match = req.if_none_match |
| >>> 'x' in req.if_none_match |
| True |
| >>> req.if_none_match = None |
| >>> 'x' in req.if_none_match |
| False |
| >>> req.if_match = None |
| >>> 'x' in req.if_match |
| True |
| >>> req.if_match = req.if_match |
| >>> 'x' in req.if_match |
| True |
| >>> req.headers.get('If-Match') |
| '*' |
| |
| >>> del req.if_none_match |
| |
| >>> req.range = (1, 5) |
| >>> result = req.get_response(res) |
| >>> result.headers['content-range'] |
| 'bytes 1-4/10' |
| >>> result.body |
| b'1234' |