mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 05:13:01 +00:00
* fix(cache): move odds key check before live/scoreboard check in get_data_type_from_key Cache keys like odds_espn_nba_game_123_live contain 'live', so they were matched by the generic ['live', 'current', 'scoreboard'] branch (sports_live, 30s TTL) before the 'odds' branch was ever reached. This caused live odds to expire every 30 seconds instead of every 120 seconds, hitting the ESPN odds API 4x more often than intended and risking rate-limiting. Fix: move the 'odds' check above the 'live'/'current'/'scoreboard' check so the more-specific prefix wins. No regressions: pure live_*/scoreboard_* keys (without 'odds') still route to sports_live. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cache): remove dead soccer branch in get_data_type_from_key The inner `if 'soccer' in key_lower: return 'sports_live'` branch was dead code — both the soccer and non-soccer paths returned the same 'sports_live' value. Collapse to a single return statement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
296 lines
11 KiB
Python
296 lines
11 KiB
Python
"""
|
|
Cache Strategy
|
|
|
|
Manages cache strategies for different data types with sport-specific configurations.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, Optional
|
|
from datetime import datetime
|
|
import pytz
|
|
|
|
|
|
class CacheStrategy:
|
|
"""Manages cache strategies for different data types."""
|
|
|
|
def __init__(self, config_manager: Optional[Any] = None, logger: Optional[logging.Logger] = None) -> None:
|
|
"""
|
|
Initialize cache strategy manager.
|
|
|
|
Args:
|
|
config_manager: Optional ConfigManager instance for sport-specific configs
|
|
logger: Optional logger instance
|
|
"""
|
|
self.config_manager = config_manager
|
|
self.logger = logger or logging.getLogger(__name__)
|
|
|
|
def get_sport_live_interval(self, sport_key: str) -> int:
|
|
"""
|
|
Get the live_update_interval for a specific sport from config.
|
|
Falls back to default values if config is not available.
|
|
|
|
Args:
|
|
sport_key: Sport identifier (e.g., 'nba', 'nfl')
|
|
|
|
Returns:
|
|
Live update interval in seconds
|
|
"""
|
|
if not self.config_manager:
|
|
# Default intervals - all sports use 60 seconds as default
|
|
default_intervals = {
|
|
'soccer': 60,
|
|
'nfl': 60,
|
|
'nhl': 60,
|
|
'nba': 60,
|
|
'mlb': 60,
|
|
'milb': 60,
|
|
'ncaa_fb': 60,
|
|
'ncaa_baseball': 60,
|
|
'ncaam_basketball': 60,
|
|
}
|
|
return default_intervals.get(sport_key, 60)
|
|
|
|
try:
|
|
config = self.config_manager.config
|
|
# All sports now use _scoreboard suffix
|
|
sport_config = config.get(f"{sport_key}_scoreboard", {})
|
|
return sport_config.get("live_update_interval", 60) # Default to 60 seconds
|
|
except (KeyError, AttributeError, TypeError) as e:
|
|
self.logger.warning("Could not get live_update_interval for %s: %s", sport_key, e, exc_info=True)
|
|
return 60 # Default to 60 seconds
|
|
|
|
def get_cache_strategy(self, data_type: str, sport_key: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Get cache strategy for different data types.
|
|
Now respects sport-specific live_update_interval configurations.
|
|
|
|
Args:
|
|
data_type: Type of data (e.g., 'live_scores', 'stocks', 'weather_current')
|
|
sport_key: Optional sport key for sport-specific intervals
|
|
|
|
Returns:
|
|
Dictionary with cache strategy (max_age, memory_ttl, etc.)
|
|
"""
|
|
# Get sport-specific live interval if provided
|
|
live_interval = None
|
|
if sport_key and data_type in ['sports_live', 'live_scores']:
|
|
live_interval = self.get_sport_live_interval(sport_key)
|
|
|
|
# Try to read sport-specific config for recent/upcoming
|
|
recent_interval = None
|
|
upcoming_interval = None
|
|
if self.config_manager and sport_key:
|
|
try:
|
|
# All sports now use _scoreboard suffix
|
|
sport_cfg = self.config_manager.config.get(f"{sport_key}_scoreboard", {})
|
|
recent_interval = sport_cfg.get('recent_update_interval')
|
|
upcoming_interval = sport_cfg.get('upcoming_update_interval')
|
|
except (KeyError, AttributeError, TypeError) as e:
|
|
self.logger.debug("Could not read sport-specific recent/upcoming intervals for %s: %s",
|
|
sport_key, e, exc_info=True)
|
|
|
|
strategies = {
|
|
# Ultra time-sensitive data (live scores, current weather)
|
|
'live_scores': {
|
|
'max_age': live_interval or 15, # Use sport-specific interval
|
|
'memory_ttl': (live_interval or 15) * 2, # 2x for memory cache
|
|
'force_refresh': True
|
|
},
|
|
'sports_live': {
|
|
'max_age': live_interval or 30, # Use sport-specific interval
|
|
'memory_ttl': (live_interval or 30) * 2,
|
|
'force_refresh': True
|
|
},
|
|
'weather_current': {
|
|
'max_age': 300, # 5 minutes
|
|
'memory_ttl': 600,
|
|
'force_refresh': False
|
|
},
|
|
|
|
# Market data (stocks, crypto)
|
|
'stocks': {
|
|
'max_age': 600, # 10 minutes
|
|
'memory_ttl': 1200,
|
|
'market_hours_only': True,
|
|
'force_refresh': False
|
|
},
|
|
'crypto': {
|
|
'max_age': 300, # 5 minutes (crypto trades 24/7)
|
|
'memory_ttl': 600,
|
|
'force_refresh': False
|
|
},
|
|
|
|
# Sports data
|
|
'sports_recent': {
|
|
'max_age': recent_interval or 1800, # 30 minutes default; override by config
|
|
'memory_ttl': (recent_interval or 1800) * 2,
|
|
'force_refresh': False
|
|
},
|
|
'sports_upcoming': {
|
|
'max_age': upcoming_interval or 10800, # 3 hours default; override by config
|
|
'memory_ttl': (upcoming_interval or 10800) * 2,
|
|
'force_refresh': False
|
|
},
|
|
'sports_schedules': {
|
|
'max_age': 86400, # 24 hours
|
|
'memory_ttl': 172800,
|
|
'force_refresh': False
|
|
},
|
|
'leaderboard': {
|
|
'max_age': 604800, # 7 days (1 week) - football rankings updated weekly
|
|
'memory_ttl': 1209600, # 14 days in memory
|
|
'force_refresh': False
|
|
},
|
|
|
|
# News and odds
|
|
'news': {
|
|
'max_age': 3600, # 1 hour
|
|
'memory_ttl': 7200,
|
|
'force_refresh': False
|
|
},
|
|
'odds': {
|
|
'max_age': 1800, # 30 minutes for upcoming games
|
|
'memory_ttl': 3600,
|
|
'force_refresh': False
|
|
},
|
|
'odds_live': {
|
|
'max_age': 120, # 2 minutes for live games (odds change rapidly)
|
|
'memory_ttl': 240,
|
|
'force_refresh': False
|
|
},
|
|
|
|
# Static/stable data
|
|
'team_info': {
|
|
'max_age': 604800, # 1 week
|
|
'memory_ttl': 1209600,
|
|
'force_refresh': False
|
|
},
|
|
'logos': {
|
|
'max_age': 2592000, # 30 days
|
|
'memory_ttl': 5184000,
|
|
'force_refresh': False
|
|
},
|
|
|
|
# Default fallback
|
|
'default': {
|
|
'max_age': 300, # 5 minutes
|
|
'memory_ttl': 600,
|
|
'force_refresh': False
|
|
}
|
|
}
|
|
|
|
return strategies.get(data_type, strategies['default'])
|
|
|
|
def get_data_type_from_key(self, key: str) -> str:
|
|
"""
|
|
Determine the appropriate cache strategy based on the cache key.
|
|
This helps automatically select the right cache duration.
|
|
|
|
Args:
|
|
key: Cache key
|
|
|
|
Returns:
|
|
Data type string for strategy lookup
|
|
"""
|
|
key_lower = key.lower()
|
|
|
|
# Odds data — checked FIRST because odds keys may also contain 'live'/'current'
|
|
# (e.g. odds_espn_nba_game_123_live). The odds TTL (120s for live, 1800s for
|
|
# upcoming) must win over the generic sports_live TTL (30s) to avoid hitting
|
|
# the ESPN odds API every 30 seconds per game.
|
|
if 'odds' in key_lower:
|
|
# For live games, use shorter cache; for upcoming games, use longer cache
|
|
if any(x in key_lower for x in ['live', 'current']):
|
|
return 'odds_live' # Live odds change more frequently (120s TTL)
|
|
return 'odds' # Regular odds for upcoming games (1800s TTL)
|
|
|
|
# Live sports data (only reached if key does NOT contain 'odds')
|
|
if any(x in key_lower for x in ['live', 'current', 'scoreboard']):
|
|
return 'sports_live'
|
|
|
|
# Weather data
|
|
if 'weather' in key_lower:
|
|
return 'weather_current'
|
|
|
|
# Market data
|
|
if 'stock' in key_lower or 'crypto' in key_lower:
|
|
if 'crypto' in key_lower:
|
|
return 'crypto'
|
|
return 'stocks'
|
|
|
|
# News data
|
|
if 'news' in key_lower:
|
|
return 'news'
|
|
|
|
# Sports schedules and team info
|
|
if any(x in key_lower for x in ['schedule', 'team_map', 'league']):
|
|
return 'sports_schedules'
|
|
|
|
# Recent games (last few hours)
|
|
if 'recent' in key_lower:
|
|
return 'sports_recent'
|
|
|
|
# Upcoming games
|
|
if 'upcoming' in key_lower:
|
|
return 'sports_upcoming'
|
|
|
|
# Static data like logos, team info
|
|
if any(x in key_lower for x in ['logo', 'team_info', 'config']):
|
|
return 'team_info'
|
|
|
|
# Default fallback
|
|
return 'default'
|
|
|
|
def get_sport_key_from_cache_key(self, key: str) -> Optional[str]:
|
|
"""
|
|
Extract sport key from cache key to determine appropriate live_update_interval.
|
|
|
|
Args:
|
|
key: Cache key
|
|
|
|
Returns:
|
|
Sport key or None if not found
|
|
"""
|
|
key_lower = key.lower()
|
|
|
|
# Map cache key patterns to sport keys
|
|
sport_patterns = {
|
|
'nfl': ['nfl'],
|
|
'nba': ['nba', 'basketball'],
|
|
'mlb': ['mlb', 'baseball'],
|
|
'nhl': ['nhl', 'hockey'],
|
|
'soccer': ['soccer'],
|
|
'ncaa_fb': ['ncaa_fb', 'ncaafb', 'college_football'],
|
|
'ncaa_baseball': ['ncaa_baseball', 'college_baseball'],
|
|
'ncaam_basketball': ['ncaam_basketball', 'college_basketball'],
|
|
'milb': ['milb', 'minor_league'],
|
|
}
|
|
|
|
for sport_key, patterns in sport_patterns.items():
|
|
if any(pattern in key_lower for pattern in patterns):
|
|
return sport_key
|
|
|
|
return None
|
|
|
|
def is_market_open(self) -> bool:
|
|
"""
|
|
Check if the US stock market is currently open.
|
|
|
|
Returns:
|
|
True if market is open, False otherwise
|
|
"""
|
|
et_tz = pytz.timezone('America/New_York')
|
|
now = datetime.now(et_tz)
|
|
|
|
# Check if it's a weekday
|
|
if now.weekday() >= 5: # 5 = Saturday, 6 = Sunday
|
|
return False
|
|
|
|
# Convert current time to ET
|
|
current_time = now.time()
|
|
market_open = datetime.strptime('09:30', '%H:%M').time()
|
|
market_close = datetime.strptime('16:00', '%H:%M').time()
|
|
|
|
return market_open <= current_time <= market_close
|
|
|