Skip to content

Instantly share code, notes, and snippets.

@mx-moth
Created October 2, 2018 05:18
Show Gist options
  • Save mx-moth/1f4d9284e4f4d4f545439577c0ca6300 to your computer and use it in GitHub Desktop.
Save mx-moth/1f4d9284e4f4d4f545439577c0ca6300 to your computer and use it in GitHub Desktop.
A nice decorator for Flask views that supports ETag and Last-Modified headers, responding with a 304 Not Modified where possible
import logging
from functools import wraps
from flask import Response, make_response, request
from werkzeug.http import parse_date
def check_empty_iterator(iterator, message="Iterator was not empty"):
try:
next(iterator)
except StopIteration:
pass
else:
raise RuntimeError(message)
def etag_cache(func):
"""
Handles ETag and Last-Modified caching for a view. The view should be
a generator that yields two items: a dict of caching headers for the
response, which should include any caching directives, and optionally ETag
and Last-Modified headers; and the response to send to the client. The
response will only be generated if the ETags do not match, or the
Last-Modified date has changed.
.. code-block:: python
@etag_cache
def my_view():
# Do any authentication required
authenticate_request(request)
# Yield a dict of caching headers
yield {
'ETag': compute_etag_for_request(request),
'Last-Modified': compute_last_modified_for_request(request),
'Cache-Control': 'max-age=60',
}
# Make the response
yield Response("Hello, world!")
In the case where the ETags match, the generator will be discarded, and the
remainer of the view function will not run. If the ETags do not match, or
are missing from the request, the next value yielded from the view will be
used as the response object. The generator is then called one last time, to
ensure it is empty. If the generator yields a third value, an error is
thrown. To prevent the generator yielding a third value from a branch, for
example, an empty ``return`` will terminate the generator early:
.. code-block:: python
def my_view():
# Yield a dict of caching headers
yield {
'ETag': compute_etag_for_request(request),
'Cache-Control': 'max-age=60',
}
# Return some other value in certain circumstances
if some_condition():
yeild Response("Reticulating splines")
return # Finish the generator early
# Make the typical response
yield Response("Hello, world!")
"""
@wraps(func)
def wrapper(*args, **kwargs):
response = None
gen = func(*args, **kwargs)
# Get the caching headers from the view function
headers = next(gen)
# Check for ETag caching
etag = headers.pop('ETag', None)
if etag in request.if_none_match:
response = Response(status=304)
# Check for Last-Modified caching
last_modified = headers.get('Last-Modified')
if last_modified and request.if_modified_since:
last_modified = parse_date(last_modified)
if last_modified <= request.if_modified_since:
response = Response(status=304)
# No valid caching found, so get the real response
if response is None:
response = make_response(next(gen))
check_empty_iterator(gen, "ETag view generator had not finished")
# Set the caching headers
if etag:
response.set_etag(etag)
for key, value in headers.items():
response.headers[key] = value
return response
return wrapper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment