cache rework

This commit is contained in:
Chuck
2025-07-21 21:18:34 -05:00
parent 3a450b717a
commit bf44d7b55b
2 changed files with 230 additions and 190 deletions

View File

@@ -195,6 +195,7 @@
"live_update_interval": 30, "live_update_interval": 30,
"live_odds_update_interval": 3600, "live_odds_update_interval": 3600,
"odds_update_interval": 3600, "odds_update_interval": 3600,
"season_cache_duration_seconds": 86400,
"fetch_past_games": 1, "fetch_past_games": 1,
"fetch_future_games": 2, "fetch_future_games": 2,
"favorite_teams": ["UGA", "AUB"], "favorite_teams": ["UGA", "AUB"],

View File

@@ -12,6 +12,8 @@ from src.cache_manager import CacheManager # Keep CacheManager import
from src.config_manager import ConfigManager from src.config_manager import ConfigManager
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
import pytz import pytz
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Constants # Constants
ESPN_NCAAFB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" # Changed URL for NCAA FB ESPN_NCAAFB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" # Changed URL for NCAA FB
@@ -23,56 +25,7 @@ logging.basicConfig(
datefmt='%Y-%m-%d %H:%M:%S' datefmt='%Y-%m-%d %H:%M:%S'
) )
# Re-add CacheManager definition temporarily until it's confirmed where it lives
class CacheManager:
"""Manages caching of ESPN API responses."""
_instance = None
_cache = {}
_cache_timestamps = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(CacheManager, cls).__new__(cls)
return cls._instance
@classmethod
def get(cls, key: str, max_age: int = 60) -> Optional[Dict]:
"""
Get data from cache if it exists and is not stale.
Args:
key: Cache key (usually the date string)
max_age: Maximum age of cached data in seconds
Returns:
Cached data if valid, None if missing or stale
"""
if key not in cls._cache:
return None
timestamp = cls._cache_timestamps.get(key, 0)
if time.time() - timestamp > max_age:
# Data is stale, remove it
del cls._cache[key]
del cls._cache_timestamps[key]
return None
return cls._cache[key]
@classmethod
def set(cls, key: str, data: Dict) -> None:
"""
Store data in cache with current timestamp.
Args:
key: Cache key (usually the date string)
data: Data to cache
"""
cls._cache[key] = data
cls._cache_timestamps[key] = time.time()
@classmethod
def clear(cls) -> None:
"""Clear all cached data."""
cls._cache.clear()
cls._cache_timestamps.clear()
class BaseNCAAFBManager: # Renamed class class BaseNCAAFBManager: # Renamed class
@@ -83,6 +36,8 @@ class BaseNCAAFBManager: # Renamed class
_warning_cooldown = 60 # Only log warnings once per minute _warning_cooldown = 60 # Only log warnings once per minute
_shared_data = None _shared_data = None
_last_shared_update = 0 _last_shared_update = 0
_processed_games_cache = {} # Cache for processed game data
_processed_games_timestamp = 0
cache_manager = CacheManager() cache_manager = CacheManager()
odds_manager = OddsManager(cache_manager, ConfigManager()) odds_manager = OddsManager(cache_manager, ConfigManager())
logger = logging.getLogger('NCAAFB') # Changed logger name logger = logging.getLogger('NCAAFB') # Changed logger name
@@ -98,6 +53,28 @@ class BaseNCAAFBManager: # Renamed class
self.logo_dir = self.ncaa_fb_config.get("logo_dir", "assets/sports/ncaa_fbs_logos") # Changed logo dir self.logo_dir = self.ncaa_fb_config.get("logo_dir", "assets/sports/ncaa_fbs_logos") # Changed logo dir
self.update_interval = self.ncaa_fb_config.get("update_interval_seconds", 60) self.update_interval = self.ncaa_fb_config.get("update_interval_seconds", 60)
self.show_records = self.ncaa_fb_config.get('show_records', False) self.show_records = self.ncaa_fb_config.get('show_records', False)
self.season_cache_duration = self.ncaa_fb_config.get("season_cache_duration_seconds", 86400) # 24 hours default
# Set up session with retry logic
self.session = requests.Session()
retry_strategy = Retry(
total=5, # increased number of retries
backoff_factor=1, # increased backoff factor
status_forcelist=[429, 500, 502, 503, 504], # added 429 to retry list
allowed_methods=["GET", "HEAD", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("https://", adapter)
self.session.mount("http://", adapter)
# Set up headers
self.headers = {
'User-Agent': 'LEDMatrix/1.0 (https://github.com/yourusername/LEDMatrix; contact@example.com)',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive'
}
self.last_update = 0 self.last_update = 0
self.current_game = None self.current_game = None
self.fonts = self._load_fonts() self.fonts = self._load_fonts()
@@ -172,10 +149,16 @@ class BaseNCAAFBManager: # Renamed class
except Exception as e: except Exception as e:
self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}") self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}")
def _fetch_shared_data(self) -> None: def _fetch_shared_data(self) -> Optional[Dict]:
""" """
Fetches the full season schedule for NCAAFB, caches it, and then filters Fetches the full season schedule for NCAAFB, caches it, and then filters
for relevant games based on the current configuration. for relevant games based on the current configuration.
Caching Strategy:
- Season schedules: Cached for 24 hours (configurable) - schedules rarely change
- Live games: Cached for 60 seconds - scores update frequently
- Processed data: Cached for 5 minutes - avoids re-processing
- Recent/Upcoming: Use shared season data + local processing cache
""" """
now = datetime.now(pytz.utc) now = datetime.now(pytz.utc)
current_year = now.year current_year = now.year
@@ -187,7 +170,9 @@ class BaseNCAAFBManager: # Renamed class
all_events = [] all_events = []
for year in years_to_check: for year in years_to_check:
cache_key = f"ncaafb_schedule_{year}" cache_key = f"ncaafb_schedule_{year}"
cached_data = BaseNCAAFBManager.cache_manager.get(cache_key) # Use much longer cache duration for season schedules (configurable, default 24 hours)
# Season schedules rarely change and can be cached for days
cached_data = BaseNCAAFBManager.cache_manager.get(cache_key, max_age=self.season_cache_duration)
if cached_data: if cached_data:
self.logger.info(f"[NCAAFB] Using cached schedule for {year}") self.logger.info(f"[NCAAFB] Using cached schedule for {year}")
@@ -201,7 +186,7 @@ class BaseNCAAFBManager: # Renamed class
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
events = data.get('events', []) events = data.get('events', [])
BaseNCAAFBManager.cache_manager.set(cache_key, events, expiration_seconds=86400) # Cache for 24 hours BaseNCAAFBManager.cache_manager.update_cache(cache_key, events)
self.logger.info(f"[NCAAFB] Successfully fetched and cached {len(events)} events for the {year} season.") self.logger.info(f"[NCAAFB] Successfully fetched and cached {len(events)} events for the {year} season.")
all_events.extend(events) all_events.extend(events)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
@@ -210,7 +195,7 @@ class BaseNCAAFBManager: # Renamed class
if not all_events: if not all_events:
self.logger.warning("[NCAAFB] No events found in the schedule data for checked years.") self.logger.warning("[NCAAFB] No events found in the schedule data for checked years.")
return return None
# Filter the events for live, upcoming, and recent games # Filter the events for live, upcoming, and recent games
live_events = [] live_events = []
@@ -279,10 +264,40 @@ class BaseNCAAFBManager: # Renamed class
BaseNCAAFBManager.all_events = live_events + selected_upcoming + selected_past BaseNCAAFBManager.all_events = live_events + selected_upcoming + selected_past
self.logger.info(f"[NCAAFB] Processed schedule: {len(live_events)} live, {len(selected_upcoming)} upcoming, {len(selected_past)} recent games.") self.logger.info(f"[NCAAFB] Processed schedule: {len(live_events)} live, {len(selected_upcoming)} upcoming, {len(selected_past)} recent games.")
# Return the data in the expected format
return {'events': BaseNCAAFBManager.all_events}
def _get_cached_processed_games(self, manager_type: str) -> Optional[List[Dict]]:
"""Get cached processed games for a specific manager type."""
current_time = time.time()
cache_key = f"processed_games_{manager_type}"
# Cache processed games for 5 minutes
if (current_time - BaseNCAAFBManager._processed_games_timestamp < 300 and
cache_key in BaseNCAAFBManager._processed_games_cache):
return BaseNCAAFBManager._processed_games_cache[cache_key]
return None
def _cache_processed_games(self, manager_type: str, games: List[Dict]) -> None:
"""Cache processed games for a specific manager type."""
cache_key = f"processed_games_{manager_type}"
BaseNCAAFBManager._processed_games_cache[cache_key] = games
BaseNCAAFBManager._processed_games_timestamp = time.time()
def _fetch_data(self, date_str: str = None) -> Optional[Dict]: def _fetch_data(self, date_str: str = None) -> Optional[Dict]:
"""Fetch data using shared data mechanism or direct fetch for live.""" """Fetch data using shared data mechanism or direct fetch for live."""
# Check if the instance is NCAAFBLiveManager # Check if the instance is NCAAFBLiveManager
if isinstance(self, NCAAFBLiveManager): # Changed class name if isinstance(self, NCAAFBLiveManager): # Changed class name
# For live games, use shorter cache duration (60 seconds)
# Live scores can be fetched more frequently if needed
cache_key = f"ncaafb_live_{date_str or 'current'}"
cached_data = self.cache_manager.get(cache_key, max_age=60)
if cached_data:
self.logger.debug(f"[NCAAFB] Using cached live data")
return cached_data
try: try:
url = ESPN_NCAAFB_SCOREBOARD_URL # Use NCAA FB URL url = ESPN_NCAAFB_SCOREBOARD_URL # Use NCAA FB URL
params = {} params = {}
@@ -292,6 +307,8 @@ class BaseNCAAFBManager: # Renamed class
response = requests.get(url, params=params) response = requests.get(url, params=params)
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
# Cache live data for 60 seconds
self.cache_manager.update_cache(cache_key, data)
self.logger.info(f"[NCAAFB] Successfully fetched live game data from ESPN API") self.logger.info(f"[NCAAFB] Successfully fetched live game data from ESPN API")
return data return data
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
@@ -299,7 +316,11 @@ class BaseNCAAFBManager: # Renamed class
return None return None
else: else:
# For non-live games, use the shared cache # For non-live games, use the shared cache
return self._fetch_shared_data() shared_data = self._fetch_shared_data()
if shared_data is None:
self.logger.warning("[NCAAFB] No shared data available")
return None
return shared_data
def _load_fonts(self): def _load_fonts(self):
"""Load fonts used by the scoreboard.""" """Load fonts used by the scoreboard."""
@@ -961,6 +982,13 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
return return
self.last_update = current_time # Update time even if fetch fails self.last_update = current_time # Update time even if fetch fails
# Check for cached processed games first
cached_games = self._get_cached_processed_games('recent')
if cached_games:
self.logger.debug("[NCAAFB Recent] Using cached processed games")
team_games = cached_games
else:
try: try:
data = self._fetch_data() # Uses shared cache data = self._fetch_data() # Uses shared cache
if not data or 'events' not in data: if not data or 'events' not in data:
@@ -990,6 +1018,9 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
# Sort by game time, most recent first # Sort by game time, most recent first
team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.min.replace(tzinfo=timezone.utc), reverse=True) team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.min.replace(tzinfo=timezone.utc), reverse=True)
# Cache the processed games
self._cache_processed_games('recent', team_games)
# Check if the list of games to display has changed # Check if the list of games to display has changed
new_game_ids = {g['id'] for g in team_games} new_game_ids = {g['id'] for g in team_games}
current_game_ids = {g['id'] for g in self.games_list} current_game_ids = {g['id'] for g in self.games_list}
@@ -1016,7 +1047,6 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
# List content is same, just update data for current game # List content is same, just update data for current game
self.current_game = self.games_list[self.current_game_index] self.current_game = self.games_list[self.current_game_index]
if not self.games_list: if not self.games_list:
self.logger.info("[NCAAFB Recent] No relevant recent games found to display.") # Changed log prefix self.logger.info("[NCAAFB Recent] No relevant recent games found to display.") # Changed log prefix
self.current_game = None # Ensure display clears if no games self.current_game = None # Ensure display clears if no games
@@ -1162,6 +1192,13 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
return return
self.last_update = current_time self.last_update = current_time
# Check for cached processed games first
cached_games = self._get_cached_processed_games('upcoming')
if cached_games:
self.logger.debug("[NCAAFB Upcoming] Using cached processed games")
team_games = cached_games
else:
try: try:
data = self._fetch_data() # Uses shared cache data = self._fetch_data() # Uses shared cache
if not data or 'events' not in data: if not data or 'events' not in data:
@@ -1210,6 +1247,9 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
# Sort by game time, earliest first # Sort by game time, earliest first
team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.max.replace(tzinfo=timezone.utc)) team_games.sort(key=lambda g: g.get('start_time_utc') or datetime.max.replace(tzinfo=timezone.utc))
# Cache the processed games
self._cache_processed_games('upcoming', team_games)
# Log changes or periodically # Log changes or periodically
should_log = ( should_log = (
current_time - self.last_log_time >= self.log_interval or current_time - self.last_log_time >= self.log_interval or
@@ -1253,7 +1293,6 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class
elif should_log: elif should_log:
self.last_log_time = current_time self.last_log_time = current_time
except Exception as e: except Exception as e:
self.logger.error(f"[NCAAFB Upcoming] Error updating upcoming games: {e}", exc_info=True) # Changed log prefix self.logger.error(f"[NCAAFB Upcoming] Error updating upcoming games: {e}", exc_info=True) # Changed log prefix
# self.current_game = None # Decide if clear on error # self.current_game = None # Decide if clear on error