1+ import time
2+ from typing import Any
3+
4+
5+ class CacheEntry :
6+ """A cache entry with TTL support."""
7+
8+ def __init__ (self , data : list [dict [str , Any ]], ttl_seconds : int = 3600 ):
9+ self .data = data
10+ self .created_at = time .time ()
11+ self .ttl_seconds = ttl_seconds
12+
13+ def is_expired (self ) -> bool :
14+ """Check if the cache entry has expired."""
15+ return time .time () - self .created_at > self .ttl_seconds
16+
17+
118class Cache :
2- """In-memory cache for API responses."""
19+ """In-memory cache for API responses with TTL support."""
20+
21+ # Default TTL: 1 hour for most data, 24 hours for less volatile data
22+ DEFAULT_TTL = 3600 # 1 hour
23+ METRICS_TTL = 86400 # 24 hours (metrics don't change frequently)
24+ NEWS_TTL = 1800 # 30 minutes (news is more time-sensitive)
325
426 def __init__ (self ):
5- self ._prices_cache : dict [str , list [ dict [ str , any ]] ] = {}
6- self ._financial_metrics_cache : dict [str , list [ dict [ str , any ]] ] = {}
7- self ._line_items_cache : dict [str , list [ dict [ str , any ]] ] = {}
8- self ._insider_trades_cache : dict [str , list [ dict [ str , any ]] ] = {}
9- self ._company_news_cache : dict [str , list [ dict [ str , any ]] ] = {}
27+ self ._prices_cache : dict [str , CacheEntry ] = {}
28+ self ._financial_metrics_cache : dict [str , CacheEntry ] = {}
29+ self ._line_items_cache : dict [str , CacheEntry ] = {}
30+ self ._insider_trades_cache : dict [str , CacheEntry ] = {}
31+ self ._company_news_cache : dict [str , CacheEntry ] = {}
1032
1133 def _merge_data (self , existing : list [dict ] | None , new_data : list [dict ], key_field : str ) -> list [dict ]:
1234 """Merge existing and new data, avoiding duplicates based on a key field."""
@@ -21,45 +43,89 @@ def _merge_data(self, existing: list[dict] | None, new_data: list[dict], key_fie
2143 merged .extend ([item for item in new_data if item [key_field ] not in existing_keys ])
2244 return merged
2345
24- def get_prices (self , ticker : str ) -> list [dict [str , any ]] | None :
25- """Get cached price data if available."""
26- return self ._prices_cache .get (ticker )
27-
28- def set_prices (self , ticker : str , data : list [dict [str , any ]]):
29- """Append new price data to cache."""
30- self ._prices_cache [ticker ] = self ._merge_data (self ._prices_cache .get (ticker ), data , key_field = "time" )
31-
32- def get_financial_metrics (self , ticker : str ) -> list [dict [str , any ]]:
33- """Get cached financial metrics if available."""
34- return self ._financial_metrics_cache .get (ticker )
35-
36- def set_financial_metrics (self , ticker : str , data : list [dict [str , any ]]):
37- """Append new financial metrics to cache."""
38- self ._financial_metrics_cache [ticker ] = self ._merge_data (self ._financial_metrics_cache .get (ticker ), data , key_field = "report_period" )
39-
40- def get_line_items (self , ticker : str ) -> list [dict [str , any ]] | None :
41- """Get cached line items if available."""
42- return self ._line_items_cache .get (ticker )
43-
44- def set_line_items (self , ticker : str , data : list [dict [str , any ]]):
45- """Append new line items to cache."""
46- self ._line_items_cache [ticker ] = self ._merge_data (self ._line_items_cache .get (ticker ), data , key_field = "report_period" )
47-
48- def get_insider_trades (self , ticker : str ) -> list [dict [str , any ]] | None :
49- """Get cached insider trades if available."""
50- return self ._insider_trades_cache .get (ticker )
51-
52- def set_insider_trades (self , ticker : str , data : list [dict [str , any ]]):
53- """Append new insider trades to cache."""
54- self ._insider_trades_cache [ticker ] = self ._merge_data (self ._insider_trades_cache .get (ticker ), data , key_field = "filing_date" ) # Could also use transaction_date if preferred
55-
56- def get_company_news (self , ticker : str ) -> list [dict [str , any ]] | None :
57- """Get cached company news if available."""
58- return self ._company_news_cache .get (ticker )
59-
60- def set_company_news (self , ticker : str , data : list [dict [str , any ]]):
61- """Append new company news to cache."""
62- self ._company_news_cache [ticker ] = self ._merge_data (self ._company_news_cache .get (ticker ), data , key_field = "date" )
46+ def _cleanup_expired (self , cache_dict : dict [str , CacheEntry ]) -> None :
47+ """Remove expired entries from a cache dictionary."""
48+ expired_keys = [key for key , entry in cache_dict .items () if entry .is_expired ()]
49+ for key in expired_keys :
50+ del cache_dict [key ]
51+
52+ def get_prices (self , ticker : str ) -> list [dict [str , Any ]] | None :
53+ """Get cached price data if available and not expired."""
54+ self ._cleanup_expired (self ._prices_cache )
55+ entry = self ._prices_cache .get (ticker )
56+ if entry and not entry .is_expired ():
57+ return entry .data
58+ return None
59+
60+ def set_prices (self , ticker : str , data : list [dict [str , Any ]]):
61+ """Append new price data to cache with TTL."""
62+ existing_data = self .get_prices (ticker )
63+ merged = self ._merge_data (existing_data , data , key_field = "time" )
64+ self ._prices_cache [ticker ] = CacheEntry (merged , ttl_seconds = self .DEFAULT_TTL )
65+
66+ def get_financial_metrics (self , ticker : str ) -> list [dict [str , Any ]] | None :
67+ """Get cached financial metrics if available and not expired."""
68+ self ._cleanup_expired (self ._financial_metrics_cache )
69+ entry = self ._financial_metrics_cache .get (ticker )
70+ if entry and not entry .is_expired ():
71+ return entry .data
72+ return None
73+
74+ def set_financial_metrics (self , ticker : str , data : list [dict [str , Any ]]):
75+ """Append new financial metrics to cache with TTL."""
76+ existing_data = self .get_financial_metrics (ticker )
77+ merged = self ._merge_data (existing_data , data , key_field = "report_period" )
78+ self ._financial_metrics_cache [ticker ] = CacheEntry (merged , ttl_seconds = self .METRICS_TTL )
79+
80+ def get_line_items (self , ticker : str ) -> list [dict [str , Any ]] | None :
81+ """Get cached line items if available and not expired."""
82+ self ._cleanup_expired (self ._line_items_cache )
83+ entry = self ._line_items_cache .get (ticker )
84+ if entry and not entry .is_expired ():
85+ return entry .data
86+ return None
87+
88+ def set_line_items (self , ticker : str , data : list [dict [str , Any ]]):
89+ """Append new line items to cache with TTL."""
90+ existing_data = self .get_line_items (ticker )
91+ merged = self ._merge_data (existing_data , data , key_field = "report_period" )
92+ self ._line_items_cache [ticker ] = CacheEntry (merged , ttl_seconds = self .METRICS_TTL )
93+
94+ def get_insider_trades (self , ticker : str ) -> list [dict [str , Any ]] | None :
95+ """Get cached insider trades if available and not expired."""
96+ self ._cleanup_expired (self ._insider_trades_cache )
97+ entry = self ._insider_trades_cache .get (ticker )
98+ if entry and not entry .is_expired ():
99+ return entry .data
100+ return None
101+
102+ def set_insider_trades (self , ticker : str , data : list [dict [str , Any ]]):
103+ """Append new insider trades to cache with TTL."""
104+ existing_data = self .get_insider_trades (ticker )
105+ merged = self ._merge_data (existing_data , data , key_field = "filing_date" )
106+ self ._insider_trades_cache [ticker ] = CacheEntry (merged , ttl_seconds = self .DEFAULT_TTL )
107+
108+ def get_company_news (self , ticker : str ) -> list [dict [str , Any ]] | None :
109+ """Get cached company news if available and not expired."""
110+ self ._cleanup_expired (self ._company_news_cache )
111+ entry = self ._company_news_cache .get (ticker )
112+ if entry and not entry .is_expired ():
113+ return entry .data
114+ return None
115+
116+ def set_company_news (self , ticker : str , data : list [dict [str , Any ]]):
117+ """Append new company news to cache with TTL."""
118+ existing_data = self .get_company_news (ticker )
119+ merged = self ._merge_data (existing_data , data , key_field = "date" )
120+ self ._company_news_cache [ticker ] = CacheEntry (merged , ttl_seconds = self .NEWS_TTL )
121+
122+ def clear (self ) -> None :
123+ """Clear all caches."""
124+ self ._prices_cache .clear ()
125+ self ._financial_metrics_cache .clear ()
126+ self ._line_items_cache .clear ()
127+ self ._insider_trades_cache .clear ()
128+ self ._company_news_cache .clear ()
63129
64130
65131# Global cache instance
0 commit comments