mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-19 02:58:37 +00:00
* fix(cache): check odds keys before generic live check in get_data_type_from_key Cache keys like odds_espn_basketball_nba_<id>_live contain both 'odds' and 'live'. The previous ordering matched the generic 'live' check first, returning 'sports_live' (30 s TTL) instead of the correct 'odds_live' (120 s TTL). This caused the ESPN odds API to be hit every 30 s per live game, frequently triggering the 3-second per-request timeout and returning no odds data. Moving the 'odds' check above the generic 'live' block restores the correct 120-second cache TTL for in-progress game odds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(display): use single-quoted HTML attributes for JSON hidden inputs Placing |tojson output (which contains double quotes) inside a double-quoted HTML attribute broke the attribute — browsers closed the attribute at the first inner quote, leaving JS with an empty or truncated value. JSON.parse then failed silently, leaving excluded=[] so all Vegas scroll plugins appeared checked (included) regardless of the actual excluded_plugins config. Switch to single-quoted HTML attributes so the JSON double quotes are valid inside the attribute value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
298 lines
11 KiB
Python
298 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 before the generic 'live' block below because
|
|
# live-odds cache keys (e.g. odds_espn_basketball_nba_<id>_live) contain
|
|
# both 'odds' AND 'live'. Without this ordering the 'live' check below
|
|
# would match first and return 'sports_live' (30 s TTL) instead of the
|
|
# correct 'odds_live' (120 s TTL).
|
|
if 'odds' in key_lower:
|
|
if any(x in key_lower for x in ['live', 'current']):
|
|
return 'odds_live' # Live odds change more frequently
|
|
return 'odds' # Regular odds for upcoming games
|
|
|
|
# Live sports data
|
|
if any(x in key_lower for x in ['live', 'current', 'scoreboard']):
|
|
if 'soccer' in key_lower:
|
|
return 'sports_live' # Soccer live data is very time-sensitive
|
|
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
|
|
|