root/trunk/diva/http.py

Revision 228, 6.9 KB (checked in by cmlenz, 10 months ago)

Add cookie-based session storage.

  • Property svn:eol-style set to native
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007-2008 Christopher Lenz
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution.
8
9"""Various objects and functions for working with HTTP exchanges."""
10
11from calendar import timegm
12from datetime import datetime, timedelta
13from email.Utils import formatdate
14import logging
15
16from diva import app
17from diva.core import register_filter
18from diva.util import decorator
19from webob.exc import status_map
20
21__all__ = ['abort', 'allow', 'caching', 'match']
22__docformat__ = 'restructuredtext en'
23
24log = logging.getLogger('diva.http')
25
26
27def abort(status, message='', headers=None):
28    """Aborts the request processing immediately by raising an HTTP exception
29    corresponding to the specified status code.
30   
31    >>> abort(404)
32    Traceback (most recent call last):
33      ...
34    HTTPNotFound: 404 Not Found
35    Content-Type: text/html; charset=UTF-8
36    Content-Length: 0
37    <BLANKLINE>
38    <BLANKLINE>
39   
40    >>> abort(301, headers=[('Location', 'http://example.org/')])
41    Traceback (most recent call last):
42      ...
43    HTTPMovedPermanently: 301 Moved Permanently
44    Content-Type: text/html; charset=UTF-8
45    Content-Length: 0
46    Location: http://example.org/
47    <BLANKLINE>
48    <BLANKLINE>
49   
50    :param status: the HTTP status code
51    :param message: an optional message to be displayed to the user (for example
52                    on the error page)
53    :param headers: a list of ``(name, value)`` tuples that define any headers
54                    that should be included with the response
55    """
56    log.debug('Abort with status %d (%r)', status, message)
57    raise status_map[status](headers=headers, detail=message).exception
58
59
60def allow(*methods):
61    """Decorator for request handlers that handle only requests using the
62    specified methods.
63
64    Requests using a method not in the allowed list result in a ``405 Method
65    Not Allowed`` error response.
66
67    Note that allowing ``GET`` implies allowing ``HEAD``.
68
69    :param methods: the allowed request methods as strings
70    """
71    methods = [m.upper() for m in methods]
72    if 'GET' in methods and 'HEAD' not in methods:
73        methods.append('HEAD')
74    def decorate(func):
75        @decorator(func)
76        def wrapper(*args, **kwargs):
77            __traceback_hide__ = True
78            if app.ctxt.request.method.upper() not in methods:
79                abort(405, headers=[('Allow', ','.join(methods))])
80            return func(*args, **kwargs)
81        return wrapper
82    return decorate
83
84
85def _add_caching_headers(response, max_age=None, vary_on=None, **options):
86    status = response.status_int
87    if status < 200 or status >= 300:
88        # Caching headers only for success status codes
89        return
90
91    if max_age is not None and not isinstance(max_age, timedelta):
92        max_age = timedelta(0, max_age)
93
94    headers = response.headers
95    if 'Cache-Control' not in headers:
96        params = []
97        for name, value in options.items():
98            name = name.replace('_', '-')
99            if value is True:
100                params.append(name)
101        if max_age is not None:
102            params.append('max-age=%d' % (
103                max_age.seconds + max_age.days * 86400
104            ))
105        if params:
106            headers['Cache-Control'] = '; '.join(params)
107
108    if max_age is not None and 'Expires' not in headers:
109        expires = datetime.utcnow() + max_age
110        headers['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
111
112    if vary_on and 'Vary' not in headers:
113        headers['Vary'] = ','.join(vary_on)
114
115
116def caching(max_age=None, vary_on=None, **options):
117    """Decorator that allows controlling HTTP headers related to caching.
118   
119    :param max_age: the maximum number of seconds the output should be cached
120    :param vary_on: a list of HTTP header names that should be taken into
121                    account by cache
122    :keyword options: any additional keyword arguments are added to the
123                      "Cache-Control" header of the response if they have a
124                      truth value; for example, ``must_revalidate=1`` would add
125                      the "must-revalidate" flag to the header
126    """
127    def decorate(func):
128        @decorator(func)
129        def wrapper(*args, **kwargs):
130            __traceback_hide__ = True
131            retval = func(*args, **kwargs)
132            _add_caching_headers(app.ctxt.response, max_age=max_age,
133                                 vary_on=vary_on, **options)
134            return retval
135        return wrapper
136    return decorate
137
138
139def match(etag=None, last_modified=None):
140    """Checks the current request for conditional GET semantics based on the
141    "If-Modified-Since" and/or "If-None-Match" headers.
142   
143    If the request includes one of those headers, and their value matches the
144    given parameters, a "204 Not Modified" response is sent. Otherwise, the
145    headers "Last-Modified" and/or "ETag" are headers are added to the response.
146   
147    :param etag: the entity tag value
148    :param last_modified: the last modification date (either as a ``datetime``
149                          object or as a numeric timestamp)
150    """
151    request = app.ctxt.request
152    response = app.ctxt.response
153    if response.status_int < 200 or response.status_int >= 300:
154        # Only do the matching for responses with a HTTP success codes
155        return
156
157    if etag is not None:
158        response.etag = str('"%s"' % etag) # FIXME: WebOb #251
159        if request.method in ('PUT', 'DELETE') and request.if_match \
160                and etag not in request.if_match:
161            log.debug('Etag %r did not match %r', etag, request.if_match)
162            abort(412)
163        elif request.method in ('GET', 'HEAD') and request.if_none_match \
164                and etag in request.if_none_match:
165            log.debug('Etag %r matched %r', etag, request.if_none_match)
166            abort(304)
167
168    if last_modified is not None:
169        response.last_modified = last_modified
170        if request.method in ('PUT', 'DELETE') \
171                and request.if_unmodified_since \
172                and response.last_modified != request.if_unmodified_since:
173            log.debug('Modification time "%s" did not match "%s"',
174                      response.last_modified, request.if_unmodified_since)
175            abort(412)
176        elif request.method in ('GET', 'HEAD') and request.if_modified_since \
177                and response.last_modified == request.if_modified_since:
178            log.debug('Modification time "%s" matched "%s"',
179                      response.last_modified, request.if_modified_since)
180            abort(304)
181
182
183@register_filter('caching')
184def caching_filter(request, response, chain):
185    """Request filter that adds standard cache disabling headers to responses
186    that have no explicit cache control.
187    """
188    try:
189        return chain.next()(request, response, chain)
190    finally:
191        _add_caching_headers(response, max_age=0)
Note: See TracBrowser for help on using the browser.