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]