Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 11 additions & 1 deletion docs/flask.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions tests/test_flask_cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from tna_utilities.flask import (
cacheable_duration,
cacheable_duration_cloudfront,
do_not_cache,
set_cache_control,
vary_by_cookies,
Expand Down Expand Up @@ -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",
)
1 change: 1 addition & 0 deletions tna_utilities/flask/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
24 changes: 24 additions & 0 deletions tna_utilities/flask/cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading