From 72e10e06d462915c45d7396244007ef1885e67ee Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:07:46 -0500 Subject: [PATCH] attempting cache improvements to see if that is affecting ability to see recent odds updates --- src/cache_manager.py | 119 ++++++++++++++++---------------- src/mlb_manager.py | 160 ++++++++++++++++++++++--------------------- 2 files changed, 142 insertions(+), 137 deletions(-) diff --git a/src/cache_manager.py b/src/cache_manager.py index 5aa0d1bf..d7390a61 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -23,60 +23,61 @@ class CacheManager: # Initialize logger first self.logger = logging.getLogger(__name__) - # Get the actual user's home directory, even when running with sudo - try: - # Try to get the real user's home directory - real_user = os.environ.get('SUDO_USER') or os.environ.get('USER') - if real_user: - home_dir = f"/home/{real_user}" - else: - home_dir = os.path.expanduser('~') - except Exception: - home_dir = os.path.expanduser('~') - - # Determine the appropriate cache directory - if os.geteuid() == 0: # Running as root/sudo - self.cache_dir = "/var/cache/ledmatrix" + # Determine the most reliable writable directory + self.cache_dir = self._get_writable_cache_dir() + if self.cache_dir: + self.logger.info(f"Using cache directory: {self.cache_dir}") else: - self.cache_dir = os.path.join(home_dir, '.ledmatrix_cache') - + # This is a critical failure, as caching is essential. + self.logger.error("Could not find or create a writable cache directory. Caching will be disabled.") + self.cache_dir = None + self._memory_cache = {} # In-memory cache for faster access self._memory_cache_timestamps = {} self._cache_lock = threading.Lock() - # Ensure cache directory exists after logger is initialized - self._ensure_cache_dir() - - def _ensure_cache_dir(self): - """Ensure the cache directory exists with proper permissions.""" + def _get_writable_cache_dir(self) -> Optional[str]: + """Tries to find or create a writable cache directory in a few common locations.""" + # Attempt 1: User's home directory (handling sudo) try: - os.makedirs(self.cache_dir, exist_ok=True) - # Set permissions to allow both root and the user to access - if os.geteuid() == 0: # Running as root/sudo - os.chmod(self.cache_dir, 0o777) # Full permissions for all users - # Also set ownership to the real user if we're running as root - real_user = os.environ.get('SUDO_USER') - if real_user: - try: - import pwd - uid = pwd.getpwnam(real_user).pw_uid - gid = pwd.getpwnam(real_user).pw_gid - os.chown(self.cache_dir, uid, gid) - except Exception as e: - self.logger.warning(f"Could not set cache directory ownership: {e}") - except Exception as e: - self.logger.error(f"Failed to create cache directory: {e}") - # Fallback to temp directory if we can't create the cache directory - self.cache_dir = os.path.join(tempfile.gettempdir(), 'ledmatrix_cache') - try: - os.makedirs(self.cache_dir, exist_ok=True) - self.logger.info(f"Using fallback cache directory: {self.cache_dir}") - except Exception as e: - self.logger.error(f"Failed to create fallback cache directory: {e}") - raise # Re-raise if we can't create any cache directory + real_user = os.environ.get('SUDO_USER') or os.environ.get('USER', 'default') + if real_user and real_user != 'root': + home_dir = os.path.expanduser(f"~{real_user}") + else: + home_dir = os.path.expanduser('~') - def _get_cache_path(self, key: str) -> str: + user_cache_dir = os.path.join(home_dir, '.ledmatrix_cache') + os.makedirs(user_cache_dir, exist_ok=True) + + # Test writability + test_file = os.path.join(user_cache_dir, '.writetest') + with open(test_file, 'w') as f: + f.write('test') + os.remove(test_file) + return user_cache_dir + except Exception as e: + self.logger.warning(f"Could not use user-specific cache directory: {e}") + + # Attempt 2: System-wide temporary directory + try: + system_cache_dir = os.path.join(tempfile.gettempdir(), 'ledmatrix_cache') + os.makedirs(system_cache_dir, exist_ok=True) + if os.access(system_cache_dir, os.W_OK): + return system_cache_dir + except Exception as e: + self.logger.warning(f"Could not use system-wide temporary cache directory: {e}") + + # Return None if no directory is writable + return None + + def _ensure_cache_dir(self): + """This method is deprecated and no longer needed.""" + pass + + def _get_cache_path(self, key: str) -> Optional[str]: """Get the path for a cache file.""" + if not self.cache_dir: + return None return os.path.join(self.cache_dir, f"{key}.json") def get_cached_data(self, key: str, max_age: int = 300) -> Optional[Dict]: @@ -112,15 +113,16 @@ class CacheManager: data: Data to cache """ try: - # Save to file - cache_path = self._get_cache_path(key) - with self._cache_lock: - with open(cache_path, 'w') as f: - json.dump(data, f, indent=4, cls=DateTimeEncoder) - - # Update memory cache + # Update memory cache first self._memory_cache[key] = data self._memory_cache_timestamps[key] = time.time() + + # Save to file if a cache directory is available + cache_path = self._get_cache_path(key) + if cache_path: + with self._cache_lock: + with open(cache_path, 'w') as f: + json.dump(data, f, indent=4, cls=DateTimeEncoder) except (IOError, OSError) as e: self.logger.error(f"Failed to save cache for key '{key}': {e}") @@ -143,7 +145,7 @@ class CacheManager: del self._memory_cache_timestamps[key] cache_path = self._get_cache_path(key) - if not os.path.exists(cache_path): + if not cache_path or not os.path.exists(cache_path): return None try: @@ -173,15 +175,16 @@ class CacheManager: del self._memory_cache[key] del self._memory_cache_timestamps[key] cache_path = self._get_cache_path(key) - if os.path.exists(cache_path): + if cache_path and os.path.exists(cache_path): os.remove(cache_path) else: # Clear all keys self._memory_cache.clear() self._memory_cache_timestamps.clear() - for file in os.listdir(self.cache_dir): - if file.endswith('.json'): - os.remove(os.path.join(self.cache_dir, file)) + if self.cache_dir: + for file in os.listdir(self.cache_dir): + if file.endswith('.json'): + os.remove(os.path.join(self.cache_dir, file)) def has_data_changed(self, data_type: str, new_data: Dict[str, Any]) -> bool: """Check if data has changed from cached version.""" diff --git a/src/mlb_manager.py b/src/mlb_manager.py index c9cdb825..d638c4e6 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -988,20 +988,16 @@ class MLBRecentManager(BaseMLBManager): def update(self): """Update recent games data.""" current_time = time.time() - if current_time - self.last_log_time > self.log_interval: - self.logger.info(f"Checking for recent MLB games. Last update was at {self.last_update}") - self.last_log_time = current_time - + self.logger.info(f"Checking for recent MLB games. Last update was at {self.last_update}") if self.last_update != 0 and (current_time - self.last_update < self.update_interval): - if current_time - self.last_log_time > self.log_interval: - self.logger.info(f"Skipping recent games update, interval not reached. Next update in {self.update_interval - (current_time - self.last_update):.0f} seconds.") + self.logger.info(f"Skipping recent games update, interval not reached. Next update in {self.update_interval - (current_time - self.last_update):.0f} seconds.") return try: # Fetch data from MLB API games = self._fetch_mlb_api_data() if not games: - logger.warning("[MLB] No games returned from API") + logger.warning("[MLB] No games returned from API for recent games update.") return # Process games @@ -1009,9 +1005,10 @@ class MLBRecentManager(BaseMLBManager): now = datetime.now(timezone.utc) # Make timezone-aware recent_cutoff = now - timedelta(hours=self.recent_hours) - logger.info(f"[MLB] Time window: {recent_cutoff} to {now}") + self.logger.info(f"[MLB] Recent games time window: {recent_cutoff} to {now}") for game_id, game in games.items(): + self.logger.info(f"[MLB] Processing game {game_id} for recent games...") # Convert game time to UTC datetime game_time_str = game['start_time'].replace('Z', '+00:00') game_time = datetime.fromisoformat(game_time_str) @@ -1022,25 +1019,28 @@ class MLBRecentManager(BaseMLBManager): is_favorite_game = (game['home_team'] in self.favorite_teams or game['away_team'] in self.favorite_teams) - if is_favorite_game: - logger.info(f"[MLB] Checking favorite team game: {game['away_team']} @ {game['home_team']}") - logger.info(f"[MLB] Game time (UTC): {game_time}") - logger.info(f"[MLB] Game status: {game['status']}, State: {game['status_state']}") + if not is_favorite_game: + self.logger.info(f"[MLB] Skipping game {game_id} - not a favorite team.") + continue + + self.logger.info(f"[MLB] Favorite team game found: {game['away_team']} @ {game['home_team']}") + self.logger.info(f"[MLB] Game time (UTC): {game_time}") + self.logger.info(f"[MLB] Game status: {game['status']}, State: {game['status_state']}") # Use status_state to determine if game is final is_final = game['status_state'] in ['post', 'final', 'completed'] is_within_time = recent_cutoff <= game_time <= now - if is_favorite_game: - logger.info(f"[MLB] Is final: {is_final}") - logger.info(f"[MLB] Is within time window: {is_within_time}") - logger.info(f"[MLB] Time comparison: {recent_cutoff} <= {game_time} <= {now}") + self.logger.info(f"[MLB] Is final: {is_final}") + self.logger.info(f"[MLB] Is within time window: {is_within_time}") # Only add favorite team games that are final and within time window - if is_favorite_game and is_final and is_within_time: + if is_final and is_within_time: + self.logger.info(f"[MLB] Adding game {game_id} to recent games list.") self._fetch_odds(game) new_recent_games.append(game) - logger.info(f"[MLB] Added favorite team game to recent list: {game['away_team']} @ {game['home_team']}") + else: + self.logger.info(f"[MLB] Skipping game {game_id} - does not meet criteria for recent games.") if new_recent_games: logger.info(f"[MLB] Found {len(new_recent_games)} recent games for favorite teams: {self.favorite_teams}") @@ -1107,80 +1107,82 @@ class MLBUpcomingManager(BaseMLBManager): def update(self): """Update upcoming games data.""" current_time = time.time() - if current_time - self.last_log_time > self.log_interval: - self.logger.info(f"Checking for upcoming MLB games. Last update was at {self.last_update}") - self.last_log_time = current_time - + self.logger.info(f"Checking for upcoming MLB games. Last update was at {self.last_update}") if self.last_update != 0 and (current_time - self.last_update < self.update_interval): - if current_time - self.last_log_time > self.log_interval: - self.logger.info(f"Skipping upcoming games update, interval not reached. Next update in {self.update_interval - (current_time - self.last_update):.0f} seconds.") + self.logger.info(f"Skipping upcoming games update, interval not reached. Next update in {self.update_interval - (current_time - self.last_update):.0f} seconds.") return try: # Fetch data from MLB API games = self._fetch_mlb_api_data() - if games: - # Process games - new_upcoming_games = [] - now = datetime.now(timezone.utc) # Make timezone-aware - upcoming_cutoff = now + timedelta(hours=24) + if not games: + self.logger.warning("[MLB] No games returned from API for upcoming games update.") + return + + # Process games + new_upcoming_games = [] + now = datetime.now(timezone.utc) # Make timezone-aware + upcoming_cutoff = now + timedelta(hours=24) + + self.logger.info(f"[MLB] Upcoming games time window: {now} to {upcoming_cutoff}") + + for game_id, game in games.items(): + self.logger.info(f"[MLB] Processing game {game_id} for upcoming games...") + # Check if this is a favorite team game first + is_favorite_game = (game['home_team'] in self.favorite_teams or + game['away_team'] in self.favorite_teams) - logger.info(f"Looking for games between {now} and {upcoming_cutoff}") - - for game in games.values(): - # Check if this is a favorite team game first - is_favorite_game = (game['home_team'] in self.favorite_teams or - game['away_team'] in self.favorite_teams) - - if not is_favorite_game: - continue # Skip non-favorite team games + if not is_favorite_game: + self.logger.info(f"[MLB] Skipping game {game_id} - not a favorite team.") + continue - game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00')) - # Ensure game_time is timezone-aware (UTC) - if game_time.tzinfo is None: - game_time = game_time.replace(tzinfo=timezone.utc) - logger.info(f"Checking favorite team game: {game['away_team']} @ {game['home_team']} at {game_time}") - logger.info(f"Game status: {game['status']}, State: {game['status_state']}") - - # Check if game is within our time window - is_within_time = now <= game_time <= upcoming_cutoff - - # For upcoming games, we'll consider any game that: - # 1. Is within our time window - # 2. Is not final (not 'post' or 'final' state) - # 3. Has a future start time - is_upcoming = ( - is_within_time and - game['status_state'] not in ['post', 'final', 'completed'] and - game_time > now - ) - - logger.info(f"Within time window: {is_within_time}") - logger.info(f"Is upcoming: {is_upcoming}") - logger.info(f"Game time > now: {game_time > now}") - logger.info(f"Status state not final: {game['status_state'] not in ['post', 'final', 'completed']}") - - if is_upcoming: - self._fetch_odds(game) - new_upcoming_games.append(game) - logger.info(f"Added favorite team game to upcoming list: {game['away_team']} @ {game['home_team']}") + game_time = datetime.fromisoformat(game['start_time'].replace('Z', '+00:00')) + # Ensure game_time is timezone-aware (UTC) + if game_time.tzinfo is None: + game_time = game_time.replace(tzinfo=timezone.utc) + + self.logger.info(f"[MLB] Favorite team game found: {game['away_team']} @ {game['home_team']} at {game_time}") + self.logger.info(f"[MLB] Game status: {game['status']}, State: {game['status_state']}") - # Filter for favorite teams (though we already filtered above, this is a safety check) - new_team_games = [game for game in new_upcoming_games + # Check if game is within our time window + is_within_time = now <= game_time <= upcoming_cutoff + + # For upcoming games, we'll consider any game that: + # 1. Is within our time window + # 2. Is not final (not 'post' or 'final' state) + # 3. Has a future start time + is_upcoming = ( + is_within_time and + game['status_state'] not in ['post', 'final', 'completed'] and + game_time > now + ) + + self.logger.info(f"[MLB] Is within time window: {is_within_time}") + self.logger.info(f"[MLB] Is upcoming: {is_upcoming}") + + if is_upcoming: + self.logger.info(f"[MLB] Adding game {game_id} to upcoming games list.") + self._fetch_odds(game) + new_upcoming_games.append(game) + else: + self.logger.info(f"[MLB] Skipping game {game_id} - does not meet criteria for upcoming games.") + + # Filter for favorite teams (though we already filtered above, this is a safety check) + new_team_games = [game for game in new_upcoming_games if game['home_team'] in self.favorite_teams or game['away_team'] in self.favorite_teams] - if new_team_games: - logger.info(f"[MLB] Found {len(new_team_games)} upcoming games for favorite teams") - self.upcoming_games = new_team_games - if not self.current_game: - self.current_game = self.upcoming_games[0] - else: - logger.info("[MLB] No upcoming games found for favorite teams") - self.upcoming_games = [] - self.current_game = None + if new_team_games: + logger.info(f"[MLB] Found {len(new_team_games)} upcoming games for favorite teams") + self.upcoming_games = new_team_games + if not self.current_game: + self.current_game = self.upcoming_games[0] + else: + logger.info("[MLB] No upcoming games found for favorite teams") + self.upcoming_games = [] + self.current_game = None - self.last_update = current_time + self.last_update = current_time except Exception as e: logger.error(f"[MLB] Error updating upcoming games: {e}", exc_info=True)