File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1- from sqlalchemy .orm import Session
21from sqlalchemy import select
2+ from sqlalchemy .orm import Session
3+
34from 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
Original file line number Diff line number Diff line change 77from app .database import SessionLocal
88from app .models import RequestLog
99from contextlib import asynccontextmanager
10+ from app .redis_client import redis_client
1011
1112@asynccontextmanager
1213async def lifespan (app : FastAPI ):
Original file line number Diff line number Diff line change 33from app .models import APIKey
44from app .rate_limiter import TokenBucket
55
6-
7-
86def 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
Original file line number Diff line number Diff line change 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 )
Original file line number Diff line number Diff line change @@ -22,6 +22,7 @@ Pygments==2.20.0
2222pytest == 9.0.3
2323python-dotenv == 1.2.2
2424PyYAML == 6.0.3
25+ redis == 8.0.0
2526respx == 0.23.1
2627SQLAlchemy == 2.0.49
2728starlette == 1.0.0
You can’t perform that action at this time.
0 commit comments