diff --git a/CHANGELOG.md b/CHANGELOG.md index 7815e28..90a8409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `vary_by_cookies()` and `vary_by_headers()` decorators for Flask +- Added `cacheable_duration_cloudfront()` decorator for adding `Cache-Control` headers that are Cloudfront compatible ### Changed diff --git a/docs/flask.md b/docs/flask.md index abc675f..25e37da 100644 --- a/docs/flask.md +++ b/docs/flask.md @@ -10,7 +10,12 @@ A set of decorators to manage the `Cache-Control` response header of a route. ```python from flask import Flask -from tna_utilities.flask import cacheable_duration, do_not_cache, set_cache_control +from tna_utilities.flask import ( + cacheable_duration, + do_not_cache, + set_cache_control, + cacheable_duration_cloudfront +) app = Flask(__name__) @@ -32,6 +37,11 @@ def not_cachable(): @set_cache_control("private, max-age=120") def custom_cache(): return "Cache me in private caches for up to 2 minutes" + +@app.route("/cloudfront-cache/") +@cacheable_duration_cloudfront(3600, 86400) +def cloudfront_cache(): + return "Cache me in client caches for up to an hour and in Cloudfront for up to a day" ``` ### Vary diff --git a/tests/test_flask_cache_control.py b/tests/test_flask_cache_control.py index 0d5b927..0bdb32c 100644 --- a/tests/test_flask_cache_control.py +++ b/tests/test_flask_cache_control.py @@ -4,6 +4,7 @@ from tna_utilities.flask import ( cacheable_duration, + cacheable_duration_cloudfront, do_not_cache, set_cache_control, vary_by_cookies, @@ -115,3 +116,59 @@ def index(): rv.headers["Vary"], "Accept-Encoding, User-Agent", ) + + def test_cache_control_and_vary_by_headers_route(self): + @self.app.route("/") + @set_cache_control("private, max-age=120") + @vary_by_headers("Accept-Encoding, User-Agent") + def index(): + return "OK" + + rv = self.test_client.get("/") + + self.assertEqual(rv.status_code, 200) + self.assertIn("Vary", rv.headers) + self.assertEqual( + rv.headers["Vary"], + "Accept-Encoding, User-Agent", + ) + self.assertIn("Cache-Control", rv.headers) + self.assertEqual( + rv.headers["Cache-Control"], + "private, max-age=120", + ) + + def test_cacheable_duration_cloudfront_route(self): + @self.app.route("/") + @cacheable_duration_cloudfront(client_seconds=60, cloudfront_seconds=120) + def index(): + return "OK" + + rv = self.test_client.get("/") + + self.assertEqual(rv.status_code, 200) + self.assertIn("Cache-Control", rv.headers) + self.assertEqual( + rv.headers["Cache-Control"], + "public, max-age=60, s-maxage=120", + ) + + def test_cacheable_duration_cloudfront_route_extras(self): + @self.app.route("/") + @cacheable_duration_cloudfront( + client_seconds=60, + cloudfront_seconds=120, + stale_while_revalidate_seconds=30, + stale_if_error_seconds=15, + ) + def index(): + return "OK" + + rv = self.test_client.get("/") + + self.assertEqual(rv.status_code, 200) + self.assertIn("Cache-Control", rv.headers) + self.assertEqual( + rv.headers["Cache-Control"], + "public, max-age=60, s-maxage=120, stale-while-revalidate=30, stale-if-error=15", + ) diff --git a/tna_utilities/flask/__init__.py b/tna_utilities/flask/__init__.py index 1cd3322..336918b 100644 --- a/tna_utilities/flask/__init__.py +++ b/tna_utilities/flask/__init__.py @@ -1,5 +1,6 @@ from tna_utilities.flask.cache_control import ( cacheable_duration, + cacheable_duration_cloudfront, do_not_cache, set_cache_control, vary_by_cookies, diff --git a/tna_utilities/flask/cache_control.py b/tna_utilities/flask/cache_control.py index ba76cd6..9f4a389 100644 --- a/tna_utilities/flask/cache_control.py +++ b/tna_utilities/flask/cache_control.py @@ -19,6 +19,30 @@ def cacheable_duration(seconds: int = 3600): return set_cache_control(f"public, max-age={seconds}") +def cacheable_duration_cloudfront( + client_seconds: int = 3600, + cloudfront_seconds: int = 3600, + stale_while_revalidate_seconds: int = 0, + stale_if_error_seconds: int = 0, +): + """ + Decorator to set Cache-Control headers to allow caching of the response for a specified duration with consideration for CloudFront's caching behavior. + See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html for details on CloudFront's caching behavior. + """ + + cache_control_value = f"public, max-age={client_seconds}" + if cloudfront_seconds > 0 and cloudfront_seconds != client_seconds: + cache_control_value += f", s-maxage={cloudfront_seconds}" + if stale_while_revalidate_seconds > 0: + cache_control_value += ( + f", stale-while-revalidate={stale_while_revalidate_seconds}" + ) + if stale_if_error_seconds > 0: + cache_control_value += f", stale-if-error={stale_if_error_seconds}" + + return set_cache_control(cache_control_value) + + def set_cache_control(instructions: str): """ Decorator to set Cache-Control headers with custom instructions provided as a string.