Skip to content

Commit 500c531

Browse files
feat: add redis for api caching
1 parent aa1d044 commit 500c531

5 files changed

Lines changed: 82 additions & 10 deletions

File tree

‎app/crud.py‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1-
from sqlalchemy.orm import Session
21
from sqlalchemy import select
2+
from sqlalchemy.orm import Session
3+
34
from app.models import APIKey
5+
from app.redis_client import redis_client, get_cached_api_key, cache_api_key
6+
7+
8+
def get_api_key(db: Session, key_value: str):
9+
cached_key = get_cached_api_key(redis_client, key_value)
10+
11+
if cached_key is not None:
12+
return cached_key
13+
14+
api_key = db.execute(
15+
select(APIKey).where(APIKey.key == key_value)
16+
).scalar_one_or_none()
17+
18+
if api_key is None:
19+
return None
20+
21+
cache_api_key(redis_client, key_value, api_key)
422

5-
def get_api_key(db: Session, key_value: str) -> APIKey | None:
6-
return db.execute(select(APIKey).where(APIKey.key == key_value)).scalar_one_or_none()
23+
return api_key

‎app/main.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from app.database import SessionLocal
88
from app.models import RequestLog
99
from contextlib import asynccontextmanager
10+
from app.redis_client import redis_client
1011

1112
@asynccontextmanager
1213
async def lifespan(app: FastAPI):

‎app/rate_limit_dependency.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,23 @@
33
from app.models import APIKey
44
from app.rate_limiter import TokenBucket
55

6-
7-
86
def enforce_rate_limit(
97
request: Request,
108
api_key: APIKey = Depends(require_api_key),
119
) -> APIKey:
1210
buckets = request.app.state.buckets
13-
if api_key.key not in buckets:
14-
buckets[api_key.key] = TokenBucket(
11+
12+
bucket_key = f"api_key:{api_key.id}"
13+
14+
if bucket_key not in buckets:
15+
buckets[bucket_key] = TokenBucket(
1516
capacity=api_key.rate_limit_capacity,
1617
refill_rate=api_key.rate_limit_refill_rate,
1718
)
18-
bucket = buckets[api_key.key]
19+
20+
bucket = buckets[bucket_key]
21+
1922
if not bucket.consume():
2023
raise HTTPException(status_code=429, detail="Rate limit exceeded")
21-
return api_key
22-
24+
25+
return api_key

‎app/redis_client.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import hashlib
2+
import json
3+
import os
4+
from types import SimpleNamespace
5+
6+
import redis
7+
from dotenv import load_dotenv
8+
9+
10+
load_dotenv()
11+
12+
redis_client = redis.Redis.from_url(
13+
os.getenv("REDIS_URL", "redis://localhost:6379/0"),
14+
decode_responses=True,
15+
)
16+
17+
18+
def api_key_cache_key(raw_api_key: str) -> str:
19+
hashed = hashlib.sha256(raw_api_key.encode()).hexdigest()
20+
return f"api_key:{hashed}"
21+
22+
23+
def cache_api_key(redis_client, key_value: str, api_key) -> None:
24+
cache_key = api_key_cache_key(key_value)
25+
26+
data = {
27+
"id": api_key.id,
28+
"name": api_key.name,
29+
"rate_limit_capacity": api_key.rate_limit_capacity,
30+
"rate_limit_refill_rate": api_key.rate_limit_refill_rate,
31+
}
32+
33+
redis_client.setex(
34+
cache_key,
35+
300,
36+
json.dumps(data),
37+
)
38+
39+
40+
def get_cached_api_key(redis_client, key_value: str):
41+
cache_key = api_key_cache_key(key_value)
42+
43+
cached = redis_client.get(cache_key)
44+
45+
if cached is None:
46+
return None
47+
48+
data = json.loads(cached)
49+
50+
return SimpleNamespace(**data)

‎requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Pygments==2.20.0
2222
pytest==9.0.3
2323
python-dotenv==1.2.2
2424
PyYAML==6.0.3
25+
redis==8.0.0
2526
respx==0.23.1
2627
SQLAlchemy==2.0.49
2728
starlette==1.0.0

0 commit comments

Comments
 (0)