Created
October 2, 2018 05:18
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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