From 77ec263bc62d904bca72366498437d171097bc9a Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:22:07 -0400 Subject: [PATCH 01/11] Fix leaderboard scrolling performance after PR #39 merge - Restore leaderboard background updates that were accidentally removed - Fix duration method call from get_dynamic_duration() back to get_duration() - Restore proper fallback duration (600s instead of 60s) for leaderboard - Add back sports manager updates that feed data to leaderboard - Fix leaderboard defer_update priority to prevent scrolling lag These changes restore the leaderboard's dynamic duration calculation and ensure it gets proper background updates for smooth scrolling. --- src/display_controller.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 5244c0ece..d5148da0c 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -541,19 +541,20 @@ def get_current_duration(self) -> int: # Fall back to configured duration return self.display_durations.get(mode_key, 60) - # Handle dynamic duration for leaderboard + # Handle leaderboard duration (user choice between fixed or dynamic) elif mode_key == 'leaderboard' and self.leaderboard: try: - dynamic_duration = self.leaderboard.get_dynamic_duration() + duration = self.leaderboard.get_duration() + mode_type = "dynamic" if self.leaderboard.dynamic_duration else "fixed" # Only log if duration has changed or we haven't logged this duration yet - if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != dynamic_duration: - logger.info(f"Using dynamic duration for leaderboard: {dynamic_duration} seconds") - self._last_logged_leaderboard_duration = dynamic_duration - return dynamic_duration + if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != duration: + logger.info(f"Using leaderboard {mode_type} duration: {duration} seconds") + self._last_logged_leaderboard_duration = duration + return duration except Exception as e: - logger.error(f"Error getting dynamic duration for leaderboard: {e}") + logger.error(f"Error getting duration for leaderboard: {e}") # Fall back to configured duration - return self.display_durations.get(mode_key, 60) + return self.display_durations.get(mode_key, 600) # Simplify weather key handling elif mode_key.startswith('weather_'): @@ -575,6 +576,8 @@ def _update_modules(self): # Defer updates for modules that might cause lag during scrolling if self.odds_ticker: self.display_manager.defer_update(self.odds_ticker.update, priority=1) + if self.leaderboard: + self.display_manager.defer_update(self.leaderboard.update, priority=1) if self.stocks: self.display_manager.defer_update(self.stocks.update_stock_data, priority=2) if self.news: @@ -608,6 +611,17 @@ def _update_modules(self): if self.youtube: self.youtube.update() if self.text_display: self.text_display.update() if self.of_the_day: self.of_the_day.update(time.time()) + + # Update sports managers for leaderboard data + if self.leaderboard: self.leaderboard.update() + + # Update key sports managers that feed the leaderboard + if self.nfl_live: self.nfl_live.update() + if self.nfl_recent: self.nfl_recent.update() + if self.nfl_upcoming: self.nfl_upcoming.update() + if self.ncaa_fb_live: self.ncaa_fb_live.update() + if self.ncaa_fb_recent: self.ncaa_fb_recent.update() + if self.ncaa_fb_upcoming: self.ncaa_fb_upcoming.update() # News manager fetches data when displayed, not during updates # if self.news_manager: self.news_manager.fetch_news_data() From cbbfd7ebf2e66f50ab5b589260ef234612a70f04 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:37:30 -0400 Subject: [PATCH 02/11] Apply PR #60 leaderboard performance optimizations - Change scroll_delay from 0.05s to 0.01s (100fps instead of 20fps) - Remove conditional scrolling logic - scroll every frame for smooth animation - Add FPS tracking and logging for performance monitoring - Restore high-framerate scrolling that was working before PR #39 merge These changes restore the smooth leaderboard scrolling performance that was achieved in PR #60 but was lost during the PR #39 merge. --- src/leaderboard_manager.py | 41 +++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/leaderboard_manager.py b/src/leaderboard_manager.py index bb37612e1..a469a48ba 100644 --- a/src/leaderboard_manager.py +++ b/src/leaderboard_manager.py @@ -40,7 +40,7 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.enabled_sports = self.leaderboard_config.get('enabled_sports', {}) self.update_interval = self.leaderboard_config.get('update_interval', 3600) self.scroll_speed = self.leaderboard_config.get('scroll_speed', 2) - self.scroll_delay = self.leaderboard_config.get('scroll_delay', 0.05) + self.scroll_delay = self.leaderboard_config.get('scroll_delay', 0.01) self.display_duration = self.leaderboard_config.get('display_duration', 30) self.loop = self.leaderboard_config.get('loop', True) self.request_timeout = self.leaderboard_config.get('request_timeout', 30) @@ -53,6 +53,12 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): self.dynamic_duration = 60 # Default duration in seconds self.total_scroll_width = 0 # Track total width for dynamic duration calculation + # FPS tracking variables + self.frame_times = [] # Store last 30 frame times for averaging + self.last_frame_time = 0 + self.fps_log_interval = 10.0 # Log FPS every 10 seconds + self.last_fps_log_time = 0 + # Initialize managers self.cache_manager = CacheManager() # Store reference to config instead of creating new ConfigManager @@ -1329,20 +1335,31 @@ def display(self, force_clear: bool = False) -> None: try: current_time = time.time() - # Check if we should be scrolling - should_scroll = current_time - self.last_scroll_time >= self.scroll_delay + # FPS tracking + if self.last_frame_time > 0: + frame_time = current_time - self.last_frame_time + self.frame_times.append(frame_time) + if len(self.frame_times) > 30: + self.frame_times.pop(0) + + # Log FPS every 10 seconds + if current_time - self.last_fps_log_time >= self.fps_log_interval: + if self.frame_times: + avg_frame_time = sum(self.frame_times) / len(self.frame_times) + fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + logger.info(f"Leaderboard FPS: {fps:.1f} (avg frame time: {avg_frame_time*1000:.1f}ms)") + self.last_fps_log_time = current_time + + self.last_frame_time = current_time # Signal scrolling state to display manager - if should_scroll: - self.display_manager.set_scrolling_state(True) - else: - # If we're not scrolling, check if we should process deferred updates - self.display_manager.process_deferred_updates() + self.display_manager.set_scrolling_state(True) + + # Scroll the image every frame for smooth animation + self.scroll_position += self.scroll_speed - # Scroll the image - if should_scroll: - self.scroll_position += self.scroll_speed - self.last_scroll_time = current_time + # Add scroll delay like other managers for consistent timing + time.sleep(self.scroll_delay) # Calculate crop region width = self.display_manager.matrix.width From 2b2f5ee7abd086d7a247878238b967fdc904fb3a Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:39:39 -0400 Subject: [PATCH 03/11] Fix critical bugs identified in PR #39 review - Fix record filtering logic bug: change away_record == set to away_record in set - Fix incorrect sport specification: change 'nfl' to 'ncaa_fb' for NCAA Football data requests - These bugs were causing incorrect data display and wrong sport data fetching Addresses issues found by cursor bot in PR #39 review: - Record filtering was always evaluating to False - NCAA Football was fetching NFL data instead of college football data --- src/base_classes/sports.py | 2 +- src/ncaa_fb_managers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/base_classes/sports.py b/src/base_classes/sports.py index 5b87b9061..94d376114 100644 --- a/src/base_classes/sports.py +++ b/src/base_classes/sports.py @@ -393,7 +393,7 @@ def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, D # Don't show "0-0" records - set to blank instead if home_record in {"0-0", "0-0-0"}: home_record = '' - if away_record == {"0-0", "0-0-0"}: + if away_record in {"0-0", "0-0-0"}: away_record = '' details = { diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 9eb11a3b7..155a04c9c 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -140,7 +140,7 @@ def fetch_callback(result): # Submit background fetch request request_id = self.background_service.submit_fetch_request( - sport="nfl", + sport="ncaa_fb", year=season_year, url=ESPN_NCAAFB_SCOREBOARD_URL, cache_key=cache_key, From 78ed58397eba5bd91b1f662e4d3ce4f22657a55b Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:41:11 -0400 Subject: [PATCH 04/11] Enhance cache clearing implementation from PR #39 - Add detailed logging to cache clearing process for better visibility - Log cache clearing statistics (memory entries and file count) - Improve startup logging to show cache clearing and data refetch process - Addresses legoguy1000's comment about preventing stale data issues This enhances the cache clearing implementation that was added in PR #39 to help prevent legacy cache issues and stale data problems. --- src/cache_manager.py | 5 +++++ src/display_controller.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/cache_manager.py b/src/cache_manager.py index 93ac7741f..9dad671cf 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -308,14 +308,19 @@ def clear_cache(self, key: Optional[str] = None) -> None: cache_path = self._get_cache_path(key) if cache_path and os.path.exists(cache_path): os.remove(cache_path) + self.logger.info(f"Cleared cache for key: {key}") else: # Clear all keys + memory_count = len(self._memory_cache) self._memory_cache.clear() self._memory_cache_timestamps.clear() + file_count = 0 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)) + file_count += 1 + self.logger.info(f"Cleared all cache: {memory_count} memory entries, {file_count} cache files") 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/display_controller.py b/src/display_controller.py index d5148da0c..29f621ea8 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -1020,8 +1020,10 @@ def run(self): return try: + logger.info("Clearing cache and refetching data to prevent stale data issues...") self.cache_manager.clear_cache() self._update_modules() + logger.info("Cache cleared, waiting 5 seconds for fresh data fetch...") time.sleep(5) self.current_display_mode = self.available_modes[self.current_mode_index] if self.available_modes else 'none' while True: From 23ac421273a36f2bd962fc1e95baa833cf7658a4 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:27:31 -0400 Subject: [PATCH 05/11] continuing on base_classes - added baseball and api extractor since we don't use ESPN api for all sports --- src/base_classes/api_extractors.py | 379 +++++++++++++++++++++++++++++ src/base_classes/baseball.py | 175 +++++++++++++ src/base_classes/data_sources.py | 301 +++++++++++++++++++++++ src/base_classes/sport_configs.py | 195 +++++++++++++++ src/base_classes/sports.py | 176 +++++++++++++- src/mlb_managers.py | 278 +++++++++++++++++++++ src/ncaa_fb_managers.py | 4 + src/ncaam_hockey_managers.py | 4 + src/nfl_managers.py | 4 + 9 files changed, 1506 insertions(+), 10 deletions(-) create mode 100644 src/base_classes/api_extractors.py create mode 100644 src/base_classes/baseball.py create mode 100644 src/base_classes/data_sources.py create mode 100644 src/base_classes/sport_configs.py create mode 100644 src/mlb_managers.py diff --git a/src/base_classes/api_extractors.py b/src/base_classes/api_extractors.py new file mode 100644 index 000000000..c8fcccf4f --- /dev/null +++ b/src/base_classes/api_extractors.py @@ -0,0 +1,379 @@ +""" +Abstract API Data Extraction Layer + +This module provides a pluggable system for extracting game data from different +sports APIs. Each sport can have its own extractor that handles sport-specific +fields and data structures. +""" + +from abc import ABC, abstractmethod +from typing import Dict, Any, Optional, List +import logging +from datetime import datetime +import pytz + +class APIDataExtractor(ABC): + """Abstract base class for API data extraction.""" + + def __init__(self, logger: logging.Logger): + self.logger = logger + + @abstractmethod + def extract_game_details(self, game_event: Dict) -> Optional[Dict]: + """Extract common game details from raw API data.""" + pass + + @abstractmethod + def get_sport_specific_fields(self, game_event: Dict) -> Dict: + """Extract sport-specific fields (downs, innings, periods, etc.).""" + pass + + def _extract_common_details(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]: + """Extract common game details that work across all sports.""" + if not game_event: + return None, None, None, None, None + + try: + competition = game_event["competitions"][0] + status = competition["status"] + competitors = competition["competitors"] + game_date_str = game_event["date"] + situation = competition.get("situation") + + # Parse game time + start_time_utc = None + try: + start_time_utc = datetime.fromisoformat(game_date_str.replace("Z", "+00:00")) + except ValueError: + self.logger.warning(f"Could not parse game date: {game_date_str}") + + # Extract teams + home_team = next((c for c in competitors if c.get("homeAway") == "home"), None) + away_team = next((c for c in competitors if c.get("homeAway") == "away"), None) + + if not home_team or not away_team: + self.logger.warning(f"Could not find home or away team in event: {game_event.get('id')}") + return None, None, None, None, None + + return { + "game_event": game_event, + "competition": competition, + "status": status, + "situation": situation, + "start_time_utc": start_time_utc, + "home_team": home_team, + "away_team": away_team + }, home_team, away_team, status, situation + + except Exception as e: + self.logger.error(f"Error extracting common details: {e}") + return None, None, None, None, None + + +class ESPNFootballExtractor(APIDataExtractor): + """ESPN API extractor for football (NFL/NCAA).""" + + def extract_game_details(self, game_event: Dict) -> Optional[Dict]: + """Extract football game details from ESPN API.""" + common_data, home_team, away_team, status, situation = self._extract_common_details(game_event) + if not common_data: + return None + + try: + # Extract basic team info + home_abbr = home_team["team"]["abbreviation"] + away_abbr = away_team["team"]["abbreviation"] + home_score = home_team.get("score", "0") + away_score = away_team.get("score", "0") + + # Extract sport-specific fields + sport_fields = self.get_sport_specific_fields(game_event) + + # Build game details + details = { + "id": game_event.get("id"), + "home_abbr": home_abbr, + "away_abbr": away_abbr, + "home_score": str(home_score), + "away_score": str(away_score), + "home_team_name": home_team["team"].get("displayName", ""), + "away_team_name": away_team["team"].get("displayName", ""), + "status_text": status["type"].get("shortDetail", ""), + "is_live": status["type"]["state"] == "in", + "is_final": status["type"]["state"] == "post", + "is_upcoming": status["type"]["state"] == "pre", + **sport_fields # Add sport-specific fields + } + + return details + + except Exception as e: + self.logger.error(f"Error extracting football game details: {e}") + return None + + def get_sport_specific_fields(self, game_event: Dict) -> Dict: + """Extract football-specific fields.""" + try: + competition = game_event["competitions"][0] + status = competition["status"] + situation = competition.get("situation", {}) + + sport_fields = { + "down": "", + "distance": "", + "possession": "", + "is_redzone": False, + "home_timeouts": 0, + "away_timeouts": 0, + "scoring_event": "" + } + + if situation and status["type"]["state"] == "in": + sport_fields.update({ + "down": situation.get("down", ""), + "distance": situation.get("distance", ""), + "possession": situation.get("possession", ""), + "is_redzone": situation.get("isRedZone", False), + "home_timeouts": situation.get("homeTimeouts", 0), + "away_timeouts": situation.get("awayTimeouts", 0) + }) + + # Detect scoring events + status_detail = status["type"].get("detail", "").lower() + if "touchdown" in status_detail or "field goal" in status_detail: + sport_fields["scoring_event"] = status_detail + + return sport_fields + + except Exception as e: + self.logger.error(f"Error extracting football-specific fields: {e}") + return {} + + +class ESPNBaseballExtractor(APIDataExtractor): + """ESPN API extractor for baseball (MLB).""" + + def extract_game_details(self, game_event: Dict) -> Optional[Dict]: + """Extract baseball game details from ESPN API.""" + common_data, home_team, away_team, status, situation = self._extract_common_details(game_event) + if not common_data: + return None + + try: + # Extract basic team info + home_abbr = home_team["team"]["abbreviation"] + away_abbr = away_team["team"]["abbreviation"] + home_score = home_team.get("score", "0") + away_score = away_team.get("score", "0") + + # Extract sport-specific fields + sport_fields = self.get_sport_specific_fields(game_event) + + # Build game details + details = { + "id": game_event.get("id"), + "home_abbr": home_abbr, + "away_abbr": away_abbr, + "home_score": str(home_score), + "away_score": str(away_score), + "home_team_name": home_team["team"].get("displayName", ""), + "away_team_name": away_team["team"].get("displayName", ""), + "status_text": status["type"].get("shortDetail", ""), + "is_live": status["type"]["state"] == "in", + "is_final": status["type"]["state"] == "post", + "is_upcoming": status["type"]["state"] == "pre", + **sport_fields # Add sport-specific fields + } + + return details + + except Exception as e: + self.logger.error(f"Error extracting baseball game details: {e}") + return None + + def get_sport_specific_fields(self, game_event: Dict) -> Dict: + """Extract baseball-specific fields.""" + try: + competition = game_event["competitions"][0] + status = competition["status"] + situation = competition.get("situation", {}) + + sport_fields = { + "inning": "", + "outs": 0, + "bases": "", + "strikes": 0, + "balls": 0, + "pitcher": "", + "batter": "" + } + + if situation and status["type"]["state"] == "in": + sport_fields.update({ + "inning": situation.get("inning", ""), + "outs": situation.get("outs", 0), + "bases": situation.get("bases", ""), + "strikes": situation.get("strikes", 0), + "balls": situation.get("balls", 0), + "pitcher": situation.get("pitcher", ""), + "batter": situation.get("batter", "") + }) + + return sport_fields + + except Exception as e: + self.logger.error(f"Error extracting baseball-specific fields: {e}") + return {} + + +class ESPNHockeyExtractor(APIDataExtractor): + """ESPN API extractor for hockey (NHL/NCAA).""" + + def extract_game_details(self, game_event: Dict) -> Optional[Dict]: + """Extract hockey game details from ESPN API.""" + common_data, home_team, away_team, status, situation = self._extract_common_details(game_event) + if not common_data: + return None + + try: + # Extract basic team info + home_abbr = home_team["team"]["abbreviation"] + away_abbr = away_team["team"]["abbreviation"] + home_score = home_team.get("score", "0") + away_score = away_team.get("score", "0") + + # Extract sport-specific fields + sport_fields = self.get_sport_specific_fields(game_event) + + # Build game details + details = { + "id": game_event.get("id"), + "home_abbr": home_abbr, + "away_abbr": away_abbr, + "home_score": str(home_score), + "away_score": str(away_score), + "home_team_name": home_team["team"].get("displayName", ""), + "away_team_name": away_team["team"].get("displayName", ""), + "status_text": status["type"].get("shortDetail", ""), + "is_live": status["type"]["state"] == "in", + "is_final": status["type"]["state"] == "post", + "is_upcoming": status["type"]["state"] == "pre", + **sport_fields # Add sport-specific fields + } + + return details + + except Exception as e: + self.logger.error(f"Error extracting hockey game details: {e}") + return None + + def get_sport_specific_fields(self, game_event: Dict) -> Dict: + """Extract hockey-specific fields.""" + try: + competition = game_event["competitions"][0] + status = competition["status"] + situation = competition.get("situation", {}) + + sport_fields = { + "period": "", + "period_text": "", + "power_play": False, + "penalties": "", + "shots_on_goal": {"home": 0, "away": 0} + } + + if situation and status["type"]["state"] == "in": + period = status.get("period", 0) + period_text = "" + if period == 1: + period_text = "P1" + elif period == 2: + period_text = "P2" + elif period == 3: + period_text = "P3" + elif period > 3: + period_text = f"OT{period-3}" + + sport_fields.update({ + "period": str(period), + "period_text": period_text, + "power_play": situation.get("isPowerPlay", False), + "penalties": situation.get("penalties", ""), + "shots_on_goal": { + "home": situation.get("homeShots", 0), + "away": situation.get("awayShots", 0) + } + }) + + return sport_fields + + except Exception as e: + self.logger.error(f"Error extracting hockey-specific fields: {e}") + return {} + + +class SoccerAPIExtractor(APIDataExtractor): + """Generic extractor for soccer APIs (different structure than ESPN).""" + + def extract_game_details(self, game_event: Dict) -> Optional[Dict]: + """Extract soccer game details from various soccer APIs.""" + # This would need to be adapted based on the specific soccer API being used + # For now, return a basic structure + try: + return { + "id": game_event.get("id"), + "home_abbr": game_event.get("home_team", {}).get("abbreviation", ""), + "away_abbr": game_event.get("away_team", {}).get("abbreviation", ""), + "home_score": str(game_event.get("home_score", "0")), + "away_score": str(game_event.get("away_score", "0")), + "home_team_name": game_event.get("home_team", {}).get("name", ""), + "away_team_name": game_event.get("away_team", {}).get("name", ""), + "status_text": game_event.get("status", ""), + "is_live": game_event.get("is_live", False), + "is_final": game_event.get("is_final", False), + "is_upcoming": game_event.get("is_upcoming", False), + **self.get_sport_specific_fields(game_event) + } + except Exception as e: + self.logger.error(f"Error extracting soccer game details: {e}") + return None + + def get_sport_specific_fields(self, game_event: Dict) -> Dict: + """Extract soccer-specific fields.""" + try: + return { + "half": game_event.get("half", ""), + "stoppage_time": game_event.get("stoppage_time", ""), + "cards": { + "home_yellow": game_event.get("home_yellow_cards", 0), + "away_yellow": game_event.get("away_yellow_cards", 0), + "home_red": game_event.get("home_red_cards", 0), + "away_red": game_event.get("away_red_cards", 0) + }, + "possession": { + "home": game_event.get("home_possession", 0), + "away": game_event.get("away_possession", 0) + } + } + except Exception as e: + self.logger.error(f"Error extracting soccer-specific fields: {e}") + return {} + + +def get_extractor_for_sport(sport_key: str, logger: logging.Logger) -> APIDataExtractor: + """Factory function to get the appropriate extractor for a sport.""" + extractors = { + 'nfl': ESPNFootballExtractor, + 'ncaa_fb': ESPNFootballExtractor, + 'mlb': ESPNBaseballExtractor, + 'nhl': ESPNHockeyExtractor, + 'ncaam_hockey': ESPNHockeyExtractor, + 'soccer': SoccerAPIExtractor + } + + extractor_class = extractors.get(sport_key) + if not extractor_class: + logger.warning(f"No extractor found for sport: {sport_key}, using generic ESPN extractor") + return ESPNFootballExtractor(logger) + + return extractor_class(logger) diff --git a/src/base_classes/baseball.py b/src/base_classes/baseball.py new file mode 100644 index 000000000..c3b2c4ae3 --- /dev/null +++ b/src/base_classes/baseball.py @@ -0,0 +1,175 @@ +""" +Baseball Base Classes + +This module provides baseball-specific base classes that extend the core sports functionality +with baseball-specific logic for innings, outs, bases, strikes, balls, etc. +""" + +from typing import Dict, Any, Optional, List +from src.base_classes.sports import SportsCore +import logging + +class Baseball(SportsCore): + """Base class for baseball sports with common functionality.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): + super().__init__(config, display_manager, cache_manager, logger, sport_key) + + # Baseball-specific configuration + self.show_innings = self.mode_config.get("show_innings", True) + self.show_outs = self.mode_config.get("show_outs", True) + self.show_bases = self.mode_config.get("show_bases", True) + self.show_count = self.mode_config.get("show_count", True) + self.show_pitcher_batter = self.mode_config.get("show_pitcher_batter", False) + + def _get_baseball_display_text(self, game: Dict) -> str: + """Get baseball-specific display text.""" + try: + display_parts = [] + + # Inning information + if self.show_innings: + inning = game.get('inning', '') + if inning: + display_parts.append(f"Inning: {inning}") + + # Outs information + if self.show_outs: + outs = game.get('outs', 0) + if outs is not None: + display_parts.append(f"Outs: {outs}") + + # Bases information + if self.show_bases: + bases = game.get('bases', '') + if bases: + display_parts.append(f"Bases: {bases}") + + # Count information + if self.show_count: + strikes = game.get('strikes', 0) + balls = game.get('balls', 0) + if strikes is not None and balls is not None: + display_parts.append(f"Count: {balls}-{strikes}") + + # Pitcher/Batter information + if self.show_pitcher_batter: + pitcher = game.get('pitcher', '') + batter = game.get('batter', '') + if pitcher: + display_parts.append(f"Pitcher: {pitcher}") + if batter: + display_parts.append(f"Batter: {batter}") + + return " | ".join(display_parts) if display_parts else "" + + except Exception as e: + self.logger.error(f"Error getting baseball display text: {e}") + return "" + + def _is_baseball_game_live(self, game: Dict) -> bool: + """Check if a baseball game is currently live.""" + try: + # Check if game is marked as live + is_live = game.get('is_live', False) + if is_live: + return True + + # Check inning to determine if game is active + inning = game.get('inning', '') + if inning and inning != 'Final': + return True + + return False + + except Exception as e: + self.logger.error(f"Error checking if baseball game is live: {e}") + return False + + def _get_baseball_game_status(self, game: Dict) -> str: + """Get baseball-specific game status.""" + try: + status = game.get('status_text', '') + inning = game.get('inning', '') + + if self._is_baseball_game_live(game): + if inning: + return f"Live - {inning}" + else: + return "Live" + elif game.get('is_final', False): + return "Final" + elif game.get('is_upcoming', False): + return "Upcoming" + else: + return status + + except Exception as e: + self.logger.error(f"Error getting baseball game status: {e}") + return "" + + +class BaseballLive(Baseball): + """Base class for live baseball games.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): + super().__init__(config, display_manager, cache_manager, logger, sport_key) + self.logger.info(f"{sport_key.upper()} Live Manager initialized") + + def _should_show_baseball_game(self, game: Dict) -> bool: + """Determine if a baseball game should be shown.""" + try: + # Only show live games + if not self._is_baseball_game_live(game): + return False + + # Check if game meets display criteria + return self._should_show_game(game) + + except Exception as e: + self.logger.error(f"Error checking if baseball game should be shown: {e}") + return False + + +class BaseballRecent(Baseball): + """Base class for recent baseball games.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): + super().__init__(config, display_manager, cache_manager, logger, sport_key) + self.logger.info(f"{sport_key.upper()} Recent Manager initialized") + + def _should_show_baseball_game(self, game: Dict) -> bool: + """Determine if a recent baseball game should be shown.""" + try: + # Only show final games + if not game.get('is_final', False): + return False + + # Check if game meets display criteria + return self._should_show_game(game) + + except Exception as e: + self.logger.error(f"Error checking if baseball game should be shown: {e}") + return False + + +class BaseballUpcoming(Baseball): + """Base class for upcoming baseball games.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): + super().__init__(config, display_manager, cache_manager, logger, sport_key) + self.logger.info(f"{sport_key.upper()} Upcoming Manager initialized") + + def _should_show_baseball_game(self, game: Dict) -> bool: + """Determine if an upcoming baseball game should be shown.""" + try: + # Only show upcoming games + if not game.get('is_upcoming', False): + return False + + # Check if game meets display criteria + return self._should_show_game(game) + + except Exception as e: + self.logger.error(f"Error checking if baseball game should be shown: {e}") + return False diff --git a/src/base_classes/data_sources.py b/src/base_classes/data_sources.py new file mode 100644 index 000000000..eb9516ad6 --- /dev/null +++ b/src/base_classes/data_sources.py @@ -0,0 +1,301 @@ +""" +Pluggable Data Source Architecture + +This module provides abstract data sources that can be plugged into the sports system +to support different APIs and data providers. +""" + +from abc import ABC, abstractmethod +from typing import Dict, Any, Optional, List +import requests +import logging +from datetime import datetime, timedelta +import time + +class DataSource(ABC): + """Abstract base class for data sources.""" + + def __init__(self, logger: logging.Logger): + self.logger = logger + self.session = requests.Session() + + # Configure retry strategy + from requests.adapters import HTTPAdapter + from urllib3.util.retry import Retry + + retry_strategy = Retry( + total=5, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + @abstractmethod + def fetch_live_games(self, sport: str, league: str) -> List[Dict]: + """Fetch live games for a sport/league.""" + pass + + @abstractmethod + def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]: + """Fetch schedule for a sport/league within date range.""" + pass + + @abstractmethod + def fetch_standings(self, sport: str, league: str) -> Dict: + """Fetch standings for a sport/league.""" + pass + + def get_headers(self) -> Dict[str, str]: + """Get headers for API requests.""" + return { + 'User-Agent': 'LEDMatrix/1.0', + 'Accept': 'application/json' + } + + +class ESPNDataSource(DataSource): + """ESPN API data source.""" + + def __init__(self, logger: logging.Logger): + super().__init__(logger) + self.base_url = "https://site.api.espn.com/apis/site/v2/sports" + + def fetch_live_games(self, sport: str, league: str) -> List[Dict]: + """Fetch live games from ESPN API.""" + try: + url = f"{self.base_url}/{sport}/{league}/scoreboard" + response = self.session.get(url, headers=self.get_headers(), timeout=15) + response.raise_for_status() + + data = response.json() + events = data.get('events', []) + + # Filter for live games + live_events = [event for event in events + if event.get('competitions', [{}])[0].get('status', {}).get('type', {}).get('state') == 'in'] + + self.logger.debug(f"Fetched {len(live_events)} live games for {sport}/{league}") + return live_events + + except Exception as e: + self.logger.error(f"Error fetching live games from ESPN: {e}") + return [] + + def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]: + """Fetch schedule from ESPN API.""" + try: + start_date, end_date = date_range + url = f"{self.base_url}/{sport}/{league}/scoreboard" + + params = { + 'dates': f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}" + } + + response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) + response.raise_for_status() + + data = response.json() + events = data.get('events', []) + + self.logger.debug(f"Fetched {len(events)} scheduled games for {sport}/{league}") + return events + + except Exception as e: + self.logger.error(f"Error fetching schedule from ESPN: {e}") + return [] + + def fetch_standings(self, sport: str, league: str) -> Dict: + """Fetch standings from ESPN API.""" + try: + url = f"{self.base_url}/{sport}/{league}/standings" + response = self.session.get(url, headers=self.get_headers(), timeout=15) + response.raise_for_status() + + data = response.json() + self.logger.debug(f"Fetched standings for {sport}/{league}") + return data + + except Exception as e: + self.logger.error(f"Error fetching standings from ESPN: {e}") + return {} + + +class MLBAPIDataSource(DataSource): + """MLB API data source.""" + + def __init__(self, logger: logging.Logger): + super().__init__(logger) + self.base_url = "https://statsapi.mlb.com/api/v1" + + def fetch_live_games(self, sport: str, league: str) -> List[Dict]: + """Fetch live games from MLB API.""" + try: + url = f"{self.base_url}/schedule" + params = { + 'sportId': 1, # MLB + 'date': datetime.now().strftime('%Y-%m-%d'), + 'hydrate': 'game,team,venue,weather' + } + + response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) + response.raise_for_status() + + data = response.json() + games = data.get('dates', [{}])[0].get('games', []) + + # Filter for live games + live_games = [game for game in games + if game.get('status', {}).get('abstractGameState') == 'Live'] + + self.logger.debug(f"Fetched {len(live_games)} live games from MLB API") + return live_games + + except Exception as e: + self.logger.error(f"Error fetching live games from MLB API: {e}") + return [] + + def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]: + """Fetch schedule from MLB API.""" + try: + start_date, end_date = date_range + url = f"{self.base_url}/schedule" + + params = { + 'sportId': 1, # MLB + 'startDate': start_date.strftime('%Y-%m-%d'), + 'endDate': end_date.strftime('%Y-%m-%d'), + 'hydrate': 'game,team,venue' + } + + response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) + response.raise_for_status() + + data = response.json() + all_games = [] + for date_data in data.get('dates', []): + all_games.extend(date_data.get('games', [])) + + self.logger.debug(f"Fetched {len(all_games)} scheduled games from MLB API") + return all_games + + except Exception as e: + self.logger.error(f"Error fetching schedule from MLB API: {e}") + return [] + + def fetch_standings(self, sport: str, league: str) -> Dict: + """Fetch standings from MLB API.""" + try: + url = f"{self.base_url}/standings" + params = { + 'leagueId': 103, # American League + 'season': datetime.now().year, + 'standingsType': 'regularSeason' + } + + response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) + response.raise_for_status() + + data = response.json() + self.logger.debug(f"Fetched standings from MLB API") + return data + + except Exception as e: + self.logger.error(f"Error fetching standings from MLB API: {e}") + return {} + + +class SoccerAPIDataSource(DataSource): + """Soccer API data source (generic structure).""" + + def __init__(self, logger: logging.Logger, api_key: str = None): + super().__init__(logger) + self.api_key = api_key + self.base_url = "https://api.football-data.org/v4" # Example API + + def get_headers(self) -> Dict[str, str]: + """Get headers with API key for soccer API.""" + headers = super().get_headers() + if self.api_key: + headers['X-Auth-Token'] = self.api_key + return headers + + def fetch_live_games(self, sport: str, league: str) -> List[Dict]: + """Fetch live games from soccer API.""" + try: + # This would need to be adapted based on the specific soccer API + url = f"{self.base_url}/matches" + params = { + 'status': 'LIVE', + 'competition': league + } + + response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) + response.raise_for_status() + + data = response.json() + matches = data.get('matches', []) + + self.logger.debug(f"Fetched {len(matches)} live games from soccer API") + return matches + + except Exception as e: + self.logger.error(f"Error fetching live games from soccer API: {e}") + return [] + + def fetch_schedule(self, sport: str, league: str, date_range: tuple) -> List[Dict]: + """Fetch schedule from soccer API.""" + try: + start_date, end_date = date_range + url = f"{self.base_url}/matches" + + params = { + 'competition': league, + 'dateFrom': start_date.strftime('%Y-%m-%d'), + 'dateTo': end_date.strftime('%Y-%m-%d') + } + + response = self.session.get(url, headers=self.get_headers(), params=params, timeout=15) + response.raise_for_status() + + data = response.json() + matches = data.get('matches', []) + + self.logger.debug(f"Fetched {len(matches)} scheduled games from soccer API") + return matches + + except Exception as e: + self.logger.error(f"Error fetching schedule from soccer API: {e}") + return [] + + def fetch_standings(self, sport: str, league: str) -> Dict: + """Fetch standings from soccer API.""" + try: + url = f"{self.base_url}/competitions/{league}/standings" + response = self.session.get(url, headers=self.get_headers(), timeout=15) + response.raise_for_status() + + data = response.json() + self.logger.debug(f"Fetched standings from soccer API") + return data + + except Exception as e: + self.logger.error(f"Error fetching standings from soccer API: {e}") + return {} + + +def get_data_source_for_sport(sport_key: str, data_source_type: str, logger: logging.Logger, **kwargs) -> DataSource: + """Factory function to get the appropriate data source for a sport.""" + data_sources = { + 'espn': ESPNDataSource, + 'mlb_api': MLBAPIDataSource, + 'soccer_api': SoccerAPIDataSource + } + + data_source_class = data_sources.get(data_source_type) + if not data_source_class: + logger.warning(f"No data source found for type: {data_source_type}, using ESPN") + return ESPNDataSource(logger) + + return data_source_class(logger, **kwargs) diff --git a/src/base_classes/sport_configs.py b/src/base_classes/sport_configs.py new file mode 100644 index 000000000..c2a919177 --- /dev/null +++ b/src/base_classes/sport_configs.py @@ -0,0 +1,195 @@ +""" +Sport-Specific Configuration System + +This module provides sport-specific configurations including update cadences, +season characteristics, and sport-specific fields for different sports. +""" + +from typing import Dict, Any, List +import logging + +class SportConfig: + """Configuration for a specific sport.""" + + def __init__(self, sport_key: str, config: Dict[str, Any]): + self.sport_key = sport_key + self.config = config + + # Sport-specific characteristics + self.update_cadence = config.get('update_cadence', 'daily') + self.season_length = config.get('season_length', 16) + self.games_per_week = config.get('games_per_week', 1) + self.api_endpoints = config.get('api_endpoints', ['scoreboard']) + self.sport_specific_fields = config.get('sport_specific_fields', []) + self.update_interval_seconds = config.get('update_interval_seconds', 60) + self.logo_dir = config.get('logo_dir', 'assets/sports/ncaa_logos') + + # Display characteristics + self.show_records = config.get('show_records', False) + self.show_ranking = config.get('show_ranking', False) + self.show_odds = config.get('show_odds', False) + + # Data source configuration + self.data_source_type = config.get('data_source_type', 'espn') + self.api_base_url = config.get('api_base_url', '') + self.requires_authentication = config.get('requires_authentication', False) + + def get_update_interval(self) -> int: + """Get the appropriate update interval for this sport.""" + return self.update_interval_seconds + + def should_update_now(self, last_update: float, current_time: float) -> bool: + """Check if this sport should be updated based on its cadence.""" + time_since_update = current_time - last_update + + if self.update_cadence == 'daily': + return time_since_update >= 3600 # 1 hour + elif self.update_cadence == 'weekly': + return time_since_update >= 86400 # 24 hours + elif self.update_cadence == 'hourly': + return time_since_update >= 3600 # 1 hour + elif self.update_cadence == 'live_only': + return time_since_update >= 30 # 30 seconds for live games + else: + return time_since_update >= self.update_interval_seconds + + +def get_sport_configs() -> Dict[str, Dict[str, Any]]: + """Get all sport-specific configurations.""" + return { + 'nfl': { + 'update_cadence': 'weekly', + 'season_length': 17, + 'games_per_week': 1, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], + 'update_interval_seconds': 60, + 'logo_dir': 'assets/sports/nfl_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl' + }, + 'ncaa_fb': { + 'update_cadence': 'weekly', + 'season_length': 12, + 'games_per_week': 1, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], + 'update_interval_seconds': 60, + 'logo_dir': 'assets/sports/ncaa_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football' + }, + 'mlb': { + 'update_cadence': 'daily', + 'season_length': 162, + 'games_per_week': 6, + 'api_endpoints': ['scoreboard', 'standings', 'stats'], + 'sport_specific_fields': ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'], + 'update_interval_seconds': 30, + 'logo_dir': 'assets/sports/mlb_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'mlb_api', + 'api_base_url': 'https://statsapi.mlb.com/api/v1' + }, + 'nhl': { + 'update_cadence': 'daily', + 'season_length': 82, + 'games_per_week': 3, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['period', 'power_play', 'penalties', 'shots_on_goal'], + 'update_interval_seconds': 30, + 'logo_dir': 'assets/sports/nhl_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey/nhl' + }, + 'ncaam_hockey': { + 'update_cadence': 'weekly', + 'season_length': 34, + 'games_per_week': 2, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['period', 'power_play', 'penalties', 'shots_on_goal'], + 'update_interval_seconds': 60, + 'logo_dir': 'assets/sports/ncaa_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': False, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey' + }, + 'soccer': { + 'update_cadence': 'weekly', + 'season_length': 34, + 'games_per_week': 1, + 'api_endpoints': ['fixtures', 'standings'], + 'sport_specific_fields': ['half', 'stoppage_time', 'cards', 'possession'], + 'update_interval_seconds': 60, + 'logo_dir': 'assets/sports/soccer_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'soccer_api', + 'api_base_url': 'https://api.football-data.org/v4' + }, + 'nba': { + 'update_cadence': 'daily', + 'season_length': 82, + 'games_per_week': 3, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['quarter', 'time_remaining', 'fouls', 'timeouts'], + 'update_interval_seconds': 30, + 'logo_dir': 'assets/sports/nba_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba' + } + } + + +def get_sport_config(sport_key: str, logger: logging.Logger) -> SportConfig: + """Get configuration for a specific sport.""" + configs = get_sport_configs() + sport_config = configs.get(sport_key, {}) + + if not sport_config: + logger.warning(f"No configuration found for sport: {sport_key}, using default") + sport_config = { + 'update_cadence': 'daily', + 'season_length': 16, + 'games_per_week': 1, + 'api_endpoints': ['scoreboard'], + 'sport_specific_fields': [], + 'update_interval_seconds': 60, + 'logo_dir': 'assets/sports/ncaa_logos', + 'show_records': False, + 'show_ranking': False, + 'show_odds': False, + 'data_source_type': 'espn', + 'api_base_url': '' + } + + return SportConfig(sport_key, sport_config) + + +def get_sports_by_update_cadence(cadence: str) -> List[str]: + """Get all sports that use a specific update cadence.""" + configs = get_sport_configs() + return [sport for sport, config in configs.items() if config.get('update_cadence') == cadence] + + +def get_sports_by_data_source(data_source_type: str) -> List[str]: + """Get all sports that use a specific data source.""" + configs = get_sport_configs() + return [sport for sport, config in configs.items() if config.get('data_source_type') == data_source_type] diff --git a/src/base_classes/sports.py b/src/base_classes/sports.py index 94d376114..9d5c2aec3 100644 --- a/src/base_classes/sports.py +++ b/src/base_classes/sports.py @@ -15,6 +15,11 @@ from src.logo_downloader import download_missing_logo, LogoDownloader from pathlib import Path +# Import new architecture components +from .api_extractors import get_extractor_for_sport +from .sport_configs import get_sport_config +from .data_sources import get_data_source_for_sport + class SportsCore: def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): self.logger = logger @@ -28,6 +33,15 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cach self.display_height = self.display_manager.matrix.height self.sport_key = sport_key + + # Initialize new architecture components + self.sport_config = get_sport_config(sport_key, logger) + self.api_extractor = get_extractor_for_sport(sport_key, logger) + self.data_source = get_data_source_for_sport( + sport_key, + self.sport_config.data_source_type, + logger + ) self.mode_config = config.get(f"{sport_key}_scoreboard", {}) # Changed config key self.is_enabled = self.mode_config.get("enabled", False) self.show_odds = self.mode_config.get("show_odds", False) @@ -267,20 +281,143 @@ def _load_and_resize_logo(self, team_id: str, team_abbrev: str, logo_path: Path, return None def _fetch_data(self) -> Optional[Dict]: - """Override this from the sports class""" - pass + """Fetch data using the new architecture components.""" + try: + # Use the data source to fetch live games + live_games = self.data_source.fetch_live_games(self.sport_key, self.sport_key) + + if not live_games: + self.logger.debug(f"No live games found for {self.sport_key}") + return None + + # Use the API extractor to process each game + processed_games = [] + for game_event in live_games: + game_details = self.api_extractor.extract_game_details(game_event) + if game_details: + # Add sport-specific fields + sport_fields = self.api_extractor.get_sport_specific_fields(game_event) + game_details.update(sport_fields) + + # Fetch odds if enabled + if self.show_odds: + self._fetch_odds(game_details, self.sport_key, self.sport_key) + + processed_games.append(game_details) + + if processed_games: + self.logger.debug(f"Successfully processed {len(processed_games)} games for {self.sport_key}") + return { + 'games': processed_games, + 'sport': self.sport_key, + 'timestamp': time.time() + } + else: + self.logger.debug(f"No valid games processed for {self.sport_key}") + return None + + except Exception as e: + self.logger.error(f"Error fetching data for {self.sport_key}: {e}") + return None def _get_partial_schedule_data(self, year: int) -> List[Dict]: - """Override this from the sports class""" - return [] + """Get schedule data using the new architecture components.""" + try: + # Calculate date range for the year + start_date = datetime(year, 1, 1) + end_date = datetime(year, 12, 31) + + # Use the data source to fetch schedule + schedule_games = self.data_source.fetch_schedule( + self.sport_key, + self.sport_key, + (start_date, end_date) + ) + + if not schedule_games: + self.logger.debug(f"No schedule data found for {self.sport_key} in {year}") + return [] + + # Use the API extractor to process each game + processed_games = [] + for game_event in schedule_games: + game_details = self.api_extractor.extract_game_details(game_event) + if game_details: + # Add sport-specific fields + sport_fields = self.api_extractor.get_sport_specific_fields(game_event) + game_details.update(sport_fields) + processed_games.append(game_details) + + self.logger.debug(f"Successfully processed {len(processed_games)} schedule games for {self.sport_key} in {year}") + return processed_games + + except Exception as e: + self.logger.error(f"Error fetching schedule data for {self.sport_key} in {year}: {e}") + return [] def _fetch_immediate_games(self) -> List[Dict]: - """Override this from the sports class""" - return [] + """Fetch immediate games using the new architecture components.""" + try: + # Use the data source to fetch live games + live_games = self.data_source.fetch_live_games(self.sport_key, self.sport_key) + + if not live_games: + self.logger.debug(f"No immediate games found for {self.sport_key}") + return [] + + # Use the API extractor to process each game + processed_games = [] + for game_event in live_games: + game_details = self.api_extractor.extract_game_details(game_event) + if game_details: + # Add sport-specific fields + sport_fields = self.api_extractor.get_sport_specific_fields(game_event) + game_details.update(sport_fields) + processed_games.append(game_details) + + self.logger.debug(f"Successfully processed {len(processed_games)} immediate games for {self.sport_key}") + return processed_games + + except Exception as e: + self.logger.error(f"Error fetching immediate games for {self.sport_key}: {e}") + return [] - def _fetch_game_odds(self, _: Dict) -> None: - """Override this from the sports class""" - pass + def _fetch_game_odds(self, game: Dict) -> None: + """Fetch odds for a specific game using the new architecture.""" + try: + if not self.show_odds: + return + + # Check if we should only fetch for favorite teams + is_favorites_only = self.mode_config.get("show_favorite_teams_only", False) + if is_favorites_only: + home_abbr = game.get('home_abbr') + away_abbr = game.get('away_abbr') + if not (home_abbr in self.favorite_teams or away_abbr in self.favorite_teams): + self.logger.debug(f"Skipping odds fetch for non-favorite game in favorites-only mode: {away_abbr}@{home_abbr}") + return + + # Determine update interval based on game state + is_live = game.get('is_live', False) + update_interval = self.mode_config.get("live_odds_update_interval", 60) if is_live \ + else self.mode_config.get("odds_update_interval", 3600) + + # Fetch odds using OddsManager + odds_data = self.odds_manager.get_odds( + sport=self.sport_key, + league=self.sport_key, + event_id=game['id'], + update_interval_seconds=update_interval + ) + + if odds_data: + game['odds'] = odds_data + self.logger.debug(f"Successfully fetched and attached odds for game {game['id']}") + else: + self.logger.debug(f"No odds data returned for game {game['id']}") + + except Exception as e: + self.logger.error(f"Error fetching odds for game {game.get('id', 'N/A')}: {e}") def _fetch_odds(self, game: Dict, sport: str, league: str) -> None: """Fetch odds for a specific game if conditions are met.""" @@ -339,7 +476,26 @@ def _should_log(self, warning_type: str, cooldown: int = 60) -> bool: return False def _fetch_team_rankings(self) -> Dict[str, int]: - return {} + """Fetch team rankings using the new architecture components.""" + try: + # Use the data source to fetch standings + standings_data = self.data_source.fetch_standings(self.sport_key, self.sport_key) + + if not standings_data: + self.logger.debug(f"No standings data found for {self.sport_key}") + return {} + + # Extract rankings from standings data + rankings = {} + # This would need to be implemented based on the specific data structure + # returned by each data source + + self.logger.debug(f"Successfully fetched rankings for {self.sport_key}") + return rankings + + except Exception as e: + self.logger.error(f"Error fetching team rankings for {self.sport_key}: {e}") + return {} def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]: if not game_event: diff --git a/src/mlb_managers.py b/src/mlb_managers.py new file mode 100644 index 000000000..81109bcae --- /dev/null +++ b/src/mlb_managers.py @@ -0,0 +1,278 @@ +""" +MLB (Major League Baseball) Managers + +This module demonstrates how to add a new sport using the new architecture. +Baseball has different characteristics than football/hockey: +- Daily games during season +- Different sport-specific fields (innings, outs, bases, etc.) +- Different data source (MLB API instead of ESPN) +""" + +import os +import time +import logging +import requests +from typing import Dict, Any, Optional, List +from datetime import datetime, timedelta +from src.display_manager import DisplayManager +from src.cache_manager import CacheManager +import pytz +from src.base_classes.sports import SportsRecent, SportsUpcoming, SportsCore +from pathlib import Path + +class BaseMLBManager(SportsCore): + """Base class for MLB managers with common functionality.""" + # Class variables for warning tracking + _no_data_warning_logged = False + _last_warning_time = 0 + _warning_cooldown = 60 # Only log warnings once per minute + _shared_data = None + _last_shared_update = 0 + _processed_games_cache = {} # Cache for processed game data + _processed_games_timestamp = 0 + + def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): + self.logger = logging.getLogger('MLB') + super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="mlb") + + # Override configuration with sport-specific settings + self.logo_dir = Path(self.sport_config.logo_dir) + self.update_interval = self.sport_config.get_update_interval() + + # Check display modes to determine what data to fetch + display_modes = self.mode_config.get("display_modes", {}) + self.recent_enabled = display_modes.get("mlb_recent", False) + self.upcoming_enabled = display_modes.get("mlb_upcoming", False) + self.live_enabled = display_modes.get("mlb_live", False) + + # MLB-specific configuration + self.favorite_teams = self.mode_config.get("favorite_teams", []) + self.show_records = self.sport_config.show_records + self.show_ranking = self.sport_config.show_ranking + self.show_odds = self.sport_config.show_odds + + def _get_sport_specific_display_text(self, game: Dict) -> str: + """Get sport-specific display text for baseball.""" + try: + # Extract baseball-specific fields + inning = game.get('inning', '') + outs = game.get('outs', 0) + bases = game.get('bases', '') + strikes = game.get('strikes', 0) + balls = game.get('balls', 0) + + # Build display text + display_parts = [] + + if inning: + display_parts.append(f"Inning: {inning}") + + if outs is not None: + display_parts.append(f"Outs: {outs}") + + if bases: + display_parts.append(f"Bases: {bases}") + + if strikes is not None and balls is not None: + display_parts.append(f"Count: {balls}-{strikes}") + + return " | ".join(display_parts) if display_parts else "" + + except Exception as e: + self.logger.error(f"Error getting sport-specific display text: {e}") + return "" + + def _should_show_game(self, game: Dict) -> bool: + """Determine if a game should be shown based on MLB-specific criteria.""" + try: + # Check if game is live or recent + is_live = game.get('is_live', False) + is_final = game.get('is_final', False) + is_upcoming = game.get('is_upcoming', False) + + # Show live games + if is_live and self.live_enabled: + return True + + # Show recent games (within last 24 hours) + if is_final and self.recent_enabled: + # Check if game ended within last 24 hours + game_time = game.get('start_time_utc') + if game_time: + time_diff = datetime.now(pytz.UTC) - game_time + if time_diff.total_seconds() < 86400: # 24 hours + return True + + # Show upcoming games (within next 7 days) + if is_upcoming and self.upcoming_enabled: + game_time = game.get('start_time_utc') + if game_time: + time_diff = game_time - datetime.now(pytz.UTC) + if time_diff.total_seconds() < 604800: # 7 days + return True + + return False + + except Exception as e: + self.logger.error(f"Error checking if game should be shown: {e}") + return False + + +class MLBLiveManager(BaseMLBManager): + """Manager for live MLB games.""" + + def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("MLB Live Manager initialized") + + def get_duration(self) -> int: + """Get display duration for live MLB games.""" + return self.mode_config.get("duration", 10) + + def display(self) -> bool: + """Display live MLB games.""" + try: + # Fetch live games using the new architecture + live_games = self._fetch_immediate_games() + + if not live_games: + if not self._no_data_warning_logged: + self.logger.warning("No live MLB games found") + self._no_data_warning_logged = True + return False + + # Filter games based on criteria + games_to_show = [game for game in live_games if self._should_show_game(game)] + + if not games_to_show: + self.logger.debug("No MLB games meet display criteria") + return False + + # Display each game + for game in games_to_show: + self._display_single_game(game) + time.sleep(2) # Brief pause between games + + return True + + except Exception as e: + self.logger.error(f"Error displaying live MLB games: {e}") + return False + + def _display_single_game(self, game: Dict) -> None: + """Display a single MLB game.""" + try: + # Get game details + home_team = game.get('home_team_name', '') + away_team = game.get('away_team_name', '') + home_score = game.get('home_score', '0') + away_score = game.get('away_score', '0') + status = game.get('status_text', '') + + # Get sport-specific display text + sport_text = self._get_sport_specific_display_text(game) + + # Create display text + display_text = f"{away_team} {away_score} @ {home_team} {home_score}" + if status: + display_text += f" - {status}" + if sport_text: + display_text += f" ({sport_text})" + + # Display the text + self.display_manager.display_text(display_text) + + except Exception as e: + self.logger.error(f"Error displaying single MLB game: {e}") + + +class MLBRecentManager(BaseMLBManager): + """Manager for recent MLB games.""" + + def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("MLB Recent Manager initialized") + + def get_duration(self) -> int: + """Get display duration for recent MLB games.""" + return self.mode_config.get("duration", 8) + + def display(self) -> bool: + """Display recent MLB games.""" + try: + # Fetch recent games using the new architecture + recent_games = self._get_partial_schedule_data(datetime.now().year) + + if not recent_games: + if not self._no_data_warning_logged: + self.logger.warning("No recent MLB games found") + self._no_data_warning_logged = True + return False + + # Filter for recent games (last 24 hours) + now = datetime.now(pytz.UTC) + recent_games = [game for game in recent_games + if game.get('is_final', False) and + game.get('start_time_utc') and + (now - game['start_time_utc']).total_seconds() < 86400] + + if not recent_games: + self.logger.debug("No recent MLB games in last 24 hours") + return False + + # Display each game + for game in recent_games: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying recent MLB games: {e}") + return False + + +class MLBUpcomingManager(BaseMLBManager): + """Manager for upcoming MLB games.""" + + def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("MLB Upcoming Manager initialized") + + def get_duration(self) -> int: + """Get display duration for upcoming MLB games.""" + return self.mode_config.get("duration", 6) + + def display(self) -> bool: + """Display upcoming MLB games.""" + try: + # Fetch upcoming games using the new architecture + upcoming_games = self._get_partial_schedule_data(datetime.now().year) + + if not upcoming_games: + if not self._no_data_warning_logged: + self.logger.warning("No upcoming MLB games found") + self._no_data_warning_logged = True + return False + + # Filter for upcoming games (next 7 days) + now = datetime.now(pytz.UTC) + upcoming_games = [game for game in upcoming_games + if game.get('is_upcoming', False) and + game.get('start_time_utc') and + (game['start_time_utc'] - now).total_seconds() < 604800] + + if not upcoming_games: + self.logger.debug("No upcoming MLB games in next 7 days") + return False + + # Display each game + for game in upcoming_games: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying upcoming MLB games: {e}") + return False diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 155a04c9c..d72073068 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -28,6 +28,10 @@ class BaseNCAAFBManager(Football): # Renamed class def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): self.logger = logging.getLogger('NCAAFB') # Changed logger name super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaa_fb") + + # Override configuration with sport-specific settings + self.logo_dir = Path(self.sport_config.logo_dir) + self.update_interval = self.sport_config.get_update_interval() # Check display modes to determine what data to fetch display_modes = self.mode_config.get("display_modes", {}) diff --git a/src/ncaam_hockey_managers.py b/src/ncaam_hockey_managers.py index d533ffa50..99fe60b46 100644 --- a/src/ncaam_hockey_managers.py +++ b/src/ncaam_hockey_managers.py @@ -33,6 +33,10 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): self.logger = logging.getLogger('NCAAMH') # Changed logger name super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaam_hockey") + + # Override configuration with sport-specific settings + self.logo_dir = Path(self.sport_config.logo_dir) + self.update_interval = self.sport_config.get_update_interval() # Check display modes to determine what data to fetch display_modes = self.mode_config.get("display_modes", {}) diff --git a/src/nfl_managers.py b/src/nfl_managers.py index 2cecc08d3..c85e90ee6 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -25,6 +25,10 @@ class BaseNFLManager(Football): # Renamed class def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): self.logger = logging.getLogger('NFL') # Changed logger name super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="nfl") + + # Override configuration with sport-specific settings + self.logo_dir = Path(self.sport_config.logo_dir) + self.update_interval = self.sport_config.get_update_interval() # Check display modes to determine what data to fetch display_modes = self.mode_config.get("display_modes", {}) From cf559afcf7a53c0f6adddd154fbed6d6ed8745fd Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:30:25 -0400 Subject: [PATCH 06/11] tests --- test/test_baseball_architecture.py | 256 +++++++++++++++++++++++++++ test/test_new_architecture.py | 243 ++++++++++++++++++++++++++ test/test_sports_integration.py | 270 +++++++++++++++++++++++++++++ 3 files changed, 769 insertions(+) create mode 100644 test/test_baseball_architecture.py create mode 100644 test/test_new_architecture.py create mode 100644 test/test_sports_integration.py diff --git a/test/test_baseball_architecture.py b/test/test_baseball_architecture.py new file mode 100644 index 000000000..bcec37c6a --- /dev/null +++ b/test/test_baseball_architecture.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +Test Baseball Architecture + +This test validates the new baseball base class and its integration +with the new architecture components. +""" + +import sys +import os +import logging +from typing import Dict, Any + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def test_baseball_imports(): + """Test that baseball base classes can be imported.""" + print("🧪 Testing Baseball Imports...") + + try: + from src.base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming + print("✅ Baseball base classes imported successfully") + return True + except Exception as e: + print(f"❌ Baseball import failed: {e}") + return False + +def test_baseball_configuration(): + """Test baseball-specific configuration.""" + print("\n🧪 Testing Baseball Configuration...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test MLB configuration + mlb_config = get_sport_config('mlb', None) + + # Validate MLB-specific settings + assert mlb_config.update_cadence == 'daily', "MLB should have daily updates" + assert mlb_config.season_length == 162, "MLB season should be 162 games" + assert mlb_config.games_per_week == 6, "MLB should have ~6 games per week" + assert mlb_config.data_source_type == 'mlb_api', "MLB should use MLB API" + + # Test baseball-specific fields + expected_fields = ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'] + for field in expected_fields: + assert field in mlb_config.sport_specific_fields, f"Missing baseball field: {field}" + + print("✅ Baseball configuration is correct") + return True + + except Exception as e: + print(f"❌ Baseball configuration test failed: {e}") + return False + +def test_baseball_api_extractor(): + """Test baseball API extractor.""" + print("\n🧪 Testing Baseball API Extractor...") + + try: + from src.base_classes.api_extractors import get_extractor_for_sport + logger = logging.getLogger('test') + + # Get MLB extractor + mlb_extractor = get_extractor_for_sport('mlb', logger) + print(f"✅ MLB extractor: {type(mlb_extractor).__name__}") + + # Test that extractor has baseball-specific methods + assert hasattr(mlb_extractor, 'extract_game_details') + assert hasattr(mlb_extractor, 'get_sport_specific_fields') + + # Test with sample baseball data + sample_baseball_game = { + "id": "test_game", + "competitions": [{ + "status": {"type": {"state": "in", "detail": "Top 3rd"}}, + "competitors": [ + {"homeAway": "home", "team": {"abbreviation": "NYY", "displayName": "Yankees"}, "score": "2"}, + {"homeAway": "away", "team": {"abbreviation": "BOS", "displayName": "Red Sox"}, "score": "1"} + ], + "situation": { + "inning": "3rd", + "outs": 2, + "bases": "1st, 3rd", + "strikes": 2, + "balls": 1, + "pitcher": "Gerrit Cole", + "batter": "Rafael Devers" + } + }], + "date": "2024-01-01T19:00:00Z" + } + + # Test game details extraction + game_details = mlb_extractor.extract_game_details(sample_baseball_game) + if game_details: + print("✅ Baseball game details extracted successfully") + + # Test sport-specific fields + sport_fields = mlb_extractor.get_sport_specific_fields(sample_baseball_game) + expected_fields = ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'] + + for field in expected_fields: + assert field in sport_fields, f"Missing baseball field: {field}" + + print("✅ Baseball sport-specific fields extracted") + else: + print("⚠️ Baseball game details extraction returned None") + + return True + + except Exception as e: + print(f"❌ Baseball API extractor test failed: {e}") + return False + +def test_baseball_data_source(): + """Test baseball data source.""" + print("\n🧪 Testing Baseball Data Source...") + + try: + from src.base_classes.data_sources import get_data_source_for_sport + logger = logging.getLogger('test') + + # Get MLB data source + mlb_data_source = get_data_source_for_sport('mlb', 'mlb_api', logger) + print(f"✅ MLB data source: {type(mlb_data_source).__name__}") + + # Test that data source has required methods + assert hasattr(mlb_data_source, 'fetch_live_games') + assert hasattr(mlb_data_source, 'fetch_schedule') + assert hasattr(mlb_data_source, 'fetch_standings') + + print("✅ Baseball data source is properly configured") + return True + + except Exception as e: + print(f"❌ Baseball data source test failed: {e}") + return False + +def test_baseball_sport_specific_logic(): + """Test baseball-specific logic without hardware dependencies.""" + print("\n🧪 Testing Baseball Sport-Specific Logic...") + + try: + # Test baseball-specific game data + sample_baseball_game = { + 'inning': '3rd', + 'outs': 2, + 'bases': '1st, 3rd', + 'strikes': 2, + 'balls': 1, + 'pitcher': 'Gerrit Cole', + 'batter': 'Rafael Devers', + 'is_live': True, + 'is_final': False, + 'is_upcoming': False + } + + # Test that we can identify baseball-specific characteristics + assert sample_baseball_game['inning'] == '3rd' + assert sample_baseball_game['outs'] == 2 + assert sample_baseball_game['bases'] == '1st, 3rd' + assert sample_baseball_game['strikes'] == 2 + assert sample_baseball_game['balls'] == 1 + + print("✅ Baseball sport-specific logic is working") + return True + + except Exception as e: + print(f"❌ Baseball sport-specific logic test failed: {e}") + return False + +def test_baseball_vs_other_sports(): + """Test that baseball has different characteristics than other sports.""" + print("\n🧪 Testing Baseball vs Other Sports...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Compare baseball with other sports + mlb_config = get_sport_config('mlb', None) + nfl_config = get_sport_config('nfl', None) + nhl_config = get_sport_config('nhl', None) + + # Baseball should have different characteristics + assert mlb_config.season_length > nfl_config.season_length, "MLB season should be longer than NFL" + assert mlb_config.games_per_week > nfl_config.games_per_week, "MLB should have more games per week than NFL" + assert mlb_config.update_cadence == 'daily', "MLB should have daily updates" + assert nfl_config.update_cadence == 'weekly', "NFL should have weekly updates" + + # Baseball should have different sport-specific fields + mlb_fields = set(mlb_config.sport_specific_fields) + nfl_fields = set(nfl_config.sport_specific_fields) + nhl_fields = set(nhl_config.sport_specific_fields) + + # Baseball should have unique fields + assert 'inning' in mlb_fields, "Baseball should have inning field" + assert 'outs' in mlb_fields, "Baseball should have outs field" + assert 'bases' in mlb_fields, "Baseball should have bases field" + assert 'strikes' in mlb_fields, "Baseball should have strikes field" + assert 'balls' in mlb_fields, "Baseball should have balls field" + + # Baseball should not have football/hockey fields + assert 'down' not in mlb_fields, "Baseball should not have down field" + assert 'distance' not in mlb_fields, "Baseball should not have distance field" + assert 'period' not in mlb_fields, "Baseball should not have period field" + + print("✅ Baseball has distinct characteristics from other sports") + return True + + except Exception as e: + print(f"❌ Baseball vs other sports test failed: {e}") + return False + +def main(): + """Run all baseball architecture tests.""" + print("⚾ Testing Baseball Architecture") + print("=" * 50) + + # Configure logging + logging.basicConfig(level=logging.WARNING) + + # Run all tests + tests = [ + test_baseball_imports, + test_baseball_configuration, + test_baseball_api_extractor, + test_baseball_data_source, + test_baseball_sport_specific_logic, + test_baseball_vs_other_sports + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + + print("\n" + "=" * 50) + print(f"🏁 Baseball Test Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All baseball architecture tests passed! Baseball is ready to use.") + return True + else: + print("❌ Some baseball tests failed. Please check the errors above.") + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) diff --git a/test/test_new_architecture.py b/test/test_new_architecture.py new file mode 100644 index 000000000..2e5804fc3 --- /dev/null +++ b/test/test_new_architecture.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +Test New Architecture Components + +This test validates the new sports architecture including: +- API extractors +- Sport configurations +- Data sources +- Baseball base classes +""" + +import sys +import os +import logging +from typing import Dict, Any + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def test_sport_configurations(): + """Test sport-specific configurations.""" + print("🧪 Testing Sport Configurations...") + + try: + from src.base_classes.sport_configs import get_sport_configs, get_sport_config + + # Test getting all configurations + configs = get_sport_configs() + print(f"✅ Loaded {len(configs)} sport configurations") + + # Test each sport + sports_to_test = ['nfl', 'ncaa_fb', 'mlb', 'nhl', 'ncaam_hockey', 'soccer', 'nba'] + + for sport_key in sports_to_test: + config = get_sport_config(sport_key, None) + print(f"✅ {sport_key}: {config.update_cadence}, {config.season_length} games, {config.data_source_type}") + + # Validate configuration + assert config.update_cadence in ['daily', 'weekly', 'hourly', 'live_only'] + assert config.season_length > 0 + assert config.data_source_type in ['espn', 'mlb_api', 'soccer_api'] + assert len(config.sport_specific_fields) > 0 + + print("✅ All sport configurations valid") + return True + + except Exception as e: + print(f"❌ Sport configuration test failed: {e}") + return False + +def test_api_extractors(): + """Test API extractors for different sports.""" + print("\n🧪 Testing API Extractors...") + + try: + from src.base_classes.api_extractors import get_extractor_for_sport + logger = logging.getLogger('test') + + # Test each sport extractor + sports_to_test = ['nfl', 'mlb', 'nhl', 'soccer'] + + for sport_key in sports_to_test: + extractor = get_extractor_for_sport(sport_key, logger) + print(f"✅ {sport_key} extractor: {type(extractor).__name__}") + + # Test that extractor has required methods + assert hasattr(extractor, 'extract_game_details') + assert hasattr(extractor, 'get_sport_specific_fields') + assert callable(extractor.extract_game_details) + assert callable(extractor.get_sport_specific_fields) + + print("✅ All API extractors valid") + return True + + except Exception as e: + print(f"❌ API extractor test failed: {e}") + return False + +def test_data_sources(): + """Test data sources for different sports.""" + print("\n🧪 Testing Data Sources...") + + try: + from src.base_classes.data_sources import get_data_source_for_sport + logger = logging.getLogger('test') + + # Test different data source types + data_source_tests = [ + ('nfl', 'espn'), + ('mlb', 'mlb_api'), + ('soccer', 'soccer_api') + ] + + for sport_key, source_type in data_source_tests: + data_source = get_data_source_for_sport(sport_key, source_type, logger) + print(f"✅ {sport_key} data source: {type(data_source).__name__}") + + # Test that data source has required methods + assert hasattr(data_source, 'fetch_live_games') + assert hasattr(data_source, 'fetch_schedule') + assert hasattr(data_source, 'fetch_standings') + assert callable(data_source.fetch_live_games) + assert callable(data_source.fetch_schedule) + assert callable(data_source.fetch_standings) + + print("✅ All data sources valid") + return True + + except Exception as e: + print(f"❌ Data source test failed: {e}") + return False + +def test_baseball_base_class(): + """Test baseball base class without hardware dependencies.""" + print("\n🧪 Testing Baseball Base Class...") + + try: + # Test that we can import the baseball base class + from src.base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming + print("✅ Baseball base classes imported successfully") + + # Test that classes are properly defined + assert Baseball is not None + assert BaseballLive is not None + assert BaseballRecent is not None + assert BaseballUpcoming is not None + + print("✅ Baseball base classes properly defined") + return True + + except Exception as e: + print(f"❌ Baseball base class test failed: {e}") + return False + +def test_sport_specific_fields(): + """Test that each sport has appropriate sport-specific fields.""" + print("\n🧪 Testing Sport-Specific Fields...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test sport-specific fields for each sport + sport_fields_tests = { + 'nfl': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], + 'mlb': ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'], + 'nhl': ['period', 'power_play', 'penalties', 'shots_on_goal'], + 'soccer': ['half', 'stoppage_time', 'cards', 'possession'] + } + + for sport_key, expected_fields in sport_fields_tests.items(): + config = get_sport_config(sport_key, None) + actual_fields = config.sport_specific_fields + + print(f"✅ {sport_key} fields: {actual_fields}") + + # Check that we have the expected fields + for field in expected_fields: + assert field in actual_fields, f"Missing field {field} for {sport_key}" + + print("✅ All sport-specific fields valid") + return True + + except Exception as e: + print(f"❌ Sport-specific fields test failed: {e}") + return False + +def test_configuration_consistency(): + """Test that configurations are consistent and logical.""" + print("\n🧪 Testing Configuration Consistency...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test that each sport has logical configuration + sports_to_test = ['nfl', 'ncaa_fb', 'mlb', 'nhl', 'ncaam_hockey', 'soccer', 'nba'] + + for sport_key in sports_to_test: + config = get_sport_config(sport_key, None) + + # Test update cadence makes sense + if config.season_length > 100: # Long season + assert config.update_cadence in ['daily', 'hourly'], f"{sport_key} should have frequent updates for long season" + elif config.season_length < 20: # Short season + assert config.update_cadence in ['weekly', 'daily'], f"{sport_key} should have less frequent updates for short season" + + # Test that games per week makes sense + assert config.games_per_week > 0, f"{sport_key} should have at least 1 game per week" + assert config.games_per_week <= 7, f"{sport_key} should not have more than 7 games per week" + + # Test that season length is reasonable + assert config.season_length > 0, f"{sport_key} should have positive season length" + assert config.season_length < 200, f"{sport_key} season length seems too long" + + print(f"✅ {sport_key} configuration is consistent") + + print("✅ All configurations are consistent") + return True + + except Exception as e: + print(f"❌ Configuration consistency test failed: {e}") + return False + +def main(): + """Run all architecture tests.""" + print("🚀 Testing New Sports Architecture") + print("=" * 50) + + # Configure logging + logging.basicConfig(level=logging.WARNING) + + # Run all tests + tests = [ + test_sport_configurations, + test_api_extractors, + test_data_sources, + test_baseball_base_class, + test_sport_specific_fields, + test_configuration_consistency + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + + print("\n" + "=" * 50) + print(f"🏁 Test Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All architecture tests passed! The new system is ready to use.") + return True + else: + print("❌ Some tests failed. Please check the errors above.") + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) diff --git a/test/test_sports_integration.py b/test/test_sports_integration.py new file mode 100644 index 000000000..603f411f0 --- /dev/null +++ b/test/test_sports_integration.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +""" +Test Sports Integration + +This test validates that all sports work together with the new architecture +and that the system can handle multiple sports simultaneously. +""" + +import sys +import os +import logging +from typing import Dict, Any, List + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def test_all_sports_configuration(): + """Test that all sports have valid configurations.""" + print("🧪 Testing All Sports Configuration...") + + try: + from src.base_classes.sport_configs import get_sport_configs, get_sport_config + + # Get all sport configurations + configs = get_sport_configs() + all_sports = list(configs.keys()) + + print(f"✅ Found {len(all_sports)} sports: {all_sports}") + + # Test each sport + for sport_key in all_sports: + config = get_sport_config(sport_key, None) + + # Validate basic configuration + assert config.update_cadence in ['daily', 'weekly', 'hourly', 'live_only'] + assert config.season_length > 0 + assert config.games_per_week > 0 + assert config.data_source_type in ['espn', 'mlb_api', 'soccer_api'] + assert len(config.sport_specific_fields) > 0 + + print(f"✅ {sport_key}: {config.update_cadence}, {config.season_length} games, {config.data_source_type}") + + print("✅ All sports have valid configurations") + return True + + except Exception as e: + print(f"❌ All sports configuration test failed: {e}") + return False + +def test_sports_api_extractors(): + """Test that all sports have working API extractors.""" + print("\n🧪 Testing All Sports API Extractors...") + + try: + from src.base_classes.api_extractors import get_extractor_for_sport + logger = logging.getLogger('test') + + # Test all sports + sports_to_test = ['nfl', 'ncaa_fb', 'mlb', 'nhl', 'ncaam_hockey', 'soccer', 'nba'] + + for sport_key in sports_to_test: + extractor = get_extractor_for_sport(sport_key, logger) + print(f"✅ {sport_key} extractor: {type(extractor).__name__}") + + # Test that extractor has required methods + assert hasattr(extractor, 'extract_game_details') + assert hasattr(extractor, 'get_sport_specific_fields') + assert callable(extractor.extract_game_details) + assert callable(extractor.get_sport_specific_fields) + + print("✅ All sports have working API extractors") + return True + + except Exception as e: + print(f"❌ Sports API extractors test failed: {e}") + return False + +def test_sports_data_sources(): + """Test that all sports have working data sources.""" + print("\n🧪 Testing All Sports Data Sources...") + + try: + from src.base_classes.data_sources import get_data_source_for_sport + from src.base_classes.sport_configs import get_sport_config + logger = logging.getLogger('test') + + # Test all sports + sports_to_test = ['nfl', 'ncaa_fb', 'mlb', 'nhl', 'ncaam_hockey', 'soccer', 'nba'] + + for sport_key in sports_to_test: + # Get sport configuration to determine data source type + config = get_sport_config(sport_key, None) + data_source_type = config.data_source_type + + # Get data source + data_source = get_data_source_for_sport(sport_key, data_source_type, logger) + print(f"✅ {sport_key} data source: {type(data_source).__name__} ({data_source_type})") + + # Test that data source has required methods + assert hasattr(data_source, 'fetch_live_games') + assert hasattr(data_source, 'fetch_schedule') + assert hasattr(data_source, 'fetch_standings') + assert callable(data_source.fetch_live_games) + assert callable(data_source.fetch_schedule) + assert callable(data_source.fetch_standings) + + print("✅ All sports have working data sources") + return True + + except Exception as e: + print(f"❌ Sports data sources test failed: {e}") + return False + +def test_sports_consistency(): + """Test that sports configurations are consistent and logical.""" + print("\n🧪 Testing Sports Consistency...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test that each sport has logical configuration + sports_to_test = ['nfl', 'ncaa_fb', 'mlb', 'nhl', 'ncaam_hockey', 'soccer', 'nba'] + + for sport_key in sports_to_test: + config = get_sport_config(sport_key, None) + + # Test update cadence makes sense for season length + if config.season_length > 100: # Long season (MLB, NBA, NHL) + assert config.update_cadence in ['daily', 'hourly'], f"{sport_key} should have frequent updates for long season" + elif config.season_length < 20: # Short season (NFL, NCAA) + assert config.update_cadence in ['weekly', 'daily'], f"{sport_key} should have less frequent updates for short season" + + # Test that games per week makes sense + assert config.games_per_week > 0, f"{sport_key} should have at least 1 game per week" + assert config.games_per_week <= 7, f"{sport_key} should not have more than 7 games per week" + + # Test that season length is reasonable + assert config.season_length > 0, f"{sport_key} should have positive season length" + assert config.season_length < 200, f"{sport_key} season length seems too long" + + print(f"✅ {sport_key} configuration is consistent") + + print("✅ All sports configurations are consistent") + return True + + except Exception as e: + print(f"❌ Sports consistency test failed: {e}") + return False + +def test_sports_uniqueness(): + """Test that each sport has unique characteristics.""" + print("\n🧪 Testing Sports Uniqueness...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test that each sport has unique sport-specific fields + sports_to_test = ['nfl', 'mlb', 'nhl', 'soccer'] + + sport_fields = {} + for sport_key in sports_to_test: + config = get_sport_config(sport_key, None) + sport_fields[sport_key] = set(config.sport_specific_fields) + + # Test that each sport has unique fields + for sport_key in sports_to_test: + current_fields = sport_fields[sport_key] + + # Check that sport has unique fields + if sport_key == 'nfl': + assert 'down' in current_fields, "NFL should have down field" + assert 'distance' in current_fields, "NFL should have distance field" + assert 'possession' in current_fields, "NFL should have possession field" + elif sport_key == 'mlb': + assert 'inning' in current_fields, "MLB should have inning field" + assert 'outs' in current_fields, "MLB should have outs field" + assert 'bases' in current_fields, "MLB should have bases field" + assert 'strikes' in current_fields, "MLB should have strikes field" + assert 'balls' in current_fields, "MLB should have balls field" + elif sport_key == 'nhl': + assert 'period' in current_fields, "NHL should have period field" + assert 'power_play' in current_fields, "NHL should have power_play field" + assert 'penalties' in current_fields, "NHL should have penalties field" + elif sport_key == 'soccer': + assert 'half' in current_fields, "Soccer should have half field" + assert 'stoppage_time' in current_fields, "Soccer should have stoppage_time field" + assert 'cards' in current_fields, "Soccer should have cards field" + assert 'possession' in current_fields, "Soccer should have possession field" + + print(f"✅ {sport_key} has unique sport-specific fields") + + print("✅ All sports have unique characteristics") + return True + + except Exception as e: + print(f"❌ Sports uniqueness test failed: {e}") + return False + +def test_sports_data_source_mapping(): + """Test that sports are mapped to appropriate data sources.""" + print("\n🧪 Testing Sports Data Source Mapping...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test that each sport uses an appropriate data source + sports_to_test = ['nfl', 'ncaa_fb', 'mlb', 'nhl', 'ncaam_hockey', 'soccer', 'nba'] + + for sport_key in sports_to_test: + config = get_sport_config(sport_key, None) + data_source_type = config.data_source_type + + # Test that data source type makes sense for the sport + if sport_key == 'mlb': + assert data_source_type == 'mlb_api', "MLB should use MLB API" + elif sport_key == 'soccer': + assert data_source_type == 'soccer_api', "Soccer should use Soccer API" + else: + assert data_source_type == 'espn', f"{sport_key} should use ESPN API" + + print(f"✅ {sport_key} uses appropriate data source: {data_source_type}") + + print("✅ All sports use appropriate data sources") + return True + + except Exception as e: + print(f"❌ Sports data source mapping test failed: {e}") + return False + +def main(): + """Run all sports integration tests.""" + print("🏈 Testing Sports Integration") + print("=" * 50) + + # Configure logging + logging.basicConfig(level=logging.WARNING) + + # Run all tests + tests = [ + test_all_sports_configuration, + test_sports_api_extractors, + test_sports_data_sources, + test_sports_consistency, + test_sports_uniqueness, + test_sports_data_source_mapping + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + + print("\n" + "=" * 50) + print(f"🏁 Sports Integration Test Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All sports integration tests passed! The system can handle multiple sports.") + return True + else: + print("❌ Some sports integration tests failed. Please check the errors above.") + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) From bdba9f0c808f6782c8f7342ff02988f369b6a3b8 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:22:50 -0400 Subject: [PATCH 07/11] fix missing duration --- src/leaderboard_manager.py | 7 ++ test/test_leaderboard_duration_fix.py | 169 ++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 test/test_leaderboard_duration_fix.py diff --git a/src/leaderboard_manager.py b/src/leaderboard_manager.py index a469a48ba..f2ab9b83d 100644 --- a/src/leaderboard_manager.py +++ b/src/leaderboard_manager.py @@ -1240,6 +1240,13 @@ def get_dynamic_duration(self) -> int: logger.debug(f"get_dynamic_duration called, returning: {self.dynamic_duration}s") return self.dynamic_duration + def get_duration(self) -> int: + """Get the display duration for the leaderboard.""" + if self.dynamic_duration_enabled: + return self.get_dynamic_duration() + else: + return self.display_duration + def update(self) -> None: """Update leaderboard data.""" current_time = time.time() diff --git a/test/test_leaderboard_duration_fix.py b/test/test_leaderboard_duration_fix.py new file mode 100644 index 000000000..ad788734f --- /dev/null +++ b/test/test_leaderboard_duration_fix.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Test Leaderboard Duration Fix + +This test validates that the LeaderboardManager has the required get_duration method +that the display controller expects. +""" + +import sys +import os +import logging + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def test_leaderboard_duration_method(): + """Test that LeaderboardManager has the get_duration method.""" + print("🧪 Testing Leaderboard Duration Method...") + + try: + # Read the leaderboard manager file + with open('src/leaderboard_manager.py', 'r') as f: + content = f.read() + + # Check that get_duration method exists + if 'def get_duration(self) -> int:' in content: + print("✅ get_duration method found in LeaderboardManager") + else: + print("❌ get_duration method not found in LeaderboardManager") + return False + + # Check that method is properly implemented + if 'return self.get_dynamic_duration()' in content: + print("✅ get_duration method uses dynamic duration when enabled") + else: + print("❌ get_duration method not properly implemented for dynamic duration") + return False + + if 'return self.display_duration' in content: + print("✅ get_duration method falls back to display_duration") + else: + print("❌ get_duration method not properly implemented for fallback") + return False + + # Check that method is in the right place (after get_dynamic_duration) + lines = content.split('\n') + get_dynamic_duration_line = None + get_duration_line = None + + for i, line in enumerate(lines): + if 'def get_dynamic_duration(self) -> int:' in line: + get_dynamic_duration_line = i + elif 'def get_duration(self) -> int:' in line: + get_duration_line = i + + if get_dynamic_duration_line is not None and get_duration_line is not None: + if get_duration_line > get_dynamic_duration_line: + print("✅ get_duration method is placed after get_dynamic_duration") + else: + print("❌ get_duration method is not in the right place") + return False + + print("✅ LeaderboardManager duration method is properly implemented") + return True + + except Exception as e: + print(f"❌ Leaderboard duration method test failed: {e}") + return False + +def test_leaderboard_duration_logic(): + """Test that the duration logic makes sense.""" + print("\n🧪 Testing Leaderboard Duration Logic...") + + try: + # Read the leaderboard manager file + with open('src/leaderboard_manager.py', 'r') as f: + content = f.read() + + # Check that the logic is correct + if 'if self.dynamic_duration_enabled:' in content: + print("✅ Dynamic duration logic is implemented") + else: + print("❌ Dynamic duration logic not found") + return False + + if 'return self.get_dynamic_duration()' in content: + print("✅ Returns dynamic duration when enabled") + else: + print("❌ Does not return dynamic duration when enabled") + return False + + if 'return self.display_duration' in content: + print("✅ Returns display duration as fallback") + else: + print("❌ Does not return display duration as fallback") + return False + + print("✅ Leaderboard duration logic is correct") + return True + + except Exception as e: + print(f"❌ Leaderboard duration logic test failed: {e}") + return False + +def test_leaderboard_method_signature(): + """Test that the method signature is correct.""" + print("\n🧪 Testing Leaderboard Method Signature...") + + try: + # Read the leaderboard manager file + with open('src/leaderboard_manager.py', 'r') as f: + content = f.read() + + # Check method signature + if 'def get_duration(self) -> int:' in content: + print("✅ Method signature is correct") + else: + print("❌ Method signature is incorrect") + return False + + # Check docstring + if '"""Get the display duration for the leaderboard."""' in content: + print("✅ Method has proper docstring") + else: + print("❌ Method missing docstring") + return False + + print("✅ Leaderboard method signature is correct") + return True + + except Exception as e: + print(f"❌ Leaderboard method signature test failed: {e}") + return False + +def main(): + """Run all leaderboard duration tests.""" + print("🏆 Testing Leaderboard Duration Fix") + print("=" * 50) + + # Run all tests + tests = [ + test_leaderboard_duration_method, + test_leaderboard_duration_logic, + test_leaderboard_method_signature + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + + print("\n" + "=" * 50) + print(f"🏁 Leaderboard Duration Test Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All leaderboard duration tests passed! The fix is working correctly.") + return True + else: + print("❌ Some leaderboard duration tests failed. Please check the errors above.") + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) From e0c0a5def59e7e0209bb964e5dc228f61c3642aa Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:57:29 -0400 Subject: [PATCH 08/11] ensure milb, mlb, ncaa bb are all using new baseball base class properly --- src/milb_managers_v2.py | 378 +++++++++++++++++++++ src/ncaa_baseball_managers_v2.py | 363 ++++++++++++++++++++ src/ncaa_fb_managers.py | 16 +- test/test_baseball_managers_integration.py | 236 +++++++++++++ test/test_baseball_managers_simple.py | 243 +++++++++++++ 5 files changed, 1228 insertions(+), 8 deletions(-) create mode 100644 src/milb_managers_v2.py create mode 100644 src/ncaa_baseball_managers_v2.py create mode 100644 test/test_baseball_managers_integration.py create mode 100644 test/test_baseball_managers_simple.py diff --git a/src/milb_managers_v2.py b/src/milb_managers_v2.py new file mode 100644 index 000000000..25985baca --- /dev/null +++ b/src/milb_managers_v2.py @@ -0,0 +1,378 @@ +""" +MiLB (Minor League Baseball) Managers - Updated to use new baseball base class + +This module demonstrates how to update existing baseball managers to use the new +baseball base class architecture while maintaining all existing functionality. +""" + +import time +import logging +import requests +import json +from typing import Dict, Any, List, Optional +from datetime import datetime, timedelta, timezone +import os +from PIL import Image, ImageDraw, ImageFont +import numpy as np +from .cache_manager import CacheManager +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +import pytz +from src.background_data_service import get_background_service + +# Import new baseball base classes +from .base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming + +# Import API counter function +try: + from web_interface_v2 import increment_api_counter +except ImportError: + def increment_api_counter(kind: str, count: int = 1): + pass + +# Get logger +logger = logging.getLogger(__name__) + +class BaseMiLBManager(Baseball): + """Base class for MiLB managers using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + # Initialize with sport_key for MiLB + super().__init__(config, display_manager, cache_manager, logger, "milb") + + # MiLB-specific configuration + self.milb_config = config.get('milb', {}) + self.favorite_teams = self.milb_config.get('favorite_teams', []) + self.show_records = self.milb_config.get('show_records', False) + + # Load MiLB team mapping + self.team_mapping = {} + self.team_name_to_abbr = {} + team_mapping_path = os.path.join('assets', 'sports', 'milb_logos', 'milb_team_mapping.json') + try: + with open(team_mapping_path, 'r') as f: + self.team_mapping = json.load(f) + self.team_name_to_abbr = {name: data['abbreviation'] for name, data in self.team_mapping.items()} + self.logger.info(f"Loaded {len(self.team_name_to_abbr)} MiLB team mappings.") + except Exception as e: + self.logger.error(f"Failed to load MiLB team mapping: {e}") + + # Set up session with retry logic + self.session = requests.Session() + retry_strategy = Retry( + total=5, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Load fonts + self.fonts = self._load_fonts() + + # Initialize game tracking + self.live_games = [] + self.current_game = None + self.current_game_index = 0 + self.last_update = 0 + self.update_interval = self.milb_config.get('live_update_interval', 20) + self.no_data_interval = max(300, self.update_interval) + self.last_game_switch = 0 + self.game_display_duration = self.milb_config.get('live_game_duration', 30) + self.last_display_update = 0 + self.last_log_time = 0 + self.log_interval = 300 + self.last_count_log_time = 0 + self.count_log_interval = 5 + self.test_mode = self.milb_config.get('test_mode', False) + + def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: + """Load fonts for display.""" + fonts = {} + try: + # Load main font + font_path = os.path.join('assets', 'fonts', '5by7.regular.ttf') + if os.path.exists(font_path): + fonts['main'] = ImageFont.truetype(font_path, 8) + else: + fonts['main'] = ImageFont.load_default() + + # Load small font + fonts['small'] = ImageFont.load_default() + + return fonts + except Exception as e: + self.logger.error(f"Error loading fonts: {e}") + return {'main': ImageFont.load_default(), 'small': ImageFont.load_default()} + + def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: + """Get team logo for display.""" + try: + logo_path = os.path.join('assets', 'sports', 'milb_logos', f"{team_abbr}.png") + if os.path.exists(logo_path): + return Image.open(logo_path) + return None + except Exception as e: + self.logger.error(f"Error loading logo for {team_abbr}: {e}") + return None + + def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): + """Draw text with outline for better visibility.""" + x, y = position + for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + draw.text((x, y), text, font=font, fill=fill) + + def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None: + """Draw base indicators for baseball.""" + base_size = 3 + base_spacing = 8 + + # Draw bases in diamond formation + base_positions = [ + (center_x, y - base_spacing), # 1st base + (center_x + base_spacing, y), # 2nd base + (center_x, y + base_spacing), # 3rd base + (center_x - base_spacing, y) # Home plate + ] + + for i, (pos, occupied) in enumerate(zip(base_positions, bases_occupied)): + color = (255, 255, 0) if occupied else (128, 128, 128) + draw.ellipse([pos[0] - base_size, pos[1] - base_size, + pos[0] + base_size, pos[1] + base_size], fill=color) + + def _create_game_display(self, game_data: Dict[str, Any]) -> Image.Image: + """Create display image for a game.""" + try: + # Create image + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + image = Image.new('RGB', (width, height), (0, 0, 0)) + draw = ImageDraw.Draw(image) + + # Get game details + home_team = game_data.get('home_team', {}) + away_team = game_data.get('away_team', {}) + home_score = game_data.get('home_score', '0') + away_score = game_data.get('away_score', '0') + + # Get baseball-specific details + inning = game_data.get('inning', '') + outs = game_data.get('outs', 0) + bases = game_data.get('bases', '') + strikes = game_data.get('strikes', 0) + balls = game_data.get('balls', 0) + + # Draw team names and scores + font = self.fonts['main'] + y_offset = 10 + + # Away team + away_text = f"{away_team.get('abbreviation', 'AWAY')} {away_score}" + draw.text((5, y_offset), away_text, font=font, fill=(255, 255, 255)) + + # Home team + home_text = f"{home_team.get('abbreviation', 'HOME')} {home_score}" + draw.text((5, y_offset + 15), home_text, font=font, fill=(255, 255, 255)) + + # Baseball-specific details + if inning: + inning_text = f"Inning: {inning}" + draw.text((5, y_offset + 30), inning_text, font=font, fill=(255, 255, 255)) + + if outs is not None: + outs_text = f"Outs: {outs}" + draw.text((5, y_offset + 45), outs_text, font=font, fill=(255, 255, 255)) + + if strikes is not None and balls is not None: + count_text = f"Count: {balls}-{strikes}" + draw.text((5, y_offset + 60), count_text, font=font, fill=(255, 255, 255)) + + return image + + except Exception as e: + self.logger.error(f"Error creating game display: {e}") + return Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + + def _fetch_milb_api_data(self, use_cache: bool = True) -> Dict[str, Any]: + """Fetch MiLB data from API.""" + try: + # This would implement the actual MiLB API fetching + # For now, return empty data + return {} + except Exception as e: + self.logger.error(f"Error fetching MiLB data: {e}") + return {} + + def _extract_game_details(self, game) -> Dict: + """Extract game details from API response.""" + try: + # This would implement the actual game details extraction + # For now, return empty data + return {} + except Exception as e: + self.logger.error(f"Error extracting game details: {e}") + return {} + + def _is_baseball_game_live(self, game: Dict) -> bool: + """Check if a baseball game is currently live.""" + return super()._is_baseball_game_live(game) + + def _get_baseball_game_status(self, game: Dict) -> str: + """Get baseball-specific game status.""" + return super()._get_baseball_game_status(game) + + +class MiLBLiveManager(BaseMiLBManager, BaseballLive): + """Manager for live MiLB games using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("MiLB Live Manager initialized with new baseball architecture") + + def get_duration(self) -> int: + """Get display duration for live MiLB games.""" + return self.milb_config.get('live_game_duration', 30) + + def display(self, force_clear: bool = False) -> bool: + """Display live MiLB games.""" + try: + # Fetch live games using the new architecture + live_games = self._fetch_immediate_games() + + if not live_games: + self.logger.warning("No live MiLB games found") + return False + + # Filter games based on criteria + games_to_show = [game for game in live_games if self._should_show_baseball_game(game)] + + if not games_to_show: + self.logger.debug("No MiLB games meet display criteria") + return False + + # Display each game + for game in games_to_show: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying live MiLB games: {e}") + return False + + def _display_single_game(self, game: Dict) -> None: + """Display a single MiLB game.""" + try: + # Get game details + home_team = game.get('home_team_name', '') + away_team = game.get('away_team_name', '') + home_score = game.get('home_score', '0') + away_score = game.get('away_score', '0') + status = game.get('status_text', '') + + # Get baseball-specific display text + baseball_text = self._get_baseball_display_text(game) + + # Create display text + display_text = f"{away_team} {away_score} @ {home_team} {home_score}" + if status: + display_text += f" - {status}" + if baseball_text: + display_text += f" ({baseball_text})" + + # Display the text + self.display_manager.display_text(display_text) + + except Exception as e: + self.logger.error(f"Error displaying single MiLB game: {e}") + + +class MiLBRecentManager(BaseMiLBManager, BaseballRecent): + """Manager for recent MiLB games using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("MiLB Recent Manager initialized with new baseball architecture") + + def get_duration(self) -> int: + """Get display duration for recent MiLB games.""" + return self.milb_config.get('recent_game_duration', 20) + + def display(self, force_clear: bool = False) -> bool: + """Display recent MiLB games.""" + try: + # Fetch recent games using the new architecture + recent_games = self._get_partial_schedule_data(datetime.now().year) + + if not recent_games: + self.logger.warning("No recent MiLB games found") + return False + + # Filter for recent games (last 24 hours) + now = datetime.now(pytz.UTC) + recent_games = [game for game in recent_games + if game.get('is_final', False) and + game.get('start_time_utc') and + (now - game['start_time_utc']).total_seconds() < 86400] + + if not recent_games: + self.logger.debug("No recent MiLB games in last 24 hours") + return False + + # Display each game + for game in recent_games: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying recent MiLB games: {e}") + return False + + +class MiLBUpcomingManager(BaseMiLBManager, BaseballUpcoming): + """Manager for upcoming MiLB games using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("MiLB Upcoming Manager initialized with new baseball architecture") + + def get_duration(self) -> int: + """Get display duration for upcoming MiLB games.""" + return self.milb_config.get('upcoming_game_duration', 15) + + def display(self, force_clear: bool = False) -> bool: + """Display upcoming MiLB games.""" + try: + # Fetch upcoming games using the new architecture + upcoming_games = self._get_partial_schedule_data(datetime.now().year) + + if not upcoming_games: + self.logger.warning("No upcoming MiLB games found") + return False + + # Filter for upcoming games (next 7 days) + now = datetime.now(pytz.UTC) + upcoming_games = [game for game in upcoming_games + if game.get('is_upcoming', False) and + game.get('start_time_utc') and + (game['start_time_utc'] - now).total_seconds() < 604800] + + if not upcoming_games: + self.logger.debug("No upcoming MiLB games in next 7 days") + return False + + # Display each game + for game in upcoming_games: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying upcoming MiLB games: {e}") + return False diff --git a/src/ncaa_baseball_managers_v2.py b/src/ncaa_baseball_managers_v2.py new file mode 100644 index 000000000..cf03418f5 --- /dev/null +++ b/src/ncaa_baseball_managers_v2.py @@ -0,0 +1,363 @@ +""" +NCAA Baseball Managers - Updated to use new baseball base class + +This module demonstrates how to update existing baseball managers to use the new +baseball base class architecture while maintaining all existing functionality. +""" + +import time +import logging +import requests +import json +from typing import Dict, Any, List, Optional +from datetime import datetime, timedelta, timezone +import os +from PIL import Image, ImageDraw, ImageFont +import numpy as np +from .cache_manager import CacheManager +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +from src.odds_manager import OddsManager +import pytz + +# Import new baseball base classes +from .base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming + +# Get logger +logger = logging.getLogger(__name__) + +# Constants for NCAA Baseball API URL +ESPN_NCAABB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard" + +class BaseNCAABaseballManager(Baseball): + """Base class for NCAA Baseball managers using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + # Initialize with sport_key for NCAA Baseball + super().__init__(config, display_manager, cache_manager, logger, "ncaa_baseball") + + # NCAA Baseball-specific configuration + self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {}) + self.show_odds = self.ncaa_baseball_config.get('show_odds', False) + self.show_records = self.ncaa_baseball_config.get('show_records', False) + self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) + + # Logo handling + self.logo_dir = self.ncaa_baseball_config.get('logo_dir', os.path.join('assets', 'sports', 'ncaa_logos')) + if not os.path.exists(self.logo_dir): + self.logger.warning(f"NCAA Baseball logos directory not found: {self.logo_dir}") + try: + os.makedirs(self.logo_dir, exist_ok=True) + self.logger.info(f"Created NCAA Baseball logos directory: {self.logo_dir}") + except Exception as e: + self.logger.error(f"Failed to create NCAA Baseball logos directory: {e}") + + # Set up session with retry logic + self.session = requests.Session() + retry_strategy = Retry( + total=5, + backoff_factor=1, + status_forcelist=[429, 500, 502, 503, 504], + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Load fonts + self.fonts = self._load_fonts() + + # Initialize game tracking + self.live_games = [] + self.current_game = None + self.current_game_index = 0 + self.last_update = 0 + self.update_interval = self.ncaa_baseball_config.get('live_update_interval', 20) + self.no_data_interval = 300 + self.last_game_switch = 0 + self.game_display_duration = self.ncaa_baseball_config.get('live_game_duration', 30) + self.last_display_update = 0 + self.last_log_time = 0 + self.log_interval = 300 + self.last_count_log_time = 0 + self.count_log_interval = 5 + self.test_mode = self.ncaa_baseball_config.get('test_mode', False) + + def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: + """Load fonts for display.""" + fonts = {} + try: + # Load main font + font_path = os.path.join('assets', 'fonts', '5by7.regular.ttf') + if os.path.exists(font_path): + fonts['main'] = ImageFont.truetype(font_path, 8) + else: + fonts['main'] = ImageFont.load_default() + + # Load small font + fonts['small'] = ImageFont.load_default() + + return fonts + except Exception as e: + self.logger.error(f"Error loading fonts: {e}") + return {'main': ImageFont.load_default(), 'small': ImageFont.load_default()} + + def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: + """Get team logo for display.""" + try: + logo_path = os.path.join(self.logo_dir, f"{team_abbr}.png") + if os.path.exists(logo_path): + return Image.open(logo_path) + return None + except Exception as e: + self.logger.error(f"Error loading logo for {team_abbr}: {e}") + return None + + def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): + """Draw text with outline for better visibility.""" + x, y = position + for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + draw.text((x, y), text, font=font, fill=fill) + + def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None: + """Draw base indicators for baseball.""" + base_size = 3 + base_spacing = 8 + + # Draw bases in diamond formation + base_positions = [ + (center_x, y - base_spacing), # 1st base + (center_x + base_spacing, y), # 2nd base + (center_x, y + base_spacing), # 3rd base + (center_x - base_spacing, y) # Home plate + ] + + for i, (pos, occupied) in enumerate(zip(base_positions, bases_occupied)): + color = (255, 255, 0) if occupied else (128, 128, 128) + draw.ellipse([pos[0] - base_size, pos[1] - base_size, + pos[0] + base_size, pos[1] + base_size], fill=color) + + def _create_game_display(self, game_data: Dict[str, Any]) -> Image.Image: + """Create display image for a game.""" + try: + # Create image + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + image = Image.new('RGB', (width, height), (0, 0, 0)) + draw = ImageDraw.Draw(image) + + # Get game details + home_team = game_data.get('home_team', {}) + away_team = game_data.get('away_team', {}) + home_score = game_data.get('home_score', '0') + away_score = game_data.get('away_score', '0') + + # Get baseball-specific details + inning = game_data.get('inning', '') + outs = game_data.get('outs', 0) + bases = game_data.get('bases', '') + strikes = game_data.get('strikes', 0) + balls = game_data.get('balls', 0) + + # Draw team names and scores + font = self.fonts['main'] + y_offset = 10 + + # Away team + away_text = f"{away_team.get('abbreviation', 'AWAY')} {away_score}" + draw.text((5, y_offset), away_text, font=font, fill=(255, 255, 255)) + + # Home team + home_text = f"{home_team.get('abbreviation', 'HOME')} {home_score}" + draw.text((5, y_offset + 15), home_text, font=font, fill=(255, 255, 255)) + + # Baseball-specific details + if inning: + inning_text = f"Inning: {inning}" + draw.text((5, y_offset + 30), inning_text, font=font, fill=(255, 255, 255)) + + if outs is not None: + outs_text = f"Outs: {outs}" + draw.text((5, y_offset + 45), outs_text, font=font, fill=(255, 255, 255)) + + if strikes is not None and balls is not None: + count_text = f"Count: {balls}-{strikes}" + draw.text((5, y_offset + 60), count_text, font=font, fill=(255, 255, 255)) + + return image + + except Exception as e: + self.logger.error(f"Error creating game display: {e}") + return Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) + + def _fetch_ncaa_baseball_api_data(self, use_cache: bool = True) -> Dict[str, Any]: + """Fetch NCAA Baseball data from ESPN API.""" + try: + # This would implement the actual NCAA Baseball API fetching + # For now, return empty data + return {} + except Exception as e: + self.logger.error(f"Error fetching NCAA Baseball data: {e}") + return {} + + def _is_baseball_game_live(self, game: Dict) -> bool: + """Check if a baseball game is currently live.""" + return super()._is_baseball_game_live(game) + + def _get_baseball_game_status(self, game: Dict) -> str: + """Get baseball-specific game status.""" + return super()._get_baseball_game_status(game) + + +class NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive): + """Manager for live NCAA Baseball games using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("NCAA Baseball Live Manager initialized with new baseball architecture") + + def get_duration(self) -> int: + """Get display duration for live NCAA Baseball games.""" + return self.ncaa_baseball_config.get('live_game_duration', 30) + + def display(self, force_clear: bool = False) -> bool: + """Display live NCAA Baseball games.""" + try: + # Fetch live games using the new architecture + live_games = self._fetch_immediate_games() + + if not live_games: + self.logger.warning("No live NCAA Baseball games found") + return False + + # Filter games based on criteria + games_to_show = [game for game in live_games if self._should_show_baseball_game(game)] + + if not games_to_show: + self.logger.debug("No NCAA Baseball games meet display criteria") + return False + + # Display each game + for game in games_to_show: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying live NCAA Baseball games: {e}") + return False + + def _display_single_game(self, game: Dict) -> None: + """Display a single NCAA Baseball game.""" + try: + # Get game details + home_team = game.get('home_team_name', '') + away_team = game.get('away_team_name', '') + home_score = game.get('home_score', '0') + away_score = game.get('away_score', '0') + status = game.get('status_text', '') + + # Get baseball-specific display text + baseball_text = self._get_baseball_display_text(game) + + # Create display text + display_text = f"{away_team} {away_score} @ {home_team} {home_score}" + if status: + display_text += f" - {status}" + if baseball_text: + display_text += f" ({baseball_text})" + + # Display the text + self.display_manager.display_text(display_text) + + except Exception as e: + self.logger.error(f"Error displaying single NCAA Baseball game: {e}") + + +class NCAABaseballRecentManager(BaseNCAABaseballManager, BaseballRecent): + """Manager for recent NCAA Baseball games using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("NCAA Baseball Recent Manager initialized with new baseball architecture") + + def get_duration(self) -> int: + """Get display duration for recent NCAA Baseball games.""" + return self.ncaa_baseball_config.get('recent_game_duration', 20) + + def display(self, force_clear: bool = False) -> bool: + """Display recent NCAA Baseball games.""" + try: + # Fetch recent games using the new architecture + recent_games = self._get_partial_schedule_data(datetime.now().year) + + if not recent_games: + self.logger.warning("No recent NCAA Baseball games found") + return False + + # Filter for recent games (last 24 hours) + now = datetime.now(pytz.UTC) + recent_games = [game for game in recent_games + if game.get('is_final', False) and + game.get('start_time_utc') and + (now - game['start_time_utc']).total_seconds() < 86400] + + if not recent_games: + self.logger.debug("No recent NCAA Baseball games in last 24 hours") + return False + + # Display each game + for game in recent_games: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying recent NCAA Baseball games: {e}") + return False + + +class NCAABaseballUpcomingManager(BaseNCAABaseballManager, BaseballUpcoming): + """Manager for upcoming NCAA Baseball games using new baseball architecture.""" + + def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): + super().__init__(config, display_manager, cache_manager) + self.logger.info("NCAA Baseball Upcoming Manager initialized with new baseball architecture") + + def get_duration(self) -> int: + """Get display duration for upcoming NCAA Baseball games.""" + return self.ncaa_baseball_config.get('upcoming_game_duration', 15) + + def display(self, force_clear: bool = False) -> bool: + """Display upcoming NCAA Baseball games.""" + try: + # Fetch upcoming games using the new architecture + upcoming_games = self._get_partial_schedule_data(datetime.now().year) + + if not upcoming_games: + self.logger.warning("No upcoming NCAA Baseball games found") + return False + + # Filter for upcoming games (next 7 days) + now = datetime.now(pytz.UTC) + upcoming_games = [game for game in upcoming_games + if game.get('is_upcoming', False) and + game.get('start_time_utc') and + (game['start_time_utc'] - now).total_seconds() < 604800] + + if not upcoming_games: + self.logger.debug("No upcoming NCAA Baseball games in next 7 days") + return False + + # Display each game + for game in upcoming_games: + self._display_single_game(game) + time.sleep(2) + + return True + + except Exception as e: + self.logger.error(f"Error displaying upcoming NCAA Baseball games: {e}") + return False diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index d72073068..c9ce3d46d 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -100,18 +100,18 @@ def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]: datestring = f"{season_year}0801-{season_year+1}0201" cache_key = f"ncaafb_schedule_{season_year}" - if use_cache: + if use_cache: cached_data = self.cache_manager.get(cache_key) - if cached_data: + if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and 'events' in cached_data: self.logger.info(f"Using cached schedule for {season_year}") - return cached_data + return cached_data elif isinstance(cached_data, list): # Handle old cache format (list of events) self.logger.info(f"Using cached schedule for {season_year} (legacy format)") return {'events': cached_data} - else: + else: self.logger.warning(f"Invalid cached data format for {season_year}: {type(cached_data)}") # Clear invalid cache self.cache_manager.clear_cache(cache_key) @@ -163,7 +163,7 @@ def fetch_callback(result): partial_data = self._get_weeks_data("college-football") if partial_data: return partial_data - return None + return None def _fetch_ncaa_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]: """ @@ -187,13 +187,13 @@ def _fetch_ncaa_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]: return {'events': events} except requests.exceptions.RequestException as e: self.logger.error(f"[API error fetching full schedule: {e}") - return None - + return None + def _fetch_data(self) -> Optional[Dict]: """Fetch data using shared data mechanism or direct fetch for live.""" if isinstance(self, NCAAFBLiveManager): return self._fetch_todays_games("college-football") - else: + else: return self._fetch_ncaa_fb_api_data(use_cache=True) diff --git a/test/test_baseball_managers_integration.py b/test/test_baseball_managers_integration.py new file mode 100644 index 000000000..e09819a1e --- /dev/null +++ b/test/test_baseball_managers_integration.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +Test Baseball Managers Integration + +This test validates that MILB and NCAA Baseball managers work with the new +baseball base class architecture. +""" + +import sys +import os +import logging +from typing import Dict, Any + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def test_milb_manager_imports(): + """Test that MILB managers can be imported.""" + print("🧪 Testing MILB Manager Imports...") + + try: + # Test that we can import the new MILB managers + from src.milb_managers_v2 import BaseMiLBManager, MiLBLiveManager, MiLBRecentManager, MiLBUpcomingManager + print("✅ MILB managers imported successfully") + + # Test that classes are properly defined + assert BaseMiLBManager is not None + assert MiLBLiveManager is not None + assert MiLBRecentManager is not None + assert MiLBUpcomingManager is not None + + print("✅ MILB managers are properly defined") + return True + + except Exception as e: + print(f"❌ MILB manager import test failed: {e}") + return False + +def test_ncaa_baseball_manager_imports(): + """Test that NCAA Baseball managers can be imported.""" + print("\n🧪 Testing NCAA Baseball Manager Imports...") + + try: + # Test that we can import the new NCAA Baseball managers + from src.ncaa_baseball_managers_v2 import BaseNCAABaseballManager, NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager + print("✅ NCAA Baseball managers imported successfully") + + # Test that classes are properly defined + assert BaseNCAABaseballManager is not None + assert NCAABaseballLiveManager is not None + assert NCAABaseballRecentManager is not None + assert NCAABaseballUpcomingManager is not None + + print("✅ NCAA Baseball managers are properly defined") + return True + + except Exception as e: + print(f"❌ NCAA Baseball manager import test failed: {e}") + return False + +def test_milb_manager_inheritance(): + """Test that MILB managers properly inherit from baseball base classes.""" + print("\n🧪 Testing MILB Manager Inheritance...") + + try: + from src.milb_managers_v2 import BaseMiLBManager, MiLBLiveManager, MiLBRecentManager, MiLBUpcomingManager + from src.base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming + + # Test inheritance + assert issubclass(BaseMiLBManager, Baseball), "BaseMiLBManager should inherit from Baseball" + assert issubclass(MiLBLiveManager, BaseballLive), "MiLBLiveManager should inherit from BaseballLive" + assert issubclass(MiLBRecentManager, BaseballRecent), "MiLBRecentManager should inherit from BaseballRecent" + assert issubclass(MiLBUpcomingManager, BaseballUpcoming), "MiLBUpcomingManager should inherit from BaseballUpcoming" + + print("✅ MILB managers properly inherit from baseball base classes") + return True + + except Exception as e: + print(f"❌ MILB manager inheritance test failed: {e}") + return False + +def test_ncaa_baseball_manager_inheritance(): + """Test that NCAA Baseball managers properly inherit from baseball base classes.""" + print("\n🧪 Testing NCAA Baseball Manager Inheritance...") + + try: + from src.ncaa_baseball_managers_v2 import BaseNCAABaseballManager, NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager + from src.base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming + + # Test inheritance + assert issubclass(BaseNCAABaseballManager, Baseball), "BaseNCAABaseballManager should inherit from Baseball" + assert issubclass(NCAABaseballLiveManager, BaseballLive), "NCAABaseballLiveManager should inherit from BaseballLive" + assert issubclass(NCAABaseballRecentManager, BaseballRecent), "NCAABaseballRecentManager should inherit from BaseballRecent" + assert issubclass(NCAABaseballUpcomingManager, BaseballUpcoming), "NCAABaseballUpcomingManager should inherit from BaseballUpcoming" + + print("✅ NCAA Baseball managers properly inherit from baseball base classes") + return True + + except Exception as e: + print(f"❌ NCAA Baseball manager inheritance test failed: {e}") + return False + +def test_milb_manager_methods(): + """Test that MILB managers have required methods.""" + print("\n🧪 Testing MILB Manager Methods...") + + try: + from src.milb_managers_v2 import BaseMiLBManager, MiLBLiveManager, MiLBRecentManager, MiLBUpcomingManager + + # Test that managers have required methods + required_methods = ['get_duration', 'display', '_display_single_game'] + + for manager_class in [MiLBLiveManager, MiLBRecentManager, MiLBUpcomingManager]: + for method in required_methods: + assert hasattr(manager_class, method), f"{manager_class.__name__} should have {method} method" + assert callable(getattr(manager_class, method)), f"{manager_class.__name__}.{method} should be callable" + + print("✅ MILB managers have all required methods") + return True + + except Exception as e: + print(f"❌ MILB manager methods test failed: {e}") + return False + +def test_ncaa_baseball_manager_methods(): + """Test that NCAA Baseball managers have required methods.""" + print("\n🧪 Testing NCAA Baseball Manager Methods...") + + try: + from src.ncaa_baseball_managers_v2 import BaseNCAABaseballManager, NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager + + # Test that managers have required methods + required_methods = ['get_duration', 'display', '_display_single_game'] + + for manager_class in [NCAABaseballLiveManager, NCAABaseballRecentManager, NCAABaseballUpcomingManager]: + for method in required_methods: + assert hasattr(manager_class, method), f"{manager_class.__name__} should have {method} method" + assert callable(getattr(manager_class, method)), f"{manager_class.__name__}.{method} should be callable" + + print("✅ NCAA Baseball managers have all required methods") + return True + + except Exception as e: + print(f"❌ NCAA Baseball manager methods test failed: {e}") + return False + +def test_baseball_sport_specific_features(): + """Test that managers have baseball-specific features.""" + print("\n🧪 Testing Baseball Sport-Specific Features...") + + try: + from src.milb_managers_v2 import BaseMiLBManager + from src.ncaa_baseball_managers_v2 import BaseNCAABaseballManager + + # Test that managers have baseball-specific methods + baseball_methods = ['_get_baseball_display_text', '_is_baseball_game_live', '_get_baseball_game_status'] + + for manager_class in [BaseMiLBManager, BaseNCAABaseballManager]: + for method in baseball_methods: + assert hasattr(manager_class, method), f"{manager_class.__name__} should have {method} method" + assert callable(getattr(manager_class, method)), f"{manager_class.__name__}.{method} should be callable" + + print("✅ Baseball managers have sport-specific features") + return True + + except Exception as e: + print(f"❌ Baseball sport-specific features test failed: {e}") + return False + +def test_manager_configuration(): + """Test that managers use proper sport configuration.""" + print("\n🧪 Testing Manager Configuration...") + + try: + from src.base_classes.sport_configs import get_sport_config + + # Test MILB configuration + milb_config = get_sport_config('milb', None) + assert milb_config is not None, "MILB should have configuration" + assert milb_config.sport_specific_fields, "MILB should have sport-specific fields" + + # Test NCAA Baseball configuration + ncaa_baseball_config = get_sport_config('ncaa_baseball', None) + assert ncaa_baseball_config is not None, "NCAA Baseball should have configuration" + assert ncaa_baseball_config.sport_specific_fields, "NCAA Baseball should have sport-specific fields" + + print("✅ Managers use proper sport configuration") + return True + + except Exception as e: + print(f"❌ Manager configuration test failed: {e}") + return False + +def main(): + """Run all baseball manager integration tests.""" + print("⚾ Testing Baseball Managers Integration") + print("=" * 50) + + # Configure logging + logging.basicConfig(level=logging.WARNING) + + # Run all tests + tests = [ + test_milb_manager_imports, + test_ncaa_baseball_manager_imports, + test_milb_manager_inheritance, + test_ncaa_baseball_manager_inheritance, + test_milb_manager_methods, + test_ncaa_baseball_manager_methods, + test_baseball_sport_specific_features, + test_manager_configuration + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + + print("\n" + "=" * 50) + print(f"🏁 Baseball Manager Integration Test Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All baseball manager integration tests passed! MILB and NCAA Baseball work with the new architecture.") + return True + else: + print("❌ Some baseball manager integration tests failed. Please check the errors above.") + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) diff --git a/test/test_baseball_managers_simple.py b/test/test_baseball_managers_simple.py new file mode 100644 index 000000000..26d3aec99 --- /dev/null +++ b/test/test_baseball_managers_simple.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +Test Baseball Managers Integration - Simple Version + +This test validates that MILB and NCAA Baseball managers work with the new +baseball base class architecture without requiring full imports. +""" + +import sys +import os +import logging + +# Add src to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +def test_milb_manager_structure(): + """Test that MILB managers have the correct structure.""" + print("🧪 Testing MILB Manager Structure...") + + try: + # Read the MILB managers file + with open('src/milb_managers_v2.py', 'r') as f: + content = f.read() + + # Check that it imports the baseball base classes + assert 'from .base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming' in content + print("✅ MILB managers import baseball base classes") + + # Check that classes are defined + assert 'class BaseMiLBManager(Baseball):' in content + assert 'class MiLBLiveManager(BaseMiLBManager, BaseballLive):' in content + assert 'class MiLBRecentManager(BaseMiLBManager, BaseballRecent):' in content + assert 'class MiLBUpcomingManager(BaseMiLBManager, BaseballUpcoming):' in content + print("✅ MILB managers have correct class definitions") + + # Check that required methods exist + assert 'def get_duration(self) -> int:' in content + assert 'def display(self, force_clear: bool = False) -> bool:' in content + assert 'def _display_single_game(self, game: Dict) -> None:' in content + print("✅ MILB managers have required methods") + + print("✅ MILB manager structure is correct") + return True + + except Exception as e: + print(f"❌ MILB manager structure test failed: {e}") + return False + +def test_ncaa_baseball_manager_structure(): + """Test that NCAA Baseball managers have the correct structure.""" + print("\n🧪 Testing NCAA Baseball Manager Structure...") + + try: + # Read the NCAA Baseball managers file + with open('src/ncaa_baseball_managers_v2.py', 'r') as f: + content = f.read() + + # Check that it imports the baseball base classes + assert 'from .base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming' in content + print("✅ NCAA Baseball managers import baseball base classes") + + # Check that classes are defined + assert 'class BaseNCAABaseballManager(Baseball):' in content + assert 'class NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive):' in content + assert 'class NCAABaseballRecentManager(BaseNCAABaseballManager, BaseballRecent):' in content + assert 'class NCAABaseballUpcomingManager(BaseNCAABaseballManager, BaseballUpcoming):' in content + print("✅ NCAA Baseball managers have correct class definitions") + + # Check that required methods exist + assert 'def get_duration(self) -> int:' in content + assert 'def display(self, force_clear: bool = False) -> bool:' in content + assert 'def _display_single_game(self, game: Dict) -> None:' in content + print("✅ NCAA Baseball managers have required methods") + + print("✅ NCAA Baseball manager structure is correct") + return True + + except Exception as e: + print(f"❌ NCAA Baseball manager structure test failed: {e}") + return False + +def test_baseball_inheritance(): + """Test that managers properly inherit from baseball base classes.""" + print("\n🧪 Testing Baseball Inheritance...") + + try: + # Read both manager files + with open('src/milb_managers_v2.py', 'r') as f: + milb_content = f.read() + + with open('src/ncaa_baseball_managers_v2.py', 'r') as f: + ncaa_content = f.read() + + # Check that managers inherit from baseball base classes + assert 'BaseMiLBManager(Baseball)' in milb_content + assert 'MiLBLiveManager(BaseMiLBManager, BaseballLive)' in milb_content + assert 'MiLBRecentManager(BaseMiLBManager, BaseballRecent)' in milb_content + assert 'MiLBUpcomingManager(BaseMiLBManager, BaseballUpcoming)' in milb_content + print("✅ MILB managers properly inherit from baseball base classes") + + assert 'BaseNCAABaseballManager(Baseball)' in ncaa_content + assert 'NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive)' in ncaa_content + assert 'NCAABaseballRecentManager(BaseNCAABaseballManager, BaseballRecent)' in ncaa_content + assert 'NCAABaseballUpcomingManager(BaseNCAABaseballManager, BaseballUpcoming)' in ncaa_content + print("✅ NCAA Baseball managers properly inherit from baseball base classes") + + print("✅ Baseball inheritance is correct") + return True + + except Exception as e: + print(f"❌ Baseball inheritance test failed: {e}") + return False + +def test_baseball_sport_specific_methods(): + """Test that managers have baseball-specific methods.""" + print("\n🧪 Testing Baseball Sport-Specific Methods...") + + try: + # Read both manager files + with open('src/milb_managers_v2.py', 'r') as f: + milb_content = f.read() + + with open('src/ncaa_baseball_managers_v2.py', 'r') as f: + ncaa_content = f.read() + + # Check for baseball-specific methods + baseball_methods = [ + '_get_baseball_display_text', + '_is_baseball_game_live', + '_get_baseball_game_status', + '_draw_base_indicators' + ] + + for method in baseball_methods: + assert method in milb_content, f"MILB managers should have {method} method" + assert method in ncaa_content, f"NCAA Baseball managers should have {method} method" + + print("✅ Baseball managers have sport-specific methods") + return True + + except Exception as e: + print(f"❌ Baseball sport-specific methods test failed: {e}") + return False + +def test_manager_initialization(): + """Test that managers are properly initialized.""" + print("\n🧪 Testing Manager Initialization...") + + try: + # Read both manager files + with open('src/milb_managers_v2.py', 'r') as f: + milb_content = f.read() + + with open('src/ncaa_baseball_managers_v2.py', 'r') as f: + ncaa_content = f.read() + + # Check that managers call super().__init__ with sport_key + assert 'super().__init__(config, display_manager, cache_manager, logger, "milb")' in milb_content + assert 'super().__init__(config, display_manager, cache_manager, logger, "ncaa_baseball")' in ncaa_content + print("✅ Managers are properly initialized with sport keys") + + # Check that managers have proper logging + assert 'self.logger.info(' in milb_content + assert 'self.logger.info(' in ncaa_content + print("✅ Managers have proper logging") + + print("✅ Manager initialization is correct") + return True + + except Exception as e: + print(f"❌ Manager initialization test failed: {e}") + return False + +def test_sport_configuration_integration(): + """Test that managers integrate with sport configuration.""" + print("\n🧪 Testing Sport Configuration Integration...") + + try: + # Read both manager files + with open('src/milb_managers_v2.py', 'r') as f: + milb_content = f.read() + + with open('src/ncaa_baseball_managers_v2.py', 'r') as f: + ncaa_content = f.read() + + # Check that managers use sport configuration + assert 'self.sport_config' in milb_content or 'super().__init__' in milb_content + assert 'self.sport_config' in ncaa_content or 'super().__init__' in ncaa_content + print("✅ Managers use sport configuration") + + # Check that managers have sport-specific configuration + assert 'self.milb_config' in milb_content + assert 'self.ncaa_baseball_config' in ncaa_content + print("✅ Managers have sport-specific configuration") + + print("✅ Sport configuration integration is correct") + return True + + except Exception as e: + print(f"❌ Sport configuration integration test failed: {e}") + return False + +def main(): + """Run all baseball manager integration tests.""" + print("⚾ Testing Baseball Managers Integration (Simple)") + print("=" * 50) + + # Configure logging + logging.basicConfig(level=logging.WARNING) + + # Run all tests + tests = [ + test_milb_manager_structure, + test_ncaa_baseball_manager_structure, + test_baseball_inheritance, + test_baseball_sport_specific_methods, + test_manager_initialization, + test_sport_configuration_integration + ] + + passed = 0 + total = len(tests) + + for test in tests: + try: + if test(): + passed += 1 + except Exception as e: + print(f"❌ Test {test.__name__} failed with exception: {e}") + + print("\n" + "=" * 50) + print(f"🏁 Baseball Manager Integration Test Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All baseball manager integration tests passed! MILB and NCAA Baseball work with the new architecture.") + return True + else: + print("❌ Some baseball manager integration tests failed. Please check the errors above.") + return False + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) From bbe28fa291698096d98ff5cd3381839b82fe9db4 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:38:52 -0400 Subject: [PATCH 09/11] cursor rule to help with PR creation --- .cursor/rules/github-branches-rule.mdc | 213 ++++++++++++++ src/base_classes/api_extractors.py | 18 +- src/base_classes/baseball.py | 74 +++-- src/base_classes/data_sources.py | 15 +- src/base_classes/football.py | 29 ++ src/base_classes/hockey.py | 29 ++ src/base_classes/sport_configs.py | 195 ------------- src/base_classes/sports.py | 19 +- src/milb_manager.py | 23 +- src/milb_managers_v2.py | 378 ------------------------- src/mlb_manager.py | 27 +- src/mlb_managers.py | 278 ------------------ src/ncaa_baseball_managers.py | 28 +- src/ncaa_baseball_managers_v2.py | 363 ------------------------ src/ncaa_fb_managers.py | 14 +- 15 files changed, 364 insertions(+), 1339 deletions(-) create mode 100644 .cursor/rules/github-branches-rule.mdc delete mode 100644 src/base_classes/sport_configs.py delete mode 100644 src/milb_managers_v2.py delete mode 100644 src/mlb_managers.py delete mode 100644 src/ncaa_baseball_managers_v2.py diff --git a/.cursor/rules/github-branches-rule.mdc b/.cursor/rules/github-branches-rule.mdc new file mode 100644 index 000000000..87b1ea5f6 --- /dev/null +++ b/.cursor/rules/github-branches-rule.mdc @@ -0,0 +1,213 @@ +--- +description: GitHub branching and pull request best practices for LEDMatrix project +globs: ["**/*.py", "**/*.md", "**/*.json", "**/*.sh"] +alwaysApply: true +--- + +# GitHub Branching and Pull Request Guidelines + +## Branch Naming Conventions + +### Feature Branches +- **Format**: `feature/description-of-feature` +- **Examples**: + - `feature/weather-forecast-improvements` + - `feature/stock-api-integration` + - `feature/nba-live-scores` + +### Bug Fix Branches +- **Format**: `fix/description-of-bug` +- **Examples**: + - `fix/leaderboard-scrolling-performance` + - `fix/weather-api-timeout` + - `fix/display-rendering-issue` + +### Hotfix Branches +- **Format**: `hotfix/critical-issue-description` +- **Examples**: + - `hotfix/display-crash-fix` + - `hotfix/api-rate-limit-fix` + +### Refactoring Branches +- **Format**: `refactor/description-of-refactor` +- **Examples**: + - `refactor/sports-manager-architecture` + - `refactor/cache-management-system` + +## Branch Management Rules + +### Main Branch Protection +- **`main`** branch is protected and requires PR reviews +- Never commit directly to `main` +- All changes must go through pull requests + +### Branch Lifecycle +1. **Create** branch from `main` when starting work +2. **Keep** branch up-to-date with `main` regularly +3. **Test** thoroughly before creating PR +4. **Delete** branch after successful merge + +### Branch Updates +```bash +# Before starting new work +git checkout main +git pull origin main + +# Create new branch +git checkout -b feature/your-feature-name + +# Keep branch updated during development +git checkout main +git pull origin main +git checkout feature/your-feature-name +git merge main +``` + +## Pull Request Guidelines + +### PR Title Format +- **Feature**: `feat: Add weather forecast improvements` +- **Fix**: `fix: Resolve leaderboard scrolling performance issue` +- **Refactor**: `refactor: Improve sports manager architecture` +- **Docs**: `docs: Update API integration guide` +- **Test**: `test: Add unit tests for weather manager` + +### PR Description Template +```markdown +## Description +Brief description of changes and motivation. + +## Type of Change +- [ ] Bug fix (non-breaking change) +- [ ] New feature (non-breaking change) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Performance improvement +- [ ] Refactoring + +## Testing +- [ ] Tested on Raspberry Pi hardware +- [ ] Verified display rendering works correctly +- [ ] Checked API integration functionality +- [ ] Tested error handling scenarios + +## Screenshots/Videos +(If applicable, add screenshots or videos of the changes) + +## Checklist +- [ ] Code follows project style guidelines +- [ ] Self-review completed +- [ ] Comments added for complex logic +- [ ] No hardcoded values or API keys +- [ ] Error handling implemented +- [ ] Logging added where appropriate +``` + +### PR Review Requirements + +#### For Reviewers +- **Code Quality**: Check for proper error handling, logging, and type hints +- **Architecture**: Ensure changes follow project patterns and don't break existing functionality +- **Performance**: Verify changes don't negatively impact display performance +- **Testing**: Confirm changes work on Raspberry Pi hardware +- **Documentation**: Check if documentation needs updates + +#### For Authors +- **Self-Review**: Review your own PR before requesting review +- **Testing**: Test thoroughly on Pi hardware before submitting +- **Documentation**: Update relevant documentation if needed +- **Clean History**: Squash commits if necessary for clean history + +## Commit Message Guidelines + +### Format +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +### Types +- **feat**: New feature +- **fix**: Bug fix +- **docs**: Documentation changes +- **style**: Code style changes (formatting, etc.) +- **refactor**: Code refactoring +- **test**: Adding or updating tests +- **chore**: Maintenance tasks + +### Examples +``` +feat(weather): Add hourly forecast display +fix(nba): Resolve live score update issue +docs(api): Update ESPN API integration guide +refactor(sports): Improve base class architecture +``` + +## Merge Strategies + +### Squash and Merge (Preferred) +- Use for feature branches and bug fixes +- Creates clean, linear history +- Combines all commits into single commit + +### Merge Commit +- Use for complex features with multiple logical commits +- Preserves commit history +- Use when commit messages are meaningful + +### Rebase and Merge +- Use sparingly for simple, single-commit changes +- Creates linear history without merge commits + +## Release Management + +### Version Tags +- Use semantic versioning: `v1.2.3` +- Tag releases on `main` branch +- Create release notes with technical details + +### Release Branches +- **Format**: `release/v1.2.3` +- Use for release preparation +- Include version bumps and final testing + +## Emergency Procedures + +### Hotfix Process +1. Create `hotfix/` branch from `main` +2. Make minimal fix +3. Test thoroughly +4. Create PR with expedited review +5. Merge to `main` and tag release +6. Cherry-pick to other branches if needed + +### Rollback Process +1. Identify last known good commit +2. Create revert PR if possible +3. Use `git revert` for clean rollback +4. Tag rollback release +5. Document issue and resolution + +## Best Practices + +### Before Creating PR +- [ ] Run all tests locally +- [ ] Test on Raspberry Pi hardware +- [ ] Check for linting errors +- [ ] Update documentation if needed +- [ ] Ensure commit messages are clear + +### During Development +- [ ] Keep branches small and focused +- [ ] Commit frequently with meaningful messages +- [ ] Update branch regularly with main +- [ ] Test changes incrementally + +### After PR Approval +- [ ] Delete feature branch after merge +- [ ] Update local main branch +- [ ] Verify changes work in production +- [ ] Update any related documentation diff --git a/src/base_classes/api_extractors.py b/src/base_classes/api_extractors.py index c8fcccf4f..93d7a3763 100644 --- a/src/base_classes/api_extractors.py +++ b/src/base_classes/api_extractors.py @@ -360,20 +360,4 @@ def get_sport_specific_fields(self, game_event: Dict) -> Dict: return {} -def get_extractor_for_sport(sport_key: str, logger: logging.Logger) -> APIDataExtractor: - """Factory function to get the appropriate extractor for a sport.""" - extractors = { - 'nfl': ESPNFootballExtractor, - 'ncaa_fb': ESPNFootballExtractor, - 'mlb': ESPNBaseballExtractor, - 'nhl': ESPNHockeyExtractor, - 'ncaam_hockey': ESPNHockeyExtractor, - 'soccer': SoccerAPIExtractor - } - - extractor_class = extractors.get(sport_key) - if not extractor_class: - logger.warning(f"No extractor found for sport: {sport_key}, using generic ESPN extractor") - return ESPNFootballExtractor(logger) - - return extractor_class(logger) +# Factory function removed - sport classes now instantiate extractors directly diff --git a/src/base_classes/baseball.py b/src/base_classes/baseball.py index c3b2c4ae3..56ffd78ec 100644 --- a/src/base_classes/baseball.py +++ b/src/base_classes/baseball.py @@ -7,14 +7,42 @@ from typing import Dict, Any, Optional, List from src.base_classes.sports import SportsCore +from src.base_classes.api_extractors import ESPNBaseballExtractor +from src.base_classes.data_sources import ESPNDataSource, MLBAPIDataSource import logging class Baseball(SportsCore): """Base class for baseball sports with common functionality.""" + # Baseball sport configuration (moved from sport_configs.py) + SPORT_CONFIG = { + 'update_cadence': 'daily', + 'season_length': 162, + 'games_per_week': 6, + 'api_endpoints': ['scoreboard', 'standings', 'stats'], + 'sport_specific_fields': ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'], + 'update_interval_seconds': 30, + 'logo_dir': 'assets/sports/mlb_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', # Can be overridden for MLB API + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/baseball' + } + def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) + # Initialize baseball-specific architecture components + self.sport_config = self.get_sport_config() + self.api_extractor = ESPNBaseballExtractor(logger) + + # Choose data source based on sport (MLB uses MLB API, others use ESPN) + if sport_key == 'mlb': + self.data_source = MLBAPIDataSource(logger) + else: + self.data_source = ESPNDataSource(logger) + # Baseball-specific configuration self.show_innings = self.mode_config.get("show_innings", True) self.show_outs = self.mode_config.get("show_outs", True) @@ -22,6 +50,10 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logge self.show_count = self.mode_config.get("show_count", True) self.show_pitcher_batter = self.mode_config.get("show_pitcher_batter", False) + def get_sport_config(self) -> Dict[str, Any]: + """Get baseball sport configuration.""" + return self.SPORT_CONFIG.copy() + def _get_baseball_display_text(self, game: Dict) -> str: """Get baseball-specific display text.""" try: @@ -131,45 +163,3 @@ def _should_show_baseball_game(self, game: Dict) -> bool: return False -class BaseballRecent(Baseball): - """Base class for recent baseball games.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): - super().__init__(config, display_manager, cache_manager, logger, sport_key) - self.logger.info(f"{sport_key.upper()} Recent Manager initialized") - - def _should_show_baseball_game(self, game: Dict) -> bool: - """Determine if a recent baseball game should be shown.""" - try: - # Only show final games - if not game.get('is_final', False): - return False - - # Check if game meets display criteria - return self._should_show_game(game) - - except Exception as e: - self.logger.error(f"Error checking if baseball game should be shown: {e}") - return False - - -class BaseballUpcoming(Baseball): - """Base class for upcoming baseball games.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager, logger: logging.Logger, sport_key: str): - super().__init__(config, display_manager, cache_manager, logger, sport_key) - self.logger.info(f"{sport_key.upper()} Upcoming Manager initialized") - - def _should_show_baseball_game(self, game: Dict) -> bool: - """Determine if an upcoming baseball game should be shown.""" - try: - # Only show upcoming games - if not game.get('is_upcoming', False): - return False - - # Check if game meets display criteria - return self._should_show_game(game) - - except Exception as e: - self.logger.error(f"Error checking if baseball game should be shown: {e}") - return False diff --git a/src/base_classes/data_sources.py b/src/base_classes/data_sources.py index eb9516ad6..6132b5360 100644 --- a/src/base_classes/data_sources.py +++ b/src/base_classes/data_sources.py @@ -285,17 +285,4 @@ def fetch_standings(self, sport: str, league: str) -> Dict: return {} -def get_data_source_for_sport(sport_key: str, data_source_type: str, logger: logging.Logger, **kwargs) -> DataSource: - """Factory function to get the appropriate data source for a sport.""" - data_sources = { - 'espn': ESPNDataSource, - 'mlb_api': MLBAPIDataSource, - 'soccer_api': SoccerAPIDataSource - } - - data_source_class = data_sources.get(data_source_type) - if not data_source_class: - logger.warning(f"No data source found for type: {data_source_type}, using ESPN") - return ESPNDataSource(logger) - - return data_source_class(logger, **kwargs) +# Factory function removed - sport classes now instantiate data sources directly diff --git a/src/base_classes/football.py b/src/base_classes/football.py index a52b2222a..cf67ca6cd 100644 --- a/src/base_classes/football.py +++ b/src/base_classes/football.py @@ -7,11 +7,40 @@ import time import pytz from src.base_classes.sports import SportsCore +from src.base_classes.api_extractors import ESPNFootballExtractor +from src.base_classes.data_sources import ESPNDataSource import requests class Football(SportsCore): + """Base class for football sports with common functionality.""" + + # Football sport configuration (moved from sport_configs.py) + SPORT_CONFIG = { + 'update_cadence': 'weekly', + 'season_length': 17, # NFL default + 'games_per_week': 1, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], + 'update_interval_seconds': 60, + 'logo_dir': 'assets/sports/nfl_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/football' + } + def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) + + # Initialize football-specific architecture components + self.sport_config = self.get_sport_config() + self.api_extractor = ESPNFootballExtractor(logger) + self.data_source = ESPNDataSource(logger) + + def get_sport_config(self) -> Dict[str, Any]: + """Get football sport configuration.""" + return self.SPORT_CONFIG.copy() def _fetch_game_odds(self, _: Dict) -> None: pass diff --git a/src/base_classes/hockey.py b/src/base_classes/hockey.py index 31b4938ad..c5743471b 100644 --- a/src/base_classes/hockey.py +++ b/src/base_classes/hockey.py @@ -6,10 +6,39 @@ from PIL import Image, ImageDraw, ImageFont import time from src.base_classes.sports import SportsCore +from src.base_classes.api_extractors import ESPNHockeyExtractor +from src.base_classes.data_sources import ESPNDataSource class Hockey(SportsCore): + """Base class for hockey sports with common functionality.""" + + # Hockey sport configuration (moved from sport_configs.py) + SPORT_CONFIG = { + 'update_cadence': 'daily', + 'season_length': 82, # NHL default + 'games_per_week': 3, + 'api_endpoints': ['scoreboard', 'standings'], + 'sport_specific_fields': ['period', 'power_play', 'penalties', 'shots_on_goal'], + 'update_interval_seconds': 30, + 'logo_dir': 'assets/sports/nhl_logos', + 'show_records': True, + 'show_ranking': True, + 'show_odds': True, + 'data_source_type': 'espn', + 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey' + } + def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): super().__init__(config, display_manager, cache_manager, logger, sport_key) + + # Initialize hockey-specific architecture components + self.sport_config = self.get_sport_config() + self.api_extractor = ESPNHockeyExtractor(logger) + self.data_source = ESPNDataSource(logger) + + def get_sport_config(self) -> Dict[str, Any]: + """Get hockey sport configuration.""" + return self.SPORT_CONFIG.copy() def _fetch_odds(self, game: Dict, league: str) -> None: super()._fetch_odds(game, "hockey", league) diff --git a/src/base_classes/sport_configs.py b/src/base_classes/sport_configs.py deleted file mode 100644 index c2a919177..000000000 --- a/src/base_classes/sport_configs.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Sport-Specific Configuration System - -This module provides sport-specific configurations including update cadences, -season characteristics, and sport-specific fields for different sports. -""" - -from typing import Dict, Any, List -import logging - -class SportConfig: - """Configuration for a specific sport.""" - - def __init__(self, sport_key: str, config: Dict[str, Any]): - self.sport_key = sport_key - self.config = config - - # Sport-specific characteristics - self.update_cadence = config.get('update_cadence', 'daily') - self.season_length = config.get('season_length', 16) - self.games_per_week = config.get('games_per_week', 1) - self.api_endpoints = config.get('api_endpoints', ['scoreboard']) - self.sport_specific_fields = config.get('sport_specific_fields', []) - self.update_interval_seconds = config.get('update_interval_seconds', 60) - self.logo_dir = config.get('logo_dir', 'assets/sports/ncaa_logos') - - # Display characteristics - self.show_records = config.get('show_records', False) - self.show_ranking = config.get('show_ranking', False) - self.show_odds = config.get('show_odds', False) - - # Data source configuration - self.data_source_type = config.get('data_source_type', 'espn') - self.api_base_url = config.get('api_base_url', '') - self.requires_authentication = config.get('requires_authentication', False) - - def get_update_interval(self) -> int: - """Get the appropriate update interval for this sport.""" - return self.update_interval_seconds - - def should_update_now(self, last_update: float, current_time: float) -> bool: - """Check if this sport should be updated based on its cadence.""" - time_since_update = current_time - last_update - - if self.update_cadence == 'daily': - return time_since_update >= 3600 # 1 hour - elif self.update_cadence == 'weekly': - return time_since_update >= 86400 # 24 hours - elif self.update_cadence == 'hourly': - return time_since_update >= 3600 # 1 hour - elif self.update_cadence == 'live_only': - return time_since_update >= 30 # 30 seconds for live games - else: - return time_since_update >= self.update_interval_seconds - - -def get_sport_configs() -> Dict[str, Dict[str, Any]]: - """Get all sport-specific configurations.""" - return { - 'nfl': { - 'update_cadence': 'weekly', - 'season_length': 17, - 'games_per_week': 1, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], - 'update_interval_seconds': 60, - 'logo_dir': 'assets/sports/nfl_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/football/nfl' - }, - 'ncaa_fb': { - 'update_cadence': 'weekly', - 'season_length': 12, - 'games_per_week': 1, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['down', 'distance', 'possession', 'timeouts', 'is_redzone'], - 'update_interval_seconds': 60, - 'logo_dir': 'assets/sports/ncaa_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football' - }, - 'mlb': { - 'update_cadence': 'daily', - 'season_length': 162, - 'games_per_week': 6, - 'api_endpoints': ['scoreboard', 'standings', 'stats'], - 'sport_specific_fields': ['inning', 'outs', 'bases', 'strikes', 'balls', 'pitcher', 'batter'], - 'update_interval_seconds': 30, - 'logo_dir': 'assets/sports/mlb_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'mlb_api', - 'api_base_url': 'https://statsapi.mlb.com/api/v1' - }, - 'nhl': { - 'update_cadence': 'daily', - 'season_length': 82, - 'games_per_week': 3, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['period', 'power_play', 'penalties', 'shots_on_goal'], - 'update_interval_seconds': 30, - 'logo_dir': 'assets/sports/nhl_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey/nhl' - }, - 'ncaam_hockey': { - 'update_cadence': 'weekly', - 'season_length': 34, - 'games_per_week': 2, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['period', 'power_play', 'penalties', 'shots_on_goal'], - 'update_interval_seconds': 60, - 'logo_dir': 'assets/sports/ncaa_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': False, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/hockey/mens-college-hockey' - }, - 'soccer': { - 'update_cadence': 'weekly', - 'season_length': 34, - 'games_per_week': 1, - 'api_endpoints': ['fixtures', 'standings'], - 'sport_specific_fields': ['half', 'stoppage_time', 'cards', 'possession'], - 'update_interval_seconds': 60, - 'logo_dir': 'assets/sports/soccer_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'soccer_api', - 'api_base_url': 'https://api.football-data.org/v4' - }, - 'nba': { - 'update_cadence': 'daily', - 'season_length': 82, - 'games_per_week': 3, - 'api_endpoints': ['scoreboard', 'standings'], - 'sport_specific_fields': ['quarter', 'time_remaining', 'fouls', 'timeouts'], - 'update_interval_seconds': 30, - 'logo_dir': 'assets/sports/nba_logos', - 'show_records': True, - 'show_ranking': True, - 'show_odds': True, - 'data_source_type': 'espn', - 'api_base_url': 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba' - } - } - - -def get_sport_config(sport_key: str, logger: logging.Logger) -> SportConfig: - """Get configuration for a specific sport.""" - configs = get_sport_configs() - sport_config = configs.get(sport_key, {}) - - if not sport_config: - logger.warning(f"No configuration found for sport: {sport_key}, using default") - sport_config = { - 'update_cadence': 'daily', - 'season_length': 16, - 'games_per_week': 1, - 'api_endpoints': ['scoreboard'], - 'sport_specific_fields': [], - 'update_interval_seconds': 60, - 'logo_dir': 'assets/sports/ncaa_logos', - 'show_records': False, - 'show_ranking': False, - 'show_odds': False, - 'data_source_type': 'espn', - 'api_base_url': '' - } - - return SportConfig(sport_key, sport_config) - - -def get_sports_by_update_cadence(cadence: str) -> List[str]: - """Get all sports that use a specific update cadence.""" - configs = get_sport_configs() - return [sport for sport, config in configs.items() if config.get('update_cadence') == cadence] - - -def get_sports_by_data_source(data_source_type: str) -> List[str]: - """Get all sports that use a specific data source.""" - configs = get_sport_configs() - return [sport for sport, config in configs.items() if config.get('data_source_type') == data_source_type] diff --git a/src/base_classes/sports.py b/src/base_classes/sports.py index 9d5c2aec3..9cd9d4200 100644 --- a/src/base_classes/sports.py +++ b/src/base_classes/sports.py @@ -15,10 +15,9 @@ from src.logo_downloader import download_missing_logo, LogoDownloader from pathlib import Path -# Import new architecture components -from .api_extractors import get_extractor_for_sport -from .sport_configs import get_sport_config -from .data_sources import get_data_source_for_sport +# Import new architecture components (individual classes will import what they need) +from .api_extractors import ESPNFootballExtractor, ESPNBaseballExtractor, ESPNHockeyExtractor +from .data_sources import ESPNDataSource, MLBAPIDataSource class SportsCore: def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): @@ -34,14 +33,10 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cach self.sport_key = sport_key - # Initialize new architecture components - self.sport_config = get_sport_config(sport_key, logger) - self.api_extractor = get_extractor_for_sport(sport_key, logger) - self.data_source = get_data_source_for_sport( - sport_key, - self.sport_config.data_source_type, - logger - ) + # Initialize new architecture components (will be overridden by sport-specific classes) + self.sport_config = None + self.api_extractor = None + self.data_source = None self.mode_config = config.get(f"{sport_key}_scoreboard", {}) # Changed config key self.is_enabled = self.mode_config.get("enabled", False) self.show_odds = self.mode_config.get("show_odds", False) diff --git a/src/milb_manager.py b/src/milb_manager.py index d7b418aa3..d3c12c2ae 100644 --- a/src/milb_manager.py +++ b/src/milb_manager.py @@ -14,6 +14,10 @@ import pytz from src.background_data_service import get_background_service +# Import baseball and standard sports classes +from .base_classes.baseball import Baseball, BaseballLive +from .base_classes.sports import SportsRecent, SportsUpcoming + # Import API counter function try: from web_interface_v2 import increment_api_counter @@ -24,17 +28,16 @@ def increment_api_counter(kind: str, count: int = 1): # Get logger logger = logging.getLogger(__name__) -class BaseMiLBManager: - """Base class for MiLB managers with common functionality.""" +class BaseMiLBManager(Baseball): + """Base class for MiLB managers using new baseball architecture.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - self.config = config - self.display_manager = display_manager + # Initialize with sport_key for MiLB + super().__init__(config, display_manager, cache_manager, logger, "milb") + + # MiLB-specific configuration self.milb_config = config.get('milb', {}) self.favorite_teams = self.milb_config.get('favorite_teams', []) self.show_records = self.milb_config.get('show_records', False) - self.cache_manager = cache_manager - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.INFO) # Set logger level to INFO # Load MiLB team mapping self.team_mapping = {} @@ -896,7 +899,7 @@ def _extract_game_details(self, game) -> Dict: return game_data return {} -class MiLBLiveManager(BaseMiLBManager): +class MiLBLiveManager(BaseMiLBManager, BaseballLive): """Manager for displaying live MiLB games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) @@ -1424,7 +1427,7 @@ def display(self, force_clear: bool = False): except Exception as e: logger.error(f"[MiLB] Error displaying live game: {e}", exc_info=True) -class MiLBRecentManager(BaseMiLBManager): +class MiLBRecentManager(BaseMiLBManager, SportsRecent): """Manager for displaying recent MiLB games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) @@ -1615,7 +1618,7 @@ def display(self, force_clear: bool = False): except Exception as e: logger.error(f"[MiLB] Error displaying recent game: {e}", exc_info=True) -class MiLBUpcomingManager(BaseMiLBManager): +class MiLBUpcomingManager(BaseMiLBManager, SportsUpcoming): """Manager for upcoming MiLB games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) diff --git a/src/milb_managers_v2.py b/src/milb_managers_v2.py deleted file mode 100644 index 25985baca..000000000 --- a/src/milb_managers_v2.py +++ /dev/null @@ -1,378 +0,0 @@ -""" -MiLB (Minor League Baseball) Managers - Updated to use new baseball base class - -This module demonstrates how to update existing baseball managers to use the new -baseball base class architecture while maintaining all existing functionality. -""" - -import time -import logging -import requests -import json -from typing import Dict, Any, List, Optional -from datetime import datetime, timedelta, timezone -import os -from PIL import Image, ImageDraw, ImageFont -import numpy as np -from .cache_manager import CacheManager -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry -import pytz -from src.background_data_service import get_background_service - -# Import new baseball base classes -from .base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming - -# Import API counter function -try: - from web_interface_v2 import increment_api_counter -except ImportError: - def increment_api_counter(kind: str, count: int = 1): - pass - -# Get logger -logger = logging.getLogger(__name__) - -class BaseMiLBManager(Baseball): - """Base class for MiLB managers using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - # Initialize with sport_key for MiLB - super().__init__(config, display_manager, cache_manager, logger, "milb") - - # MiLB-specific configuration - self.milb_config = config.get('milb', {}) - self.favorite_teams = self.milb_config.get('favorite_teams', []) - self.show_records = self.milb_config.get('show_records', False) - - # Load MiLB team mapping - self.team_mapping = {} - self.team_name_to_abbr = {} - team_mapping_path = os.path.join('assets', 'sports', 'milb_logos', 'milb_team_mapping.json') - try: - with open(team_mapping_path, 'r') as f: - self.team_mapping = json.load(f) - self.team_name_to_abbr = {name: data['abbreviation'] for name, data in self.team_mapping.items()} - self.logger.info(f"Loaded {len(self.team_name_to_abbr)} MiLB team mappings.") - except Exception as e: - self.logger.error(f"Failed to load MiLB team mapping: {e}") - - # Set up session with retry logic - self.session = requests.Session() - retry_strategy = Retry( - total=5, - backoff_factor=1, - status_forcelist=[429, 500, 502, 503, 504], - ) - adapter = HTTPAdapter(max_retries=retry_strategy) - self.session.mount("http://", adapter) - self.session.mount("https://", adapter) - - # Load fonts - self.fonts = self._load_fonts() - - # Initialize game tracking - self.live_games = [] - self.current_game = None - self.current_game_index = 0 - self.last_update = 0 - self.update_interval = self.milb_config.get('live_update_interval', 20) - self.no_data_interval = max(300, self.update_interval) - self.last_game_switch = 0 - self.game_display_duration = self.milb_config.get('live_game_duration', 30) - self.last_display_update = 0 - self.last_log_time = 0 - self.log_interval = 300 - self.last_count_log_time = 0 - self.count_log_interval = 5 - self.test_mode = self.milb_config.get('test_mode', False) - - def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: - """Load fonts for display.""" - fonts = {} - try: - # Load main font - font_path = os.path.join('assets', 'fonts', '5by7.regular.ttf') - if os.path.exists(font_path): - fonts['main'] = ImageFont.truetype(font_path, 8) - else: - fonts['main'] = ImageFont.load_default() - - # Load small font - fonts['small'] = ImageFont.load_default() - - return fonts - except Exception as e: - self.logger.error(f"Error loading fonts: {e}") - return {'main': ImageFont.load_default(), 'small': ImageFont.load_default()} - - def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: - """Get team logo for display.""" - try: - logo_path = os.path.join('assets', 'sports', 'milb_logos', f"{team_abbr}.png") - if os.path.exists(logo_path): - return Image.open(logo_path) - return None - except Exception as e: - self.logger.error(f"Error loading logo for {team_abbr}: {e}") - return None - - def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): - """Draw text with outline for better visibility.""" - x, y = position - for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: - draw.text((x + dx, y + dy), text, font=font, fill=outline_color) - draw.text((x, y), text, font=font, fill=fill) - - def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None: - """Draw base indicators for baseball.""" - base_size = 3 - base_spacing = 8 - - # Draw bases in diamond formation - base_positions = [ - (center_x, y - base_spacing), # 1st base - (center_x + base_spacing, y), # 2nd base - (center_x, y + base_spacing), # 3rd base - (center_x - base_spacing, y) # Home plate - ] - - for i, (pos, occupied) in enumerate(zip(base_positions, bases_occupied)): - color = (255, 255, 0) if occupied else (128, 128, 128) - draw.ellipse([pos[0] - base_size, pos[1] - base_size, - pos[0] + base_size, pos[1] + base_size], fill=color) - - def _create_game_display(self, game_data: Dict[str, Any]) -> Image.Image: - """Create display image for a game.""" - try: - # Create image - width = self.display_manager.matrix.width - height = self.display_manager.matrix.height - image = Image.new('RGB', (width, height), (0, 0, 0)) - draw = ImageDraw.Draw(image) - - # Get game details - home_team = game_data.get('home_team', {}) - away_team = game_data.get('away_team', {}) - home_score = game_data.get('home_score', '0') - away_score = game_data.get('away_score', '0') - - # Get baseball-specific details - inning = game_data.get('inning', '') - outs = game_data.get('outs', 0) - bases = game_data.get('bases', '') - strikes = game_data.get('strikes', 0) - balls = game_data.get('balls', 0) - - # Draw team names and scores - font = self.fonts['main'] - y_offset = 10 - - # Away team - away_text = f"{away_team.get('abbreviation', 'AWAY')} {away_score}" - draw.text((5, y_offset), away_text, font=font, fill=(255, 255, 255)) - - # Home team - home_text = f"{home_team.get('abbreviation', 'HOME')} {home_score}" - draw.text((5, y_offset + 15), home_text, font=font, fill=(255, 255, 255)) - - # Baseball-specific details - if inning: - inning_text = f"Inning: {inning}" - draw.text((5, y_offset + 30), inning_text, font=font, fill=(255, 255, 255)) - - if outs is not None: - outs_text = f"Outs: {outs}" - draw.text((5, y_offset + 45), outs_text, font=font, fill=(255, 255, 255)) - - if strikes is not None and balls is not None: - count_text = f"Count: {balls}-{strikes}" - draw.text((5, y_offset + 60), count_text, font=font, fill=(255, 255, 255)) - - return image - - except Exception as e: - self.logger.error(f"Error creating game display: {e}") - return Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) - - def _fetch_milb_api_data(self, use_cache: bool = True) -> Dict[str, Any]: - """Fetch MiLB data from API.""" - try: - # This would implement the actual MiLB API fetching - # For now, return empty data - return {} - except Exception as e: - self.logger.error(f"Error fetching MiLB data: {e}") - return {} - - def _extract_game_details(self, game) -> Dict: - """Extract game details from API response.""" - try: - # This would implement the actual game details extraction - # For now, return empty data - return {} - except Exception as e: - self.logger.error(f"Error extracting game details: {e}") - return {} - - def _is_baseball_game_live(self, game: Dict) -> bool: - """Check if a baseball game is currently live.""" - return super()._is_baseball_game_live(game) - - def _get_baseball_game_status(self, game: Dict) -> str: - """Get baseball-specific game status.""" - return super()._get_baseball_game_status(game) - - -class MiLBLiveManager(BaseMiLBManager, BaseballLive): - """Manager for live MiLB games using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("MiLB Live Manager initialized with new baseball architecture") - - def get_duration(self) -> int: - """Get display duration for live MiLB games.""" - return self.milb_config.get('live_game_duration', 30) - - def display(self, force_clear: bool = False) -> bool: - """Display live MiLB games.""" - try: - # Fetch live games using the new architecture - live_games = self._fetch_immediate_games() - - if not live_games: - self.logger.warning("No live MiLB games found") - return False - - # Filter games based on criteria - games_to_show = [game for game in live_games if self._should_show_baseball_game(game)] - - if not games_to_show: - self.logger.debug("No MiLB games meet display criteria") - return False - - # Display each game - for game in games_to_show: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying live MiLB games: {e}") - return False - - def _display_single_game(self, game: Dict) -> None: - """Display a single MiLB game.""" - try: - # Get game details - home_team = game.get('home_team_name', '') - away_team = game.get('away_team_name', '') - home_score = game.get('home_score', '0') - away_score = game.get('away_score', '0') - status = game.get('status_text', '') - - # Get baseball-specific display text - baseball_text = self._get_baseball_display_text(game) - - # Create display text - display_text = f"{away_team} {away_score} @ {home_team} {home_score}" - if status: - display_text += f" - {status}" - if baseball_text: - display_text += f" ({baseball_text})" - - # Display the text - self.display_manager.display_text(display_text) - - except Exception as e: - self.logger.error(f"Error displaying single MiLB game: {e}") - - -class MiLBRecentManager(BaseMiLBManager, BaseballRecent): - """Manager for recent MiLB games using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("MiLB Recent Manager initialized with new baseball architecture") - - def get_duration(self) -> int: - """Get display duration for recent MiLB games.""" - return self.milb_config.get('recent_game_duration', 20) - - def display(self, force_clear: bool = False) -> bool: - """Display recent MiLB games.""" - try: - # Fetch recent games using the new architecture - recent_games = self._get_partial_schedule_data(datetime.now().year) - - if not recent_games: - self.logger.warning("No recent MiLB games found") - return False - - # Filter for recent games (last 24 hours) - now = datetime.now(pytz.UTC) - recent_games = [game for game in recent_games - if game.get('is_final', False) and - game.get('start_time_utc') and - (now - game['start_time_utc']).total_seconds() < 86400] - - if not recent_games: - self.logger.debug("No recent MiLB games in last 24 hours") - return False - - # Display each game - for game in recent_games: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying recent MiLB games: {e}") - return False - - -class MiLBUpcomingManager(BaseMiLBManager, BaseballUpcoming): - """Manager for upcoming MiLB games using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("MiLB Upcoming Manager initialized with new baseball architecture") - - def get_duration(self) -> int: - """Get display duration for upcoming MiLB games.""" - return self.milb_config.get('upcoming_game_duration', 15) - - def display(self, force_clear: bool = False) -> bool: - """Display upcoming MiLB games.""" - try: - # Fetch upcoming games using the new architecture - upcoming_games = self._get_partial_schedule_data(datetime.now().year) - - if not upcoming_games: - self.logger.warning("No upcoming MiLB games found") - return False - - # Filter for upcoming games (next 7 days) - now = datetime.now(pytz.UTC) - upcoming_games = [game for game in upcoming_games - if game.get('is_upcoming', False) and - game.get('start_time_utc') and - (game['start_time_utc'] - now).total_seconds() < 604800] - - if not upcoming_games: - self.logger.debug("No upcoming MiLB games in next 7 days") - return False - - # Display each game - for game in upcoming_games: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying upcoming MiLB games: {e}") - return False diff --git a/src/mlb_manager.py b/src/mlb_manager.py index 762154b40..4885a6f56 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -14,6 +14,10 @@ from src.odds_manager import OddsManager from src.background_data_service import get_background_service +# Import baseball and standard sports classes +from .base_classes.baseball import Baseball, BaseballLive +from .base_classes.sports import SportsRecent, SportsUpcoming + # Import the API counter function from web interface try: from web_interface_v2 import increment_api_counter @@ -25,20 +29,21 @@ def increment_api_counter(kind: str, count: int = 1): # Get logger logger = logging.getLogger(__name__) -class BaseMLBManager: - """Base class for MLB managers with common functionality.""" +class BaseMLBManager(Baseball): + """Base class for MLB managers using new baseball architecture.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - self.config = config - self.display_manager = display_manager - # Store reference to config instead of creating new ConfigManager - self.config_manager = None # Not used in this class + # Initialize with sport_key for MLB + super().__init__(config, display_manager, cache_manager, logger, "mlb") + + # MLB-specific configuration self.mlb_config = config.get('mlb', {}) self.show_odds = self.mlb_config.get("show_odds", False) self.favorite_teams = self.mlb_config.get('favorite_teams', []) self.show_records = self.mlb_config.get('show_records', False) - self.cache_manager = cache_manager + + # Store reference to config instead of creating new ConfigManager + self.config_manager = None # Not used in this class self.odds_manager = OddsManager(self.cache_manager, self.config_manager) - self.logger = logging.getLogger(__name__) # Logo handling self.logo_dir = self.mlb_config.get('logo_dir', os.path.join('assets', 'sports', 'mlb_logos')) @@ -744,7 +749,7 @@ def _draw_dynamic_odds(self, draw: ImageDraw.Draw, odds: Dict[str, Any], width: self._draw_text_with_outline(draw, ou_text, (ou_x, ou_y), font, fill=(0, 255, 0)) -class MLBLiveManager(BaseMLBManager): +class MLBLiveManager(BaseMLBManager, BaseballLive): """Manager for displaying live MLB games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) @@ -1157,7 +1162,7 @@ def display(self, force_clear: bool = False): except Exception as e: logger.error(f"[MLB] Error displaying live game: {e}", exc_info=True) -class MLBRecentManager(BaseMLBManager): +class MLBRecentManager(BaseMLBManager, SportsRecent): """Manager for displaying recent MLB games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) @@ -1318,7 +1323,7 @@ def display(self, force_clear: bool = False): except Exception as e: logger.error(f"[MLB] Error displaying recent game: {e}", exc_info=True) -class MLBUpcomingManager(BaseMLBManager): +class MLBUpcomingManager(BaseMLBManager, SportsUpcoming): """Manager for displaying upcoming MLB games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) diff --git a/src/mlb_managers.py b/src/mlb_managers.py deleted file mode 100644 index 81109bcae..000000000 --- a/src/mlb_managers.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -MLB (Major League Baseball) Managers - -This module demonstrates how to add a new sport using the new architecture. -Baseball has different characteristics than football/hockey: -- Daily games during season -- Different sport-specific fields (innings, outs, bases, etc.) -- Different data source (MLB API instead of ESPN) -""" - -import os -import time -import logging -import requests -from typing import Dict, Any, Optional, List -from datetime import datetime, timedelta -from src.display_manager import DisplayManager -from src.cache_manager import CacheManager -import pytz -from src.base_classes.sports import SportsRecent, SportsUpcoming, SportsCore -from pathlib import Path - -class BaseMLBManager(SportsCore): - """Base class for MLB managers with common functionality.""" - # Class variables for warning tracking - _no_data_warning_logged = False - _last_warning_time = 0 - _warning_cooldown = 60 # Only log warnings once per minute - _shared_data = None - _last_shared_update = 0 - _processed_games_cache = {} # Cache for processed game data - _processed_games_timestamp = 0 - - def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): - self.logger = logging.getLogger('MLB') - super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="mlb") - - # Override configuration with sport-specific settings - self.logo_dir = Path(self.sport_config.logo_dir) - self.update_interval = self.sport_config.get_update_interval() - - # Check display modes to determine what data to fetch - display_modes = self.mode_config.get("display_modes", {}) - self.recent_enabled = display_modes.get("mlb_recent", False) - self.upcoming_enabled = display_modes.get("mlb_upcoming", False) - self.live_enabled = display_modes.get("mlb_live", False) - - # MLB-specific configuration - self.favorite_teams = self.mode_config.get("favorite_teams", []) - self.show_records = self.sport_config.show_records - self.show_ranking = self.sport_config.show_ranking - self.show_odds = self.sport_config.show_odds - - def _get_sport_specific_display_text(self, game: Dict) -> str: - """Get sport-specific display text for baseball.""" - try: - # Extract baseball-specific fields - inning = game.get('inning', '') - outs = game.get('outs', 0) - bases = game.get('bases', '') - strikes = game.get('strikes', 0) - balls = game.get('balls', 0) - - # Build display text - display_parts = [] - - if inning: - display_parts.append(f"Inning: {inning}") - - if outs is not None: - display_parts.append(f"Outs: {outs}") - - if bases: - display_parts.append(f"Bases: {bases}") - - if strikes is not None and balls is not None: - display_parts.append(f"Count: {balls}-{strikes}") - - return " | ".join(display_parts) if display_parts else "" - - except Exception as e: - self.logger.error(f"Error getting sport-specific display text: {e}") - return "" - - def _should_show_game(self, game: Dict) -> bool: - """Determine if a game should be shown based on MLB-specific criteria.""" - try: - # Check if game is live or recent - is_live = game.get('is_live', False) - is_final = game.get('is_final', False) - is_upcoming = game.get('is_upcoming', False) - - # Show live games - if is_live and self.live_enabled: - return True - - # Show recent games (within last 24 hours) - if is_final and self.recent_enabled: - # Check if game ended within last 24 hours - game_time = game.get('start_time_utc') - if game_time: - time_diff = datetime.now(pytz.UTC) - game_time - if time_diff.total_seconds() < 86400: # 24 hours - return True - - # Show upcoming games (within next 7 days) - if is_upcoming and self.upcoming_enabled: - game_time = game.get('start_time_utc') - if game_time: - time_diff = game_time - datetime.now(pytz.UTC) - if time_diff.total_seconds() < 604800: # 7 days - return True - - return False - - except Exception as e: - self.logger.error(f"Error checking if game should be shown: {e}") - return False - - -class MLBLiveManager(BaseMLBManager): - """Manager for live MLB games.""" - - def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("MLB Live Manager initialized") - - def get_duration(self) -> int: - """Get display duration for live MLB games.""" - return self.mode_config.get("duration", 10) - - def display(self) -> bool: - """Display live MLB games.""" - try: - # Fetch live games using the new architecture - live_games = self._fetch_immediate_games() - - if not live_games: - if not self._no_data_warning_logged: - self.logger.warning("No live MLB games found") - self._no_data_warning_logged = True - return False - - # Filter games based on criteria - games_to_show = [game for game in live_games if self._should_show_game(game)] - - if not games_to_show: - self.logger.debug("No MLB games meet display criteria") - return False - - # Display each game - for game in games_to_show: - self._display_single_game(game) - time.sleep(2) # Brief pause between games - - return True - - except Exception as e: - self.logger.error(f"Error displaying live MLB games: {e}") - return False - - def _display_single_game(self, game: Dict) -> None: - """Display a single MLB game.""" - try: - # Get game details - home_team = game.get('home_team_name', '') - away_team = game.get('away_team_name', '') - home_score = game.get('home_score', '0') - away_score = game.get('away_score', '0') - status = game.get('status_text', '') - - # Get sport-specific display text - sport_text = self._get_sport_specific_display_text(game) - - # Create display text - display_text = f"{away_team} {away_score} @ {home_team} {home_score}" - if status: - display_text += f" - {status}" - if sport_text: - display_text += f" ({sport_text})" - - # Display the text - self.display_manager.display_text(display_text) - - except Exception as e: - self.logger.error(f"Error displaying single MLB game: {e}") - - -class MLBRecentManager(BaseMLBManager): - """Manager for recent MLB games.""" - - def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("MLB Recent Manager initialized") - - def get_duration(self) -> int: - """Get display duration for recent MLB games.""" - return self.mode_config.get("duration", 8) - - def display(self) -> bool: - """Display recent MLB games.""" - try: - # Fetch recent games using the new architecture - recent_games = self._get_partial_schedule_data(datetime.now().year) - - if not recent_games: - if not self._no_data_warning_logged: - self.logger.warning("No recent MLB games found") - self._no_data_warning_logged = True - return False - - # Filter for recent games (last 24 hours) - now = datetime.now(pytz.UTC) - recent_games = [game for game in recent_games - if game.get('is_final', False) and - game.get('start_time_utc') and - (now - game['start_time_utc']).total_seconds() < 86400] - - if not recent_games: - self.logger.debug("No recent MLB games in last 24 hours") - return False - - # Display each game - for game in recent_games: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying recent MLB games: {e}") - return False - - -class MLBUpcomingManager(BaseMLBManager): - """Manager for upcoming MLB games.""" - - def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("MLB Upcoming Manager initialized") - - def get_duration(self) -> int: - """Get display duration for upcoming MLB games.""" - return self.mode_config.get("duration", 6) - - def display(self) -> bool: - """Display upcoming MLB games.""" - try: - # Fetch upcoming games using the new architecture - upcoming_games = self._get_partial_schedule_data(datetime.now().year) - - if not upcoming_games: - if not self._no_data_warning_logged: - self.logger.warning("No upcoming MLB games found") - self._no_data_warning_logged = True - return False - - # Filter for upcoming games (next 7 days) - now = datetime.now(pytz.UTC) - upcoming_games = [game for game in upcoming_games - if game.get('is_upcoming', False) and - game.get('start_time_utc') and - (game['start_time_utc'] - now).total_seconds() < 604800] - - if not upcoming_games: - self.logger.debug("No upcoming MLB games in next 7 days") - return False - - # Display each game - for game in upcoming_games: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying upcoming MLB games: {e}") - return False diff --git a/src/ncaa_baseball_managers.py b/src/ncaa_baseball_managers.py index 9e8fa782f..40f11eee4 100644 --- a/src/ncaa_baseball_managers.py +++ b/src/ncaa_baseball_managers.py @@ -13,27 +13,31 @@ from src.odds_manager import OddsManager import pytz +# Import baseball and standard sports classes +from .base_classes.baseball import Baseball, BaseballLive +from .base_classes.sports import SportsRecent, SportsUpcoming + # Get logger logger = logging.getLogger(__name__) # Constants for NCAA Baseball API URL ESPN_NCAABB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard" -class BaseNCAABaseballManager: - """Base class for NCAA Baseball managers with common functionality.""" +class BaseNCAABaseballManager(Baseball): + """Base class for NCAA Baseball managers using new baseball architecture.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - self.config = config - self.display_manager = display_manager - # Store reference to config instead of creating new ConfigManager - self.config_manager = None # Not used in this class + # Initialize with sport_key for NCAA Baseball + super().__init__(config, display_manager, cache_manager, logger, "ncaa_baseball") + + # NCAA Baseball-specific configuration self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {}) self.show_odds = self.ncaa_baseball_config.get('show_odds', False) self.show_records = self.ncaa_baseball_config.get('show_records', False) self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) - self.cache_manager = cache_manager + + # Store reference to config instead of creating new ConfigManager + self.config_manager = None # Not used in this class self.odds_manager = OddsManager(self.cache_manager, self.config_manager) - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) # Set logger level to DEBUG # Logo handling self.logo_dir = self.ncaa_baseball_config.get('logo_dir', os.path.join('assets', 'sports', 'ncaa_logos')) @@ -549,7 +553,7 @@ def _fetch_ncaa_baseball_api_data(self, use_cache: bool = True) -> Dict[str, Any self.logger.error(f"[NCAABaseball] Error fetching NCAA Baseball data from ESPN API: {e}", exc_info=True) return {} -class NCAABaseballLiveManager(BaseNCAABaseballManager): +class NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive): """Manager for displaying live NCAA Baseball games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) @@ -850,7 +854,7 @@ def display(self, force_clear: bool = False): except Exception as e: logger.error(f"[NCAABaseball] Error displaying live game: {e}", exc_info=True) -class NCAABaseballRecentManager(BaseNCAABaseballManager): +class NCAABaseballRecentManager(BaseNCAABaseballManager, SportsRecent): """Manager for displaying recent NCAA Baseball games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) @@ -974,7 +978,7 @@ def display(self, force_clear: bool = False): except Exception as e: logger.error(f"[NCAABaseball] Error displaying recent game: {e}", exc_info=True) -class NCAABaseballUpcomingManager(BaseNCAABaseballManager): +class NCAABaseballUpcomingManager(BaseNCAABaseballManager, SportsUpcoming): """Manager for displaying upcoming NCAA Baseball games.""" def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): super().__init__(config, display_manager, cache_manager) diff --git a/src/ncaa_baseball_managers_v2.py b/src/ncaa_baseball_managers_v2.py deleted file mode 100644 index cf03418f5..000000000 --- a/src/ncaa_baseball_managers_v2.py +++ /dev/null @@ -1,363 +0,0 @@ -""" -NCAA Baseball Managers - Updated to use new baseball base class - -This module demonstrates how to update existing baseball managers to use the new -baseball base class architecture while maintaining all existing functionality. -""" - -import time -import logging -import requests -import json -from typing import Dict, Any, List, Optional -from datetime import datetime, timedelta, timezone -import os -from PIL import Image, ImageDraw, ImageFont -import numpy as np -from .cache_manager import CacheManager -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry -from src.odds_manager import OddsManager -import pytz - -# Import new baseball base classes -from .base_classes.baseball import Baseball, BaseballLive, BaseballRecent, BaseballUpcoming - -# Get logger -logger = logging.getLogger(__name__) - -# Constants for NCAA Baseball API URL -ESPN_NCAABB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard" - -class BaseNCAABaseballManager(Baseball): - """Base class for NCAA Baseball managers using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - # Initialize with sport_key for NCAA Baseball - super().__init__(config, display_manager, cache_manager, logger, "ncaa_baseball") - - # NCAA Baseball-specific configuration - self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {}) - self.show_odds = self.ncaa_baseball_config.get('show_odds', False) - self.show_records = self.ncaa_baseball_config.get('show_records', False) - self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) - - # Logo handling - self.logo_dir = self.ncaa_baseball_config.get('logo_dir', os.path.join('assets', 'sports', 'ncaa_logos')) - if not os.path.exists(self.logo_dir): - self.logger.warning(f"NCAA Baseball logos directory not found: {self.logo_dir}") - try: - os.makedirs(self.logo_dir, exist_ok=True) - self.logger.info(f"Created NCAA Baseball logos directory: {self.logo_dir}") - except Exception as e: - self.logger.error(f"Failed to create NCAA Baseball logos directory: {e}") - - # Set up session with retry logic - self.session = requests.Session() - retry_strategy = Retry( - total=5, - backoff_factor=1, - status_forcelist=[429, 500, 502, 503, 504], - ) - adapter = HTTPAdapter(max_retries=retry_strategy) - self.session.mount("http://", adapter) - self.session.mount("https://", adapter) - - # Load fonts - self.fonts = self._load_fonts() - - # Initialize game tracking - self.live_games = [] - self.current_game = None - self.current_game_index = 0 - self.last_update = 0 - self.update_interval = self.ncaa_baseball_config.get('live_update_interval', 20) - self.no_data_interval = 300 - self.last_game_switch = 0 - self.game_display_duration = self.ncaa_baseball_config.get('live_game_duration', 30) - self.last_display_update = 0 - self.last_log_time = 0 - self.log_interval = 300 - self.last_count_log_time = 0 - self.count_log_interval = 5 - self.test_mode = self.ncaa_baseball_config.get('test_mode', False) - - def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: - """Load fonts for display.""" - fonts = {} - try: - # Load main font - font_path = os.path.join('assets', 'fonts', '5by7.regular.ttf') - if os.path.exists(font_path): - fonts['main'] = ImageFont.truetype(font_path, 8) - else: - fonts['main'] = ImageFont.load_default() - - # Load small font - fonts['small'] = ImageFont.load_default() - - return fonts - except Exception as e: - self.logger.error(f"Error loading fonts: {e}") - return {'main': ImageFont.load_default(), 'small': ImageFont.load_default()} - - def _get_team_logo(self, team_abbr: str) -> Optional[Image.Image]: - """Get team logo for display.""" - try: - logo_path = os.path.join(self.logo_dir, f"{team_abbr}.png") - if os.path.exists(logo_path): - return Image.open(logo_path) - return None - except Exception as e: - self.logger.error(f"Error loading logo for {team_abbr}: {e}") - return None - - def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): - """Draw text with outline for better visibility.""" - x, y = position - for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: - draw.text((x + dx, y + dy), text, font=font, fill=outline_color) - draw.text((x, y), text, font=font, fill=fill) - - def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None: - """Draw base indicators for baseball.""" - base_size = 3 - base_spacing = 8 - - # Draw bases in diamond formation - base_positions = [ - (center_x, y - base_spacing), # 1st base - (center_x + base_spacing, y), # 2nd base - (center_x, y + base_spacing), # 3rd base - (center_x - base_spacing, y) # Home plate - ] - - for i, (pos, occupied) in enumerate(zip(base_positions, bases_occupied)): - color = (255, 255, 0) if occupied else (128, 128, 128) - draw.ellipse([pos[0] - base_size, pos[1] - base_size, - pos[0] + base_size, pos[1] + base_size], fill=color) - - def _create_game_display(self, game_data: Dict[str, Any]) -> Image.Image: - """Create display image for a game.""" - try: - # Create image - width = self.display_manager.matrix.width - height = self.display_manager.matrix.height - image = Image.new('RGB', (width, height), (0, 0, 0)) - draw = ImageDraw.Draw(image) - - # Get game details - home_team = game_data.get('home_team', {}) - away_team = game_data.get('away_team', {}) - home_score = game_data.get('home_score', '0') - away_score = game_data.get('away_score', '0') - - # Get baseball-specific details - inning = game_data.get('inning', '') - outs = game_data.get('outs', 0) - bases = game_data.get('bases', '') - strikes = game_data.get('strikes', 0) - balls = game_data.get('balls', 0) - - # Draw team names and scores - font = self.fonts['main'] - y_offset = 10 - - # Away team - away_text = f"{away_team.get('abbreviation', 'AWAY')} {away_score}" - draw.text((5, y_offset), away_text, font=font, fill=(255, 255, 255)) - - # Home team - home_text = f"{home_team.get('abbreviation', 'HOME')} {home_score}" - draw.text((5, y_offset + 15), home_text, font=font, fill=(255, 255, 255)) - - # Baseball-specific details - if inning: - inning_text = f"Inning: {inning}" - draw.text((5, y_offset + 30), inning_text, font=font, fill=(255, 255, 255)) - - if outs is not None: - outs_text = f"Outs: {outs}" - draw.text((5, y_offset + 45), outs_text, font=font, fill=(255, 255, 255)) - - if strikes is not None and balls is not None: - count_text = f"Count: {balls}-{strikes}" - draw.text((5, y_offset + 60), count_text, font=font, fill=(255, 255, 255)) - - return image - - except Exception as e: - self.logger.error(f"Error creating game display: {e}") - return Image.new('RGB', (self.display_manager.matrix.width, self.display_manager.matrix.height), (0, 0, 0)) - - def _fetch_ncaa_baseball_api_data(self, use_cache: bool = True) -> Dict[str, Any]: - """Fetch NCAA Baseball data from ESPN API.""" - try: - # This would implement the actual NCAA Baseball API fetching - # For now, return empty data - return {} - except Exception as e: - self.logger.error(f"Error fetching NCAA Baseball data: {e}") - return {} - - def _is_baseball_game_live(self, game: Dict) -> bool: - """Check if a baseball game is currently live.""" - return super()._is_baseball_game_live(game) - - def _get_baseball_game_status(self, game: Dict) -> str: - """Get baseball-specific game status.""" - return super()._get_baseball_game_status(game) - - -class NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive): - """Manager for live NCAA Baseball games using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("NCAA Baseball Live Manager initialized with new baseball architecture") - - def get_duration(self) -> int: - """Get display duration for live NCAA Baseball games.""" - return self.ncaa_baseball_config.get('live_game_duration', 30) - - def display(self, force_clear: bool = False) -> bool: - """Display live NCAA Baseball games.""" - try: - # Fetch live games using the new architecture - live_games = self._fetch_immediate_games() - - if not live_games: - self.logger.warning("No live NCAA Baseball games found") - return False - - # Filter games based on criteria - games_to_show = [game for game in live_games if self._should_show_baseball_game(game)] - - if not games_to_show: - self.logger.debug("No NCAA Baseball games meet display criteria") - return False - - # Display each game - for game in games_to_show: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying live NCAA Baseball games: {e}") - return False - - def _display_single_game(self, game: Dict) -> None: - """Display a single NCAA Baseball game.""" - try: - # Get game details - home_team = game.get('home_team_name', '') - away_team = game.get('away_team_name', '') - home_score = game.get('home_score', '0') - away_score = game.get('away_score', '0') - status = game.get('status_text', '') - - # Get baseball-specific display text - baseball_text = self._get_baseball_display_text(game) - - # Create display text - display_text = f"{away_team} {away_score} @ {home_team} {home_score}" - if status: - display_text += f" - {status}" - if baseball_text: - display_text += f" ({baseball_text})" - - # Display the text - self.display_manager.display_text(display_text) - - except Exception as e: - self.logger.error(f"Error displaying single NCAA Baseball game: {e}") - - -class NCAABaseballRecentManager(BaseNCAABaseballManager, BaseballRecent): - """Manager for recent NCAA Baseball games using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("NCAA Baseball Recent Manager initialized with new baseball architecture") - - def get_duration(self) -> int: - """Get display duration for recent NCAA Baseball games.""" - return self.ncaa_baseball_config.get('recent_game_duration', 20) - - def display(self, force_clear: bool = False) -> bool: - """Display recent NCAA Baseball games.""" - try: - # Fetch recent games using the new architecture - recent_games = self._get_partial_schedule_data(datetime.now().year) - - if not recent_games: - self.logger.warning("No recent NCAA Baseball games found") - return False - - # Filter for recent games (last 24 hours) - now = datetime.now(pytz.UTC) - recent_games = [game for game in recent_games - if game.get('is_final', False) and - game.get('start_time_utc') and - (now - game['start_time_utc']).total_seconds() < 86400] - - if not recent_games: - self.logger.debug("No recent NCAA Baseball games in last 24 hours") - return False - - # Display each game - for game in recent_games: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying recent NCAA Baseball games: {e}") - return False - - -class NCAABaseballUpcomingManager(BaseNCAABaseballManager, BaseballUpcoming): - """Manager for upcoming NCAA Baseball games using new baseball architecture.""" - - def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): - super().__init__(config, display_manager, cache_manager) - self.logger.info("NCAA Baseball Upcoming Manager initialized with new baseball architecture") - - def get_duration(self) -> int: - """Get display duration for upcoming NCAA Baseball games.""" - return self.ncaa_baseball_config.get('upcoming_game_duration', 15) - - def display(self, force_clear: bool = False) -> bool: - """Display upcoming NCAA Baseball games.""" - try: - # Fetch upcoming games using the new architecture - upcoming_games = self._get_partial_schedule_data(datetime.now().year) - - if not upcoming_games: - self.logger.warning("No upcoming NCAA Baseball games found") - return False - - # Filter for upcoming games (next 7 days) - now = datetime.now(pytz.UTC) - upcoming_games = [game for game in upcoming_games - if game.get('is_upcoming', False) and - game.get('start_time_utc') and - (game['start_time_utc'] - now).total_seconds() < 604800] - - if not upcoming_games: - self.logger.debug("No upcoming NCAA Baseball games in next 7 days") - return False - - # Display each game - for game in upcoming_games: - self._display_single_game(game) - time.sleep(2) - - return True - - except Exception as e: - self.logger.error(f"Error displaying upcoming NCAA Baseball games: {e}") - return False diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index c9ce3d46d..f909532a2 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -100,18 +100,18 @@ def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]: datestring = f"{season_year}0801-{season_year+1}0201" cache_key = f"ncaafb_schedule_{season_year}" - if use_cache: + if use_cache: cached_data = self.cache_manager.get(cache_key) - if cached_data: + if cached_data: # Validate cached data structure if isinstance(cached_data, dict) and 'events' in cached_data: self.logger.info(f"Using cached schedule for {season_year}") - return cached_data + return cached_data elif isinstance(cached_data, list): # Handle old cache format (list of events) self.logger.info(f"Using cached schedule for {season_year} (legacy format)") return {'events': cached_data} - else: + else: self.logger.warning(f"Invalid cached data format for {season_year}: {type(cached_data)}") # Clear invalid cache self.cache_manager.clear_cache(cache_key) @@ -163,7 +163,7 @@ def fetch_callback(result): partial_data = self._get_weeks_data("college-football") if partial_data: return partial_data - return None + return None def _fetch_ncaa_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]: """ @@ -187,13 +187,13 @@ def _fetch_ncaa_api_data_sync(self, use_cache: bool = True) -> Optional[Dict]: return {'events': events} except requests.exceptions.RequestException as e: self.logger.error(f"[API error fetching full schedule: {e}") - return None + return None def _fetch_data(self) -> Optional[Dict]: """Fetch data using shared data mechanism or direct fetch for live.""" if isinstance(self, NCAAFBLiveManager): return self._fetch_todays_games("college-football") - else: + else: return self._fetch_ncaa_fb_api_data(use_cache=True) From 0e5e67e3de99919b55ef75a478157761462e794c Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:45:57 -0400 Subject: [PATCH 10/11] fix image call --- src/ncaa_fb_managers.py | 5 ++--- src/ncaam_hockey_managers.py | 5 ++--- src/nfl_managers.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index f909532a2..be613cc06 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -29,9 +29,8 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cach self.logger = logging.getLogger('NCAAFB') # Changed logger name super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaa_fb") - # Override configuration with sport-specific settings - self.logo_dir = Path(self.sport_config.logo_dir) - self.update_interval = self.sport_config.get_update_interval() + # Configuration is already set in base class + # self.logo_dir and self.update_interval are already configured # Check display modes to determine what data to fetch display_modes = self.mode_config.get("display_modes", {}) diff --git a/src/ncaam_hockey_managers.py b/src/ncaam_hockey_managers.py index 99fe60b46..e9edc5c33 100644 --- a/src/ncaam_hockey_managers.py +++ b/src/ncaam_hockey_managers.py @@ -34,9 +34,8 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cach self.logger = logging.getLogger('NCAAMH') # Changed logger name super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="ncaam_hockey") - # Override configuration with sport-specific settings - self.logo_dir = Path(self.sport_config.logo_dir) - self.update_interval = self.sport_config.get_update_interval() + # Configuration is already set in base class + # self.logo_dir and self.update_interval are already configured # Check display modes to determine what data to fetch display_modes = self.mode_config.get("display_modes", {}) diff --git a/src/nfl_managers.py b/src/nfl_managers.py index c85e90ee6..917cf42fd 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -26,9 +26,8 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cach self.logger = logging.getLogger('NFL') # Changed logger name super().__init__(config=config, display_manager=display_manager, cache_manager=cache_manager, logger=self.logger, sport_key="nfl") - # Override configuration with sport-specific settings - self.logo_dir = Path(self.sport_config.logo_dir) - self.update_interval = self.sport_config.get_update_interval() + # Configuration is already set in base class + # self.logo_dir and self.update_interval are already configured # Check display modes to determine what data to fetch display_modes = self.mode_config.get("display_modes", {}) From e191ae110c25d01a4f60b687b8215ea243792d4b Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:22:33 -0400 Subject: [PATCH 11/11] fix _scoreboard suffix on milb, MLB --- config/config.template.json | 4 ++-- src/cache_manager.py | 13 ++++--------- src/display_controller.py | 26 +++++++++++++------------- src/milb_manager.py | 2 +- src/mlb_manager.py | 2 +- src/odds_ticker_manager.py | 8 ++++---- test/diagnose_milb_issues.py | 2 +- test/test_games_to_show_config.py | 8 ++++---- test/test_milb_data_accuracy.py | 2 +- test_config_loading.py | 1 + test_config_simple.py | 1 + test_config_validation.py | 1 + 12 files changed, 34 insertions(+), 36 deletions(-) create mode 100644 test_config_loading.py create mode 100644 test_config_simple.py create mode 100644 test_config_validation.py diff --git a/config/config.template.json b/config/config.template.json index fd599ceb9..2950dcb8e 100644 --- a/config/config.template.json +++ b/config/config.template.json @@ -428,7 +428,7 @@ "enabled": false, "update_interval": 3600 }, - "mlb": { + "mlb_scoreboard": { "enabled": false, "live_priority": false, "live_game_duration": 30, @@ -455,7 +455,7 @@ "mlb_upcoming": true } }, - "milb": { + "milb_scoreboard": { "enabled": false, "live_priority": false, "live_game_duration": 30, diff --git a/src/cache_manager.py b/src/cache_manager.py index 9dad671cf..9c4484ae6 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -516,11 +516,8 @@ def get_sport_live_interval(self, sport_key: str) -> int: try: config = self.config_manager.config - # For MiLB, look for "milb" config instead of "milb_scoreboard" - if sport_key == 'milb': - sport_config = config.get("milb", {}) - else: - sport_config = config.get(f"{sport_key}_scoreboard", {}) + # 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 Exception as e: self.logger.warning(f"Could not get live_update_interval for {sport_key}: {e}") @@ -541,10 +538,8 @@ def get_cache_strategy(self, data_type: str, sport_key: str = None) -> Dict[str, upcoming_interval = None if self.config_manager and sport_key: try: - if sport_key == 'milb': - sport_cfg = self.config_manager.config.get('milb', {}) - else: - sport_cfg = self.config_manager.config.get(f"{sport_key}_scoreboard", {}) + # 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 Exception as e: diff --git a/src/display_controller.py b/src/display_controller.py index 29f621ea8..9a55df8ee 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -133,8 +133,8 @@ def __init__(self): # Initialize MLB managers if enabled mlb_time = time.time() - mlb_enabled = self.config.get('mlb', {}).get('enabled', False) - mlb_display_modes = self.config.get('mlb', {}).get('display_modes', {}) + mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False) + mlb_display_modes = self.config.get('mlb_scoreboard', {}).get('display_modes', {}) if mlb_enabled: self.mlb_live = MLBLiveManager(self.config, self.display_manager, self.cache_manager) if mlb_display_modes.get('mlb_live', True) else None @@ -148,8 +148,8 @@ def __init__(self): # Initialize MiLB managers if enabled milb_time = time.time() - milb_enabled = self.config.get('milb', {}).get('enabled', False) - milb_display_modes = self.config.get('milb', {}).get('display_modes', {}) + milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False) + milb_display_modes = self.config.get('milb_scoreboard', {}).get('display_modes', {}) if milb_enabled: self.milb_live = MiLBLiveManager(self.config, self.display_manager, self.cache_manager) if milb_display_modes.get('milb_live', True) else None @@ -256,14 +256,14 @@ def __init__(self): # Track MLB rotation state self.mlb_current_team_index = 0 self.mlb_showing_recent = True - self.mlb_favorite_teams = self.config.get('mlb', {}).get('favorite_teams', []) + self.mlb_favorite_teams = self.config.get('mlb_scoreboard', {}).get('favorite_teams', []) self.in_mlb_rotation = False # Read live_priority flags for all sports self.nhl_live_priority = self.config.get('nhl_scoreboard', {}).get('live_priority', True) self.nba_live_priority = self.config.get('nba_scoreboard', {}).get('live_priority', True) - self.mlb_live_priority = self.config.get('mlb', {}).get('live_priority', True) - self.milb_live_priority = self.config.get('milb', {}).get('live_priority', True) + self.mlb_live_priority = self.config.get('mlb_scoreboard', {}).get('live_priority', True) + self.milb_live_priority = self.config.get('milb_scoreboard', {}).get('live_priority', True) self.soccer_live_priority = self.config.get('soccer_scoreboard', {}).get('live_priority', True) self.nfl_live_priority = self.config.get('nfl_scoreboard', {}).get('live_priority', True) self.ncaa_fb_live_priority = self.config.get('ncaa_fb_scoreboard', {}).get('live_priority', True) @@ -438,7 +438,7 @@ def __init__(self): if mlb_enabled: logger.info(f"MLB Favorite teams: {self.mlb_favorite_teams}") if milb_enabled: - logger.info(f"MiLB Favorite teams: {self.config.get('milb', {}).get('favorite_teams', [])}") + logger.info(f"MiLB Favorite teams: {self.config.get('milb_scoreboard', {}).get('favorite_teams', [])}") if soccer_enabled: # Check if soccer is enabled logger.info(f"Soccer Favorite teams: {self.soccer_favorite_teams}") if nfl_enabled: # Check if NFL is enabled @@ -838,7 +838,7 @@ def _has_team_games(self, sport: str = 'nhl') -> bool: manager_recent = self.mlb_recent manager_upcoming = self.mlb_upcoming elif sport == 'milb': - favorite_teams = self.config.get('milb', {}).get('favorite_teams', []) + favorite_teams = self.config.get('milb_scoreboard', {}).get('favorite_teams', []) manager_recent = self.milb_recent manager_upcoming = self.milb_upcoming elif sport == 'soccer': @@ -876,8 +876,8 @@ def _rotate_team_games(self, sport: str = 'nhl') -> None: current_team = self.mlb_favorite_teams[self.mlb_current_team_index] # ... (rest of MLB rotation logic) elif sport == 'milb': - if not self.config.get('milb', {}).get('favorite_teams', []): return - current_team = self.config['milb']['favorite_teams'][self.milb_current_team_index] + if not self.config.get('milb_scoreboard', {}).get('favorite_teams', []): return + current_team = self.config['milb_scoreboard']['favorite_teams'][self.milb_current_team_index] # ... (rest of MiLB rotation logic) elif sport == 'soccer': if not self.soccer_favorite_teams: return @@ -992,8 +992,8 @@ def update_mode(mode_name, manager, live_priority, sport_enabled): # Check if each sport is enabled before processing nhl_enabled = self.config.get('nhl_scoreboard', {}).get('enabled', False) nba_enabled = self.config.get('nba_scoreboard', {}).get('enabled', False) - mlb_enabled = self.config.get('mlb', {}).get('enabled', False) - milb_enabled = self.config.get('milb', {}).get('enabled', False) + mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False) + milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False) soccer_enabled = self.config.get('soccer_scoreboard', {}).get('enabled', False) nfl_enabled = self.config.get('nfl_scoreboard', {}).get('enabled', False) ncaa_fb_enabled = self.config.get('ncaa_fb_scoreboard', {}).get('enabled', False) diff --git a/src/milb_manager.py b/src/milb_manager.py index d3c12c2ae..1b692df14 100644 --- a/src/milb_manager.py +++ b/src/milb_manager.py @@ -35,7 +35,7 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager: Cache super().__init__(config, display_manager, cache_manager, logger, "milb") # MiLB-specific configuration - self.milb_config = config.get('milb', {}) + self.milb_config = config.get('milb_scoreboard', {}) self.favorite_teams = self.milb_config.get('favorite_teams', []) self.show_records = self.milb_config.get('show_records', False) diff --git a/src/mlb_manager.py b/src/mlb_manager.py index 4885a6f56..42867d850 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -36,7 +36,7 @@ def __init__(self, config: Dict[str, Any], display_manager, cache_manager: Cache super().__init__(config, display_manager, cache_manager, logger, "mlb") # MLB-specific configuration - self.mlb_config = config.get('mlb', {}) + self.mlb_config = config.get('mlb_scoreboard', {}) self.show_odds = self.mlb_config.get("show_odds", False) self.favorite_teams = self.mlb_config.get('favorite_teams', []) self.show_records = self.mlb_config.get('show_records', False) diff --git a/src/odds_ticker_manager.py b/src/odds_ticker_manager.py index aca383cba..8154586d9 100644 --- a/src/odds_ticker_manager.py +++ b/src/odds_ticker_manager.py @@ -162,8 +162,8 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): 'league': 'mlb', 'logo_league': 'mlb', # ESPN API league identifier for logo downloading 'logo_dir': 'assets/sports/mlb_logos', - 'favorite_teams': config.get('mlb', {}).get('favorite_teams', []), - 'enabled': config.get('mlb', {}).get('enabled', False) + 'favorite_teams': config.get('mlb_scoreboard', {}).get('favorite_teams', []), + 'enabled': config.get('mlb_scoreboard', {}).get('enabled', False) }, 'ncaa_fb': { 'sport': 'football', @@ -178,8 +178,8 @@ def __init__(self, config: Dict[str, Any], display_manager: DisplayManager): 'league': 'milb', 'logo_league': 'milb', # ESPN API league identifier for logo downloading (if supported) 'logo_dir': 'assets/sports/milb_logos', - 'favorite_teams': config.get('milb', {}).get('favorite_teams', []), - 'enabled': config.get('milb', {}).get('enabled', False) + 'favorite_teams': config.get('milb_scoreboard', {}).get('favorite_teams', []), + 'enabled': config.get('milb_scoreboard', {}).get('enabled', False) }, 'nhl': { 'sport': 'hockey', diff --git a/test/diagnose_milb_issues.py b/test/diagnose_milb_issues.py index 550f65ce4..429b035e9 100644 --- a/test/diagnose_milb_issues.py +++ b/test/diagnose_milb_issues.py @@ -184,7 +184,7 @@ def test_configuration(): with open(config_path, 'r') as f: config = json.load(f) - milb_config = config.get('milb', {}) + milb_config = config.get('milb_scoreboard', {}) print(f"✅ Configuration file loaded successfully") print(f"MiLB enabled: {milb_config.get('enabled', False)}") diff --git a/test/test_games_to_show_config.py b/test/test_games_to_show_config.py index 5fc6b3435..d12d147f7 100644 --- a/test/test_games_to_show_config.py +++ b/test/test_games_to_show_config.py @@ -31,8 +31,8 @@ def test_config_values(): ("NCAA Football", config.get('ncaa_fb_scoreboard', {})), ("NCAA Baseball", config.get('ncaa_baseball_scoreboard', {})), ("NCAA Basketball", config.get('ncaam_basketball_scoreboard', {})), - ("MLB", config.get('mlb', {})), - ("MiLB", config.get('milb', {})), + ("MLB", config.get('mlb_scoreboard', {})), + ("MiLB", config.get('milb_scoreboard', {})), ("Soccer", config.get('soccer_scoreboard', {})) ] @@ -84,8 +84,8 @@ def test_config_consistency(): ("NCAA Football", config.get('ncaa_fb_scoreboard', {})), ("NCAA Baseball", config.get('ncaa_baseball_scoreboard', {})), ("NCAA Basketball", config.get('ncaam_basketball_scoreboard', {})), - ("MLB", config.get('mlb', {})), - ("MiLB", config.get('milb', {})), + ("MLB", config.get('mlb_scoreboard', {})), + ("MiLB", config.get('milb_scoreboard', {})), ("Soccer", config.get('soccer_scoreboard', {})) ] diff --git a/test/test_milb_data_accuracy.py b/test/test_milb_data_accuracy.py index c2e1d18f2..6d409591f 100644 --- a/test/test_milb_data_accuracy.py +++ b/test/test_milb_data_accuracy.py @@ -23,7 +23,7 @@ def test_milb_api_accuracy(): try: with open('config/config.json', 'r') as f: config = json.load(f) - milb_config = config.get('milb', {}) + milb_config = config.get('milb_scoreboard', {}) favorite_teams = milb_config.get('favorite_teams', []) print(f"Favorite teams: {favorite_teams}") except Exception as e: diff --git a/test_config_loading.py b/test_config_loading.py new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/test_config_loading.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_config_simple.py b/test_config_simple.py new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/test_config_simple.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_config_validation.py b/test_config_validation.py new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/test_config_validation.py @@ -0,0 +1 @@ + \ No newline at end of file