11from fastapi import Header , HTTPException
22import hishel
3+ import httpx
34from app .config import EndpointConfig
45from app .config import TornApiConfig
56from pydantic import BaseModel
7+ from .config import settings
8+ from redis import Redis
9+
610
711class ApiErrorResponse (BaseModel ):
812 error : dict [str , object ]
913
1014
1115api_config = TornApiConfig ()
16+ # Create base storage without TTL (will be set per request)
17+ base_storage = hishel .RedisStorage (
18+ client = Redis (host = settings .REDIS_HOST , port = settings .REDIS_PORT )
19+ )
1220
1321BASE_HEADERS = {"User-Agent" : "TornApiClient/1.0" }
1422
1523
16- async def fetch_torn_api (api_key : str , endpoint : EndpointConfig , params : dict ) -> dict :
24+ async def fetch_torn_api (api_key : str , endpoint : EndpointConfig , params : dict , cache : bool = True , ttl : int = 300 ) -> dict :
1725 """Helper function to make requests to the Torn API with caching.
1826
1927 Args:
2028 endpoint (EndpointConfig): The endpoint configuration from TornApiConfig.
2129 params (dict): Query parameters to include in the request.
30+ cache (bool): Whether to cache or not. Defaults to True.
31+ ttl (int): How long to keep cache in seconds. Defaults to 300 (5 minutes).
2232
2333 Returns:
2434 dict: The JSON response from the Torn API.
@@ -27,38 +37,81 @@ async def fetch_torn_api(api_key: str ,endpoint: EndpointConfig, params: dict) -
2737 HTTPException: If the request fails or the API returns an error.
2838 """
2939 headers = {** BASE_HEADERS , ** api_config .get_auth_header (api_key = api_key )}
30-
31- # Configure Hishel's AsyncCacheClient with in-memory storage
32- async with hishel .AsyncCacheClient (
33- ) as client :
34- try :
35- response = await client .get (
36- api_config .get_endpoint_url (endpoint ),
37- params = {k : v for k , v in params .items () if v is not None },
38- headers = headers ,
39- timeout = 10.0 ,
40- )
41- response .raise_for_status ()
42- return response .json ()
43- except hishel .HTTPStatusError as e :
44- try :
45- error_data = response .json ()
46- raise HTTPException (
47- status_code = response .status_code ,
48- detail = ApiErrorResponse (** error_data ).error ,
40+
41+ # Set cache control headers
42+ if cache :
43+ headers ["Cache-Control" ] = f"max-age={ ttl } "
44+ else :
45+ headers ["Cache-Control" ] = "no-cache"
46+
47+ try :
48+ if cache :
49+ # Use hishel with caching (httpx-based)
50+ with hishel .CacheClient (storage = base_storage ) as client :
51+ response = client .get (
52+ api_config .get_endpoint_url (endpoint ),
53+ params = params ,
54+ headers = headers ,
55+ timeout = 10.0 ,
4956 )
50- except ValueError :
51- raise HTTPException (
52- status_code = response .status_code ,
53- detail = "Torn API returned an error" ,
57+ else :
58+ # Bypass cache completely using regular httpx
59+ async with httpx .AsyncClient () as client :
60+ response = await client .get (
61+ api_config .get_endpoint_url (endpoint ),
62+ params = params ,
63+ headers = headers ,
64+ timeout = 10.0 ,
5465 )
55- except hishel .RequestError as e :
56- raise HTTPException (status_code = 500 , detail = f"Request error: { str (e )} " )
66+
67+ response .raise_for_status ()
68+ data = response .json ()
69+
70+ if "error" in data :
71+ raise HTTPException (
72+ status_code = 403 ,
73+ detail = "Access level of this key is not high enough" ,
74+ )
75+ return data
76+
77+ except HTTPException :
78+ # Re-raise HTTP exceptions
79+ raise
80+ except httpx .HTTPStatusError as e :
81+ # Handle HTTP errors (4xx, 5xx)
82+ try :
83+ error_data = e .response .json ()
84+ raise HTTPException (
85+ status_code = e .response .status_code ,
86+ detail = ApiErrorResponse (** error_data ).error ,
87+ )
88+ except (ValueError , AttributeError ):
89+ raise HTTPException (
90+ status_code = e .response .status_code ,
91+ detail = f"Torn API returned an error: { e .response .text } " ,
92+ )
93+ except httpx .RequestError as e :
94+ # Handle connection errors, timeouts, etc.
95+ raise HTTPException (
96+ status_code = 500 ,
97+ detail = f"Failed to connect to Torn API: { str (e )} "
98+ )
99+ except Exception as e :
100+ print (f"Unexpected error fetching Torn API: { e } " )
101+ raise HTTPException (
102+ status_code = 500 ,
103+ detail = "An unexpected error occurred while calling Torn API"
104+ )
105+
57106
58107async def get_api_key (authorization : str = Header (...)):
59108 """Extract API key from Authorization header in 'Bearer' format."""
60109 if not authorization .startswith ("Bearer " ):
61- raise HTTPException (status_code = 401 , detail = "Invalid authorization header format. Expected 'Bearer <api_key>'" )
110+ raise HTTPException (
111+ status_code = 401 ,
112+ detail = "Invalid authorization header format. Expected 'Bearer <api_key>'" ,
113+ )
62114 return authorization .replace ("Bearer " , "" ).strip ()
63115
116+
64117__all__ = ["api_config" , "fetch_torn_api" , "get_api_key" ]
0 commit comments