diff --git a/.cursor/rules/github-branches-rule.mdc b/.cursor/rules/github-branches-rule.mdc new file mode 100644 index 00000000..87b1ea5f --- /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/config/config.template.json b/config/config.template.json index fd599ceb..2950dcb8 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/base_classes/api_extractors.py b/src/base_classes/api_extractors.py new file mode 100644 index 00000000..93d7a376 --- /dev/null +++ b/src/base_classes/api_extractors.py @@ -0,0 +1,363 @@ +""" +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 {} + + +# Factory function removed - sport classes now instantiate extractors directly diff --git a/src/base_classes/baseball.py b/src/base_classes/baseball.py new file mode 100644 index 00000000..56ffd78e --- /dev/null +++ b/src/base_classes/baseball.py @@ -0,0 +1,165 @@ +""" +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 +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) + 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_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: + 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 + + diff --git a/src/base_classes/data_sources.py b/src/base_classes/data_sources.py new file mode 100644 index 00000000..6132b536 --- /dev/null +++ b/src/base_classes/data_sources.py @@ -0,0 +1,288 @@ +""" +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 {} + + +# 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 a52b2222..cf67ca6c 100644 --- a/src/base_classes/football.py +++ b/src/base_classes/football.py @@ -7,11 +7,40 @@ from PIL import Image, ImageDraw, ImageFont 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 31b4938a..c5743471 100644 --- a/src/base_classes/hockey.py +++ b/src/base_classes/hockey.py @@ -6,10 +6,39 @@ import logging 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/sports.py b/src/base_classes/sports.py index 5b87b906..9cd9d420 100644 --- a/src/base_classes/sports.py +++ b/src/base_classes/sports.py @@ -15,6 +15,10 @@ from src.background_data_service import get_background_service from src.logo_downloader import download_missing_logo, LogoDownloader from pathlib import Path +# 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): self.logger = logger @@ -28,6 +32,11 @@ class SportsCore: self.display_height = self.display_manager.matrix.height self.sport_key = sport_key + + # 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) @@ -267,20 +276,143 @@ class SportsCore: 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 +471,26 @@ class SportsCore: 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: @@ -393,7 +544,7 @@ class SportsCore: # 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/cache_manager.py b/src/cache_manager.py index 93ac7741..9c4484ae 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -308,14 +308,19 @@ class CacheManager: 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.""" @@ -511,11 +516,8 @@ class CacheManager: 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}") @@ -536,10 +538,8 @@ class CacheManager: 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 5244c0ec..9a55df8e 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -133,8 +133,8 @@ class DisplayController: # 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 @@ class DisplayController: # 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 @@ class DisplayController: # 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 @@ class DisplayController: 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 @@ -541,19 +541,20 @@ class DisplayController: # 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 @@ class DisplayController: # 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 @@ class DisplayController: 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() @@ -824,7 +838,7 @@ class DisplayController: 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': @@ -862,8 +876,8 @@ class DisplayController: 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 @@ -978,8 +992,8 @@ class DisplayController: # 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) @@ -1006,8 +1020,10 @@ class DisplayController: 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: diff --git a/src/leaderboard_manager.py b/src/leaderboard_manager.py index bb37612e..f2ab9b83 100644 --- a/src/leaderboard_manager.py +++ b/src/leaderboard_manager.py @@ -40,7 +40,7 @@ class LeaderboardManager: 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 @@ class LeaderboardManager: 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 @@ -1234,6 +1240,13 @@ class LeaderboardManager: 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() @@ -1329,20 +1342,31 @@ class LeaderboardManager: 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 - if should_scroll: - self.scroll_position += self.scroll_speed - self.last_scroll_time = current_time + # Scroll the image every frame for smooth animation + self.scroll_position += self.scroll_speed + + # Add scroll delay like other managers for consistent timing + time.sleep(self.scroll_delay) # Calculate crop region width = self.display_manager.matrix.width diff --git a/src/milb_manager.py b/src/milb_manager.py index d7b418aa..1b692df1 100644 --- a/src/milb_manager.py +++ b/src/milb_manager.py @@ -14,6 +14,10 @@ from urllib3.util.retry import Retry 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 @@ except ImportError: # 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 - self.milb_config = config.get('milb', {}) + # Initialize with sport_key for MiLB + super().__init__(config, display_manager, cache_manager, logger, "milb") + + # MiLB-specific configuration + 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) - 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 @@ class BaseMiLBManager: 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 @@ class MiLBLiveManager(BaseMiLBManager): 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 @@ class MiLBRecentManager(BaseMiLBManager): 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/mlb_manager.py b/src/mlb_manager.py index 762154b4..42867d85 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -14,6 +14,10 @@ import pytz 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 @@ except ImportError: # 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 - self.mlb_config = config.get('mlb', {}) + # Initialize with sport_key for MLB + super().__init__(config, display_manager, cache_manager, logger, "mlb") + + # MLB-specific configuration + 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) - 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 @@ class BaseMLBManager: 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 @@ class MLBLiveManager(BaseMLBManager): 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 @@ class MLBRecentManager(BaseMLBManager): 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/ncaa_baseball_managers.py b/src/ncaa_baseball_managers.py index 9e8fa782..40f11eee 100644 --- a/src/ncaa_baseball_managers.py +++ b/src/ncaa_baseball_managers.py @@ -13,27 +13,31 @@ from urllib3.util.retry import Retry 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 @@ class BaseNCAABaseballManager: 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 @@ class NCAABaseballLiveManager(BaseNCAABaseballManager): 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 @@ class NCAABaseballRecentManager(BaseNCAABaseballManager): 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_fb_managers.py b/src/ncaa_fb_managers.py index 9eb11a3b..be613cc0 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -28,6 +28,9 @@ 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") + + # 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", {}) @@ -140,7 +143,7 @@ class BaseNCAAFBManager(Football): # Renamed class # 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, @@ -184,7 +187,7 @@ class BaseNCAAFBManager(Football): # Renamed class except requests.exceptions.RequestException as e: self.logger.error(f"[API error fetching full schedule: {e}") return None - + def _fetch_data(self) -> Optional[Dict]: """Fetch data using shared data mechanism or direct fetch for live.""" if isinstance(self, NCAAFBLiveManager): diff --git a/src/ncaam_hockey_managers.py b/src/ncaam_hockey_managers.py index d533ffa5..e9edc5c3 100644 --- a/src/ncaam_hockey_managers.py +++ b/src/ncaam_hockey_managers.py @@ -33,6 +33,9 @@ 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") + + # 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 2cecc08d..917cf42f 100644 --- a/src/nfl_managers.py +++ b/src/nfl_managers.py @@ -25,6 +25,9 @@ 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") + + # 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/odds_ticker_manager.py b/src/odds_ticker_manager.py index aca383cb..8154586d 100644 --- a/src/odds_ticker_manager.py +++ b/src/odds_ticker_manager.py @@ -162,8 +162,8 @@ class OddsTickerManager: '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 @@ class OddsTickerManager: '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 550f65ce..429b035e 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_baseball_architecture.py b/test/test_baseball_architecture.py new file mode 100644 index 00000000..bcec37c6 --- /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_baseball_managers_integration.py b/test/test_baseball_managers_integration.py new file mode 100644 index 00000000..e09819a1 --- /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 00000000..26d3aec9 --- /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) diff --git a/test/test_games_to_show_config.py b/test/test_games_to_show_config.py index 5fc6b343..d12d147f 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_leaderboard_duration_fix.py b/test/test_leaderboard_duration_fix.py new file mode 100644 index 00000000..ad788734 --- /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) diff --git a/test/test_milb_data_accuracy.py b/test/test_milb_data_accuracy.py index c2e1d18f..6d409591 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/test_new_architecture.py b/test/test_new_architecture.py new file mode 100644 index 00000000..2e5804fc --- /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 00000000..603f411f --- /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) diff --git a/test_config_loading.py b/test_config_loading.py new file mode 100644 index 00000000..0519ecba --- /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 00000000..0519ecba --- /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 00000000..0519ecba --- /dev/null +++ b/test_config_validation.py @@ -0,0 +1 @@ + \ No newline at end of file