HTTP Cache Using Redis#
Warning
You need to install dependencies to use The HTTP Cache.
Overview#
HTTP caching occurs when the browser stores local copies of web resources for faster retrieval the next time the resource is required. As your application serves resources it can attach cache headers to the response specifying the desired cache behavior.
When an item is fully cached, the browser may choose to not contact the server at all and simply use its own cached copy:
HTTP cache headers#
There are two primary cache headers, Cache-Control
and Expires
.
Cache-Control#
The Cache-Control
header is the most important header to set as it effectively switches on
caching in the browser. With this header in place, and set with a value that enables caching, the browser will cache the file for as long as specified. Without this header the browser will re-request the file on each subsequent request.
Expires#
When accompanying the Cache-Control
header, Expires simply sets a date from which the cached resource should no longer be considered valid. From this date forward the browser will request a fresh copy of the resource.
This Introduction to HTTP Caching is based on the HTTP Caching Guide.
AuthX provide a simple HTTP caching model designed to work with FastAPI,
How to install#
Make sure to have the necessary dependencies installed:
Initialize the cache#
import os
import redis
from authx_extra.cache import HTTPCache
from pytz import timezone
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/3")
redis_client = redis.Redis.from_url(REDIS_URL)
africa_Casablanca = timezone('Africa/Casablanca')
HTTPCache.init(redis_url=REDIS_URL, namespace='test_namespace', tz=africa_Casablanca)
The tz
attribute becomes import when the cache
decorator relies on the expire_end_of_day
and expire_end_of_week
attributes to expire the cache key.
Define your controllers#
The ttl_in_seconds
expires the cache in 180 seconds. There are other approaches to take with helpers like expire_end_of_day
and expires_end_of_week
from datetime import datetime
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from authx_extra.cache import HTTPCache, cache
@app.get("/b/home")
@cache(key="b.home", ttl_in_seconds=180)
async def home(request: Request, response: Response):
return JSONResponse({"page": "home", "datetime": str(datetime.utcnow())})
@app.get("/b/welcome")
@cache(key="b.home", end_of_week=True)
async def home(request: Request, response: Response):
return JSONResponse({"page": "welcome", "datetime": str(datetime.utcnow())})
Building keys from parameter objects#
While it's always possible to explicitly pass keys onto the key
attribute, there are scenarios where the keys need to be built based on the parameters received by the controller method. For instance, in an authenticated API where the user_id
is fetched as a controller Depends
argument.
class User:
id: str = "112358"
user = User()
@app.get("/b/logged-in")
@cache(key="b.logged_in.{}", obj="user", obj_attr="id")
async def logged_in(request: Request, response: Response, user=user):
return JSONResponse(
{"page": "home", "user": user.id, "datetime": str(datetime.utcnow())}
)
In the example above, the key allows room for a dynamic attribute fetched from the object user
. The key eventually becomes b.logged_in.112358
if the user.id
returns 112358
Explicitly invalidating the cache#
The cache invalidation can be managed using the @invalidate_cache
decorator.
class User:
id: str = "112358"
user = User()
@app.post("/b/logged-in")
@invalidate_cache(
key="b.logged_in.{}", obj="user", obj_attr="id", namespace="test_namespace"
)
async def post_logged_in(request: Request, response: Response, user=user):
return JSONResponse(
{"page": "home", "user": user.id, "datetime": str(datetime.utcnow())}
)
Invalidating more than one key at a time#
The cache invalidation decorator allows for multiple keys to be invalidated in the same call. However, the it assumes that the object attributes generated apply all keys.
class User:
id: str = "112358"
user = User()
@app.post("/b/logged-in")
@invalidate_cache(
keys=["b.logged_in.{}", "b.profile.{}"], obj="user", obj_attr="id", namespace="test_namespace"
)
async def post_logged_in(request: Request, response: Response, user=user):
return JSONResponse(
{"page": "home", "user": user.id, "datetime": str(datetime.utcnow())}
)
Computing ttl
dynamically for cache keys using a Callable
#
A callable can be passed as part of the decorator to dynamically compute what the ttl for a cache key should be. For example
async def my_ttl_callable() -> int:
return 3600
@app.get('/b/ttl_callable')
@cache(key='b.ttl_callable_expiry', ttl_func=my_ttl_callable)
async def path_with_ttl_callable(request: Request, response: Response):
return JSONResponse(
{"page": "path_with_ttl_callable", "datetime": str(datetime.utcnow())}
)
The ttl_func
is always assumed to be an async method
Caching methods that aren't controllers#
HTTPCache works exactly the same way with regular methods. The example below explains usage of the cache in service objects and application services.
import os
import redis
from authx_extra.cache import HTTPCache, cache, invalidate_cache
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/3")
redis_client = redis.Redis.from_url(REDIS_URL)
class User:
id: str = "112358"
user = User()
HTTPCache.init(redis_url=REDIS_URL, namespace='test_namespace')
@cache(key='cache.me', ttl_in_seconds=360)
async def cache_me(x:int, invoke_count:int):
invoke_count = invoke_count + 1
result = x * 2
return [result, invoke_count]