Move cached_endpoint to decorators.
This commit is contained in:
		
							parent
							
								
									11b846a3e0
								
							
						
					
					
						commit
						bb82c1e4e7
					
				
					 4 changed files with 64 additions and 69 deletions
				
			
		|  | @ -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. | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue