Added django.utils.cache, from Hugo's #580 patch. Refs #580.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@808 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Adrian Holovaty 2005-10-08 23:19:21 +00:00
parent 71564b4349
commit 8aa98af6bb
1 changed files with 155 additions and 0 deletions

155
django/utils/cache.py Normal file
View File

@ -0,0 +1,155 @@
"""
This module contains helper functions and decorators for controlling caching.
It does so by managing the "Vary" header of responses. It includes functions
to patch the header of response objects directly and decorators that change
functions to do that header-patching themselves.
For information on the Vary header, see:
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
Essentially, the "Vary" HTTP header defines which headers a cache should take
into account when building its cache key. Requests with the same path but
different header content for headers named in "Vary" need to get different
cache keys to prevent delivery of wrong content.
A example: i18n middleware would need to distinguish caches by the
"Accept-language" header.
"""
import datetime, md5, re
from django.conf import settings
from django.core.cache import cache
vary_delim_re = re.compile(r',\s*')
def patch_response_headers(response, cache_timeout=None):
"""
Adds some useful headers to the given HttpResponse object:
ETag, Last-Modified, Expires and Cache-Control
Each header is only added if it isn't already set.
cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
by default.
"""
if cache_timeout is None:
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
now = datetime.datetime.utcnow()
expires = now + datetime.timedelta(0, cache_timeout)
if not response.has_header('ETag'):
response['ETag'] = md5.new(response.content).hexdigest()
if not response.has_header('Last-Modified'):
response['Last-Modified'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT')
if not response.has_header('Expires'):
response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
if not response.has_header('Cache-Control'):
response['Cache-Control'] = 'max-age=%d' % cache_timeout
def patch_vary_headers(response, newheaders):
"""
Adds (or updates) the "Vary" header in the given HttpResponse object.
newheaders is a list of header names that should be in "Vary". Existing
headers in "Vary" aren't removed.
"""
# Note that we need to keep the original order intact, because cache
# implementations may rely on the order of the Vary contents in, say,
# computing an MD5 hash.
vary = []
if response.has_header('Vary'):
vary = vary_delim_re.split(response['Vary'])
oldheaders = dict([(el.lower(), 1) for el in vary])
for newheader in newheaders:
if not newheader.lower() in oldheaders:
vary.append(newheader)
response['Vary'] = ', '.join(vary)
def vary_on_headers(*headers):
"""
A view decorator that adds the specified headers to the Vary header of the
response. Usage:
@vary_on_headers('Cookie', 'Accept-language')
def index(request):
...
Note that the header names are not case-sensitive.
"""
def decorator(func):
def inner_func(*args, **kwargs):
response = func(*args, **kwargs)
patch_vary_headers(response, headers)
return response
return inner_func
return decorator
def vary_on_cookie(func):
"""
A view decorator that adds "Cookie" to the Vary header of a response. This
indicates that a page's contents depends on cookies. Usage:
@vary_on_cookie
def index(request):
...
"""
def inner_func(*args, **kwargs):
response = func(*args, **kwargs)
patch_vary_headers(response, ('Cookie',))
return response
return inner_func
def _generate_cache_key(request, headerlist, key_prefix):
"Returns a cache key from the headers given in the header list."
ctx = md5.new()
for header in headerlist:
value = request.META.get(header, None)
if value is not None:
ctx.update(value)
return 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, ctx.hexdigest())
def get_cache_key(request, key_prefix=None):
"""
Returns a cache key based on the request path. It can be used in the
request phase because it pulls the list of headers to take into account
from the global path registry and uses those to build a cache key to check
against.
If there is no headerlist stored, the page needs to be rebuilt, so this
function returns None.
"""
if key_prefix is None:
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path)
headerlist = cache.get(cache_key, None)
if headerlist is not None:
return _generate_cache_key(request, headerlist, key_prefix)
else:
return None
def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
"""
Learns what headers to take into account for some request path from the
response object. It stores those headers in a global path registry so that
later access to that path will know what headers to take into account
without building the response object itself. The headers are named in the
Vary header of the response, but we want to prevent response generation.
The list of headers to use for cache key generation is stored in the same
cache as the pages themselves. If the cache ages some data out of the
cache, this just means that we have to build the response once to get at
the Vary header and so at the list of headers to use for the cache key.
"""
if key_prefix is None:
key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
if cache_timeout is None:
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path)
if response.has_header('Vary'):
headerlist = ['HTTP_'+header.upper().replace('-', '_') for header in vary_delim_re.split(response['Vary'])]
cache.set(cache_key, headerlist, cache_timeout)
return _generate_cache_key(request, headerlist, key_prefix)
else:
# if there is no Vary header, we still need a cache key
# for the request.path
cache.set(cache_key, [], cache_timeout)
return _generate_cache_key(request, [], key_prefix)