Move cached_endpoint to decorators.

This commit is contained in:
voussoir 2021-01-05 12:43:39 -08:00
parent 11b846a3e0
commit bb82c1e4e7
4 changed files with 64 additions and 69 deletions

View file

@ -1,70 +1,7 @@
import flask; from flask import request
import functools
import time
from voussoirkit import cacheclass from voussoirkit import cacheclass
from voussoirkit import passwordy
import etiquette import etiquette
def cached_endpoint(max_age):
'''
The cached_endpoint decorator can be used on slow endpoints that don't need
to be constantly updated or endpoints that produce large, static responses.
WARNING: The return value of the endpoint is shared with all users.
You should never use this cache on an endpoint that provides private
or personalized data, and you should not try to pass other headers through
the response.
When the function is run, its return value is stored and a random etag is
generated so that subsequent runs can respond with 304. This way, large
response bodies do not need to be transmitted often.
Given a nonzero max_age, the endpoint will only be run once per max_age
seconds on a global basis (not per-user). This way, you can prevent a slow
function from being run very often. In-between requests will just receive
the previous return value (still using 200 or 304 as appropriate for the
client's provided etag).
An example use case would be large-sized data dumps that don't need to be
precisely up to date every time.
'''
state = {
'max_age': max_age,
'stored_value': None,
'stored_etag': None,
'headers': {'ETag': None, 'Cache-Control': f'max-age={max_age}'},
'last_run': 0,
}
def wrapper(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
if (not state['max_age']) or (time.time() - state['last_run'] > state['max_age']):
value = function(*args, **kwargs)
if isinstance(value, flask.Response):
if value.headers.get('Content-Type'):
state['headers']['Content-Type'] = value.headers.get('Content-Type')
value = value.response
if value != state['stored_value']:
state['stored_value'] = value
state['stored_etag'] = passwordy.random_hex(20)
state['headers']['ETag'] = state['stored_etag']
state['last_run'] = time.time()
else:
value = state['stored_value']
client_etag = request.headers.get('If-None-Match', None)
if client_etag == state['stored_etag']:
response = flask.Response(status=304, headers=state['headers'])
else:
response = flask.Response(value, status=200, headers=state['headers'])
return response
return wrapped
return wrapper
class FileCacheManager: class FileCacheManager:
''' '''
The FileCacheManager serves ETag and Cache-Control headers for disk files. The FileCacheManager serves ETag and Cache-Control headers for disk files.

View file

@ -1,11 +1,71 @@
import flask import flask; from flask import request
from flask import request
import functools import functools
import time
from voussoirkit import passwordy
import etiquette import etiquette
from . import jsonify from . import jsonify
def cached_endpoint(max_age):
'''
The cached_endpoint decorator can be used on slow endpoints that don't need
to be constantly updated or endpoints that produce large, static responses.
WARNING: The return value of the endpoint is shared with all users.
You should never use this cache on an endpoint that provides private
or personalized data, and you should not try to pass other headers through
the response.
When the function is run, its return value is stored and a random etag is
generated so that subsequent runs can respond with 304. This way, large
response bodies do not need to be transmitted often.
Given a nonzero max_age, the endpoint will only be run once per max_age
seconds on a global basis (not per-user). This way, you can prevent a slow
function from being run very often. In-between requests will just receive
the previous return value (still using 200 or 304 as appropriate for the
client's provided etag).
An example use case would be large-sized data dumps that don't need to be
precisely up to date every time.
'''
state = {
'max_age': max_age,
'stored_value': None,
'stored_etag': None,
'headers': {'ETag': None, 'Cache-Control': f'max-age={max_age}'},
'last_run': 0,
}
def wrapper(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
if (not state['max_age']) or (time.time() - state['last_run'] > state['max_age']):
value = function(*args, **kwargs)
if isinstance(value, flask.Response):
if value.headers.get('Content-Type'):
state['headers']['Content-Type'] = value.headers.get('Content-Type')
value = value.response
if value != state['stored_value']:
state['stored_value'] = value
state['stored_etag'] = passwordy.random_hex(20)
state['headers']['ETag'] = state['stored_etag']
state['last_run'] = time.time()
else:
value = state['stored_value']
client_etag = request.headers.get('If-None-Match', None)
if client_etag == state['stored_etag']:
response = flask.Response(status=304, headers=state['headers'])
else:
response = flask.Response(value, status=200, headers=state['headers'])
return response
return wrapped
return wrapper
def catch_etiquette_exception(function): def catch_etiquette_exception(function):
''' '''
If an EtiquetteException is raised, automatically catch it and convert it If an EtiquetteException is raised, automatically catch it and convert it

View file

@ -6,7 +6,6 @@ from voussoirkit import stringtools
import etiquette import etiquette
from .. import caching
from .. import common from .. import common
from .. import decorators from .. import decorators
from .. import jsonify from .. import jsonify
@ -159,7 +158,7 @@ def post_album_edit(album_id):
# Album listings ################################################################################### # Album listings ###################################################################################
@site.route('/all_albums.json') @site.route('/all_albums.json')
@caching.cached_endpoint(max_age=0) @decorators.cached_endpoint(max_age=0)
def get_all_album_names(): def get_all_album_names():
all_albums = {album.display_name: album.id for album in common.P.get_albums()} all_albums = {album.display_name: album.id for album in common.P.get_albums()}
response = {'updated': int(time.time()), 'albums': all_albums} response = {'updated': int(time.time()), 'albums': all_albums}

View file

@ -3,7 +3,6 @@ import time
import etiquette import etiquette
from .. import caching
from .. import common from .. import common
from .. import decorators from .. import decorators
from .. import jsonify from .. import jsonify
@ -65,7 +64,7 @@ def post_tag_remove_child(tagname):
# Tag listings ##################################################################################### # Tag listings #####################################################################################
@site.route('/all_tags.json') @site.route('/all_tags.json')
@caching.cached_endpoint(max_age=0) @decorators.cached_endpoint(max_age=0)
def get_all_tag_names(): def get_all_tag_names():
all_tags = list(common.P.get_all_tag_names()) all_tags = list(common.P.get_all_tag_names())
all_synonyms = common.P.get_all_synonyms() all_synonyms = common.P.get_all_synonyms()