Fix leaderboard scrolling performance after PR #39 merge (#63)

* Fix leaderboard scrolling performance after PR #39 merge

- Restore leaderboard background updates that were accidentally removed
- Fix duration method call from get_dynamic_duration() back to get_duration()
- Restore proper fallback duration (600s instead of 60s) for leaderboard
- Add back sports manager updates that feed data to leaderboard
- Fix leaderboard defer_update priority to prevent scrolling lag

These changes restore the leaderboard's dynamic duration calculation
and ensure it gets proper background updates for smooth scrolling.

* Apply PR #60 leaderboard performance optimizations

- Change scroll_delay from 0.05s to 0.01s (100fps instead of 20fps)
- Remove conditional scrolling logic - scroll every frame for smooth animation
- Add FPS tracking and logging for performance monitoring
- Restore high-framerate scrolling that was working before PR #39 merge

These changes restore the smooth leaderboard scrolling performance
that was achieved in PR #60 but was lost during the PR #39 merge.

* Fix critical bugs identified in PR #39 review

- Fix record filtering logic bug: change away_record == set to away_record in set
- Fix incorrect sport specification: change 'nfl' to 'ncaa_fb' for NCAA Football data requests
- These bugs were causing incorrect data display and wrong sport data fetching

Addresses issues found by cursor bot in PR #39 review:
- Record filtering was always evaluating to False
- NCAA Football was fetching NFL data instead of college football data

* Enhance cache clearing implementation from PR #39

- Add detailed logging to cache clearing process for better visibility
- Log cache clearing statistics (memory entries and file count)
- Improve startup logging to show cache clearing and data refetch process
- Addresses legoguy1000's comment about preventing stale data issues

This enhances the cache clearing implementation that was added in PR #39
to help prevent legacy cache issues and stale data problems.

* continuing on base_classes - added baseball and api extractor since we don't use ESPN api for all sports

* tests

* fix missing duration

* ensure milb, mlb, ncaa bb are all using new baseball base class properly

* cursor rule to help with PR creation

* fix image call

* fix _scoreboard suffix on milb, MLB
This commit is contained in:
Chuck
2025-09-25 09:34:20 -04:00
committed by GitHub
parent 76a9e98ba7
commit ad8a652183
30 changed files with 2821 additions and 102 deletions

View File

@@ -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

View File

@@ -428,7 +428,7 @@
"enabled": false, "enabled": false,
"update_interval": 3600 "update_interval": 3600
}, },
"mlb": { "mlb_scoreboard": {
"enabled": false, "enabled": false,
"live_priority": false, "live_priority": false,
"live_game_duration": 30, "live_game_duration": 30,
@@ -455,7 +455,7 @@
"mlb_upcoming": true "mlb_upcoming": true
} }
}, },
"milb": { "milb_scoreboard": {
"enabled": false, "enabled": false,
"live_priority": false, "live_priority": false,
"live_game_duration": 30, "live_game_duration": 30,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -7,12 +7,41 @@ from PIL import Image, ImageDraw, ImageFont
import time import time
import pytz import pytz
from src.base_classes.sports import SportsCore 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 import requests
class Football(SportsCore): 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): 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) 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: def _fetch_game_odds(self, _: Dict) -> None:
pass pass

View File

@@ -6,11 +6,40 @@ import logging
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import time import time
from src.base_classes.sports import SportsCore 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): 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): 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) 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: def _fetch_odds(self, game: Dict, league: str) -> None:
super()._fetch_odds(game, "hockey", league) super()._fetch_odds(game, "hockey", league)

View File

@@ -15,6 +15,10 @@ from src.background_data_service import get_background_service
from src.logo_downloader import download_missing_logo, LogoDownloader from src.logo_downloader import download_missing_logo, LogoDownloader
from pathlib import Path 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: class SportsCore:
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str): def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str):
self.logger = logger self.logger = logger
@@ -28,6 +32,11 @@ class SportsCore:
self.display_height = self.display_manager.matrix.height self.display_height = self.display_manager.matrix.height
self.sport_key = sport_key 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.mode_config = config.get(f"{sport_key}_scoreboard", {}) # Changed config key
self.is_enabled = self.mode_config.get("enabled", False) self.is_enabled = self.mode_config.get("enabled", False)
self.show_odds = self.mode_config.get("show_odds", False) self.show_odds = self.mode_config.get("show_odds", False)
@@ -267,20 +276,143 @@ class SportsCore:
return None return None
def _fetch_data(self) -> Optional[Dict]: def _fetch_data(self) -> Optional[Dict]:
"""Override this from the sports class""" """Fetch data using the new architecture components."""
pass 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]: def _get_partial_schedule_data(self, year: int) -> List[Dict]:
"""Override this from the sports class""" """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 [] return []
def _fetch_immediate_games(self) -> List[Dict]: def _fetch_immediate_games(self) -> List[Dict]:
"""Override this from the sports class""" """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 [] return []
def _fetch_game_odds(self, _: Dict) -> None: # Use the API extractor to process each game
"""Override this from the sports class""" processed_games = []
pass 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, 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: def _fetch_odds(self, game: Dict, sport: str, league: str) -> None:
"""Fetch odds for a specific game if conditions are met.""" """Fetch odds for a specific game if conditions are met."""
@@ -339,6 +471,25 @@ class SportsCore:
return False return False
def _fetch_team_rankings(self) -> Dict[str, int]: def _fetch_team_rankings(self) -> Dict[str, int]:
"""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 {} return {}
def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]: def _extract_game_details_common(self, game_event: Dict) -> tuple[Dict | None, Dict | None, Dict | None, Dict | None, Dict | None]:
@@ -393,7 +544,7 @@ class SportsCore:
# Don't show "0-0" records - set to blank instead # Don't show "0-0" records - set to blank instead
if home_record in {"0-0", "0-0-0"}: if home_record in {"0-0", "0-0-0"}:
home_record = '' home_record = ''
if away_record == {"0-0", "0-0-0"}: if away_record in {"0-0", "0-0-0"}:
away_record = '' away_record = ''
details = { details = {

View File

@@ -308,14 +308,19 @@ class CacheManager:
cache_path = self._get_cache_path(key) cache_path = self._get_cache_path(key)
if cache_path and os.path.exists(cache_path): if cache_path and os.path.exists(cache_path):
os.remove(cache_path) os.remove(cache_path)
self.logger.info(f"Cleared cache for key: {key}")
else: else:
# Clear all keys # Clear all keys
memory_count = len(self._memory_cache)
self._memory_cache.clear() self._memory_cache.clear()
self._memory_cache_timestamps.clear() self._memory_cache_timestamps.clear()
file_count = 0
if self.cache_dir: if self.cache_dir:
for file in os.listdir(self.cache_dir): for file in os.listdir(self.cache_dir):
if file.endswith('.json'): if file.endswith('.json'):
os.remove(os.path.join(self.cache_dir, file)) 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: def has_data_changed(self, data_type: str, new_data: Dict[str, Any]) -> bool:
"""Check if data has changed from cached version.""" """Check if data has changed from cached version."""
@@ -511,10 +516,7 @@ class CacheManager:
try: try:
config = self.config_manager.config config = self.config_manager.config
# For MiLB, look for "milb" config instead of "milb_scoreboard" # All sports now use _scoreboard suffix
if sport_key == 'milb':
sport_config = config.get("milb", {})
else:
sport_config = config.get(f"{sport_key}_scoreboard", {}) sport_config = config.get(f"{sport_key}_scoreboard", {})
return sport_config.get("live_update_interval", 60) # Default to 60 seconds return sport_config.get("live_update_interval", 60) # Default to 60 seconds
except Exception as e: except Exception as e:
@@ -536,9 +538,7 @@ class CacheManager:
upcoming_interval = None upcoming_interval = None
if self.config_manager and sport_key: if self.config_manager and sport_key:
try: try:
if sport_key == 'milb': # All sports now use _scoreboard suffix
sport_cfg = self.config_manager.config.get('milb', {})
else:
sport_cfg = self.config_manager.config.get(f"{sport_key}_scoreboard", {}) sport_cfg = self.config_manager.config.get(f"{sport_key}_scoreboard", {})
recent_interval = sport_cfg.get('recent_update_interval') recent_interval = sport_cfg.get('recent_update_interval')
upcoming_interval = sport_cfg.get('upcoming_update_interval') upcoming_interval = sport_cfg.get('upcoming_update_interval')

View File

@@ -133,8 +133,8 @@ class DisplayController:
# Initialize MLB managers if enabled # Initialize MLB managers if enabled
mlb_time = time.time() mlb_time = time.time()
mlb_enabled = self.config.get('mlb', {}).get('enabled', False) mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False)
mlb_display_modes = self.config.get('mlb', {}).get('display_modes', {}) mlb_display_modes = self.config.get('mlb_scoreboard', {}).get('display_modes', {})
if mlb_enabled: 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 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 # Initialize MiLB managers if enabled
milb_time = time.time() milb_time = time.time()
milb_enabled = self.config.get('milb', {}).get('enabled', False) milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False)
milb_display_modes = self.config.get('milb', {}).get('display_modes', {}) milb_display_modes = self.config.get('milb_scoreboard', {}).get('display_modes', {})
if milb_enabled: 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 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 # Track MLB rotation state
self.mlb_current_team_index = 0 self.mlb_current_team_index = 0
self.mlb_showing_recent = True 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 self.in_mlb_rotation = False
# Read live_priority flags for all sports # Read live_priority flags for all sports
self.nhl_live_priority = self.config.get('nhl_scoreboard', {}).get('live_priority', True) 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.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.mlb_live_priority = self.config.get('mlb_scoreboard', {}).get('live_priority', True)
self.milb_live_priority = self.config.get('milb', {}).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.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.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) self.ncaa_fb_live_priority = self.config.get('ncaa_fb_scoreboard', {}).get('live_priority', True)
@@ -438,7 +438,7 @@ class DisplayController:
if mlb_enabled: if mlb_enabled:
logger.info(f"MLB Favorite teams: {self.mlb_favorite_teams}") logger.info(f"MLB Favorite teams: {self.mlb_favorite_teams}")
if milb_enabled: 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 if soccer_enabled: # Check if soccer is enabled
logger.info(f"Soccer Favorite teams: {self.soccer_favorite_teams}") logger.info(f"Soccer Favorite teams: {self.soccer_favorite_teams}")
if nfl_enabled: # Check if NFL is enabled if nfl_enabled: # Check if NFL is enabled
@@ -541,19 +541,20 @@ class DisplayController:
# Fall back to configured duration # Fall back to configured duration
return self.display_durations.get(mode_key, 60) 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: elif mode_key == 'leaderboard' and self.leaderboard:
try: 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 # 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: if not hasattr(self, '_last_logged_leaderboard_duration') or self._last_logged_leaderboard_duration != duration:
logger.info(f"Using dynamic duration for leaderboard: {dynamic_duration} seconds") logger.info(f"Using leaderboard {mode_type} duration: {duration} seconds")
self._last_logged_leaderboard_duration = dynamic_duration self._last_logged_leaderboard_duration = duration
return dynamic_duration return duration
except Exception as e: 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 # 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 # Simplify weather key handling
elif mode_key.startswith('weather_'): elif mode_key.startswith('weather_'):
@@ -575,6 +576,8 @@ class DisplayController:
# Defer updates for modules that might cause lag during scrolling # Defer updates for modules that might cause lag during scrolling
if self.odds_ticker: if self.odds_ticker:
self.display_manager.defer_update(self.odds_ticker.update, priority=1) 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: if self.stocks:
self.display_manager.defer_update(self.stocks.update_stock_data, priority=2) self.display_manager.defer_update(self.stocks.update_stock_data, priority=2)
if self.news: if self.news:
@@ -609,6 +612,17 @@ class DisplayController:
if self.text_display: self.text_display.update() if self.text_display: self.text_display.update()
if self.of_the_day: self.of_the_day.update(time.time()) 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 # News manager fetches data when displayed, not during updates
# if self.news_manager: self.news_manager.fetch_news_data() # if self.news_manager: self.news_manager.fetch_news_data()
@@ -824,7 +838,7 @@ class DisplayController:
manager_recent = self.mlb_recent manager_recent = self.mlb_recent
manager_upcoming = self.mlb_upcoming manager_upcoming = self.mlb_upcoming
elif sport == 'milb': 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_recent = self.milb_recent
manager_upcoming = self.milb_upcoming manager_upcoming = self.milb_upcoming
elif sport == 'soccer': elif sport == 'soccer':
@@ -862,8 +876,8 @@ class DisplayController:
current_team = self.mlb_favorite_teams[self.mlb_current_team_index] current_team = self.mlb_favorite_teams[self.mlb_current_team_index]
# ... (rest of MLB rotation logic) # ... (rest of MLB rotation logic)
elif sport == 'milb': elif sport == 'milb':
if not self.config.get('milb', {}).get('favorite_teams', []): return if not self.config.get('milb_scoreboard', {}).get('favorite_teams', []): return
current_team = self.config['milb']['favorite_teams'][self.milb_current_team_index] current_team = self.config['milb_scoreboard']['favorite_teams'][self.milb_current_team_index]
# ... (rest of MiLB rotation logic) # ... (rest of MiLB rotation logic)
elif sport == 'soccer': elif sport == 'soccer':
if not self.soccer_favorite_teams: return if not self.soccer_favorite_teams: return
@@ -978,8 +992,8 @@ class DisplayController:
# Check if each sport is enabled before processing # Check if each sport is enabled before processing
nhl_enabled = self.config.get('nhl_scoreboard', {}).get('enabled', False) nhl_enabled = self.config.get('nhl_scoreboard', {}).get('enabled', False)
nba_enabled = self.config.get('nba_scoreboard', {}).get('enabled', False) nba_enabled = self.config.get('nba_scoreboard', {}).get('enabled', False)
mlb_enabled = self.config.get('mlb', {}).get('enabled', False) mlb_enabled = self.config.get('mlb_scoreboard', {}).get('enabled', False)
milb_enabled = self.config.get('milb', {}).get('enabled', False) milb_enabled = self.config.get('milb_scoreboard', {}).get('enabled', False)
soccer_enabled = self.config.get('soccer_scoreboard', {}).get('enabled', False) soccer_enabled = self.config.get('soccer_scoreboard', {}).get('enabled', False)
nfl_enabled = self.config.get('nfl_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) ncaa_fb_enabled = self.config.get('ncaa_fb_scoreboard', {}).get('enabled', False)
@@ -1006,8 +1020,10 @@ class DisplayController:
return return
try: try:
logger.info("Clearing cache and refetching data to prevent stale data issues...")
self.cache_manager.clear_cache() self.cache_manager.clear_cache()
self._update_modules() self._update_modules()
logger.info("Cache cleared, waiting 5 seconds for fresh data fetch...")
time.sleep(5) time.sleep(5)
self.current_display_mode = self.available_modes[self.current_mode_index] if self.available_modes else 'none' self.current_display_mode = self.available_modes[self.current_mode_index] if self.available_modes else 'none'
while True: while True:

View File

@@ -40,7 +40,7 @@ class LeaderboardManager:
self.enabled_sports = self.leaderboard_config.get('enabled_sports', {}) self.enabled_sports = self.leaderboard_config.get('enabled_sports', {})
self.update_interval = self.leaderboard_config.get('update_interval', 3600) self.update_interval = self.leaderboard_config.get('update_interval', 3600)
self.scroll_speed = self.leaderboard_config.get('scroll_speed', 2) 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.display_duration = self.leaderboard_config.get('display_duration', 30)
self.loop = self.leaderboard_config.get('loop', True) self.loop = self.leaderboard_config.get('loop', True)
self.request_timeout = self.leaderboard_config.get('request_timeout', 30) 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.dynamic_duration = 60 # Default duration in seconds
self.total_scroll_width = 0 # Track total width for dynamic duration calculation 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 # Initialize managers
self.cache_manager = CacheManager() self.cache_manager = CacheManager()
# Store reference to config instead of creating new ConfigManager # 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") logger.debug(f"get_dynamic_duration called, returning: {self.dynamic_duration}s")
return self.dynamic_duration 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: def update(self) -> None:
"""Update leaderboard data.""" """Update leaderboard data."""
current_time = time.time() current_time = time.time()
@@ -1329,20 +1342,31 @@ class LeaderboardManager:
try: try:
current_time = time.time() current_time = time.time()
# Check if we should be scrolling # FPS tracking
should_scroll = current_time - self.last_scroll_time >= self.scroll_delay 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 # Signal scrolling state to display manager
if should_scroll:
self.display_manager.set_scrolling_state(True) 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()
# Scroll the image # Scroll the image every frame for smooth animation
if should_scroll:
self.scroll_position += self.scroll_speed self.scroll_position += self.scroll_speed
self.last_scroll_time = current_time
# Add scroll delay like other managers for consistent timing
time.sleep(self.scroll_delay)
# Calculate crop region # Calculate crop region
width = self.display_manager.matrix.width width = self.display_manager.matrix.width

View File

@@ -14,6 +14,10 @@ from urllib3.util.retry import Retry
import pytz import pytz
from src.background_data_service import get_background_service 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 # Import API counter function
try: try:
from web_interface_v2 import increment_api_counter from web_interface_v2 import increment_api_counter
@@ -24,17 +28,16 @@ except ImportError:
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseMiLBManager: class BaseMiLBManager(Baseball):
"""Base class for MiLB managers with common functionality.""" """Base class for MiLB managers using new baseball architecture."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
self.config = config # Initialize with sport_key for MiLB
self.display_manager = display_manager super().__init__(config, display_manager, cache_manager, logger, "milb")
self.milb_config = config.get('milb', {})
# MiLB-specific configuration
self.milb_config = config.get('milb_scoreboard', {})
self.favorite_teams = self.milb_config.get('favorite_teams', []) self.favorite_teams = self.milb_config.get('favorite_teams', [])
self.show_records = self.milb_config.get('show_records', False) 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 # Load MiLB team mapping
self.team_mapping = {} self.team_mapping = {}
@@ -896,7 +899,7 @@ class BaseMiLBManager:
return game_data return game_data
return {} return {}
class MiLBLiveManager(BaseMiLBManager): class MiLBLiveManager(BaseMiLBManager, BaseballLive):
"""Manager for displaying live MiLB games.""" """Manager for displaying live MiLB games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)
@@ -1424,7 +1427,7 @@ class MiLBLiveManager(BaseMiLBManager):
except Exception as e: except Exception as e:
logger.error(f"[MiLB] Error displaying live game: {e}", exc_info=True) 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.""" """Manager for displaying recent MiLB games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)
@@ -1615,7 +1618,7 @@ class MiLBRecentManager(BaseMiLBManager):
except Exception as e: except Exception as e:
logger.error(f"[MiLB] Error displaying recent game: {e}", exc_info=True) logger.error(f"[MiLB] Error displaying recent game: {e}", exc_info=True)
class MiLBUpcomingManager(BaseMiLBManager): class MiLBUpcomingManager(BaseMiLBManager, SportsUpcoming):
"""Manager for upcoming MiLB games.""" """Manager for upcoming MiLB games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)

View File

@@ -14,6 +14,10 @@ import pytz
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
from src.background_data_service import get_background_service 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 # Import the API counter function from web interface
try: try:
from web_interface_v2 import increment_api_counter from web_interface_v2 import increment_api_counter
@@ -25,20 +29,21 @@ except ImportError:
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseMLBManager: class BaseMLBManager(Baseball):
"""Base class for MLB managers with common functionality.""" """Base class for MLB managers using new baseball architecture."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
self.config = config # Initialize with sport_key for MLB
self.display_manager = display_manager super().__init__(config, display_manager, cache_manager, logger, "mlb")
# Store reference to config instead of creating new ConfigManager
self.config_manager = None # Not used in this class # MLB-specific configuration
self.mlb_config = config.get('mlb', {}) self.mlb_config = config.get('mlb_scoreboard', {})
self.show_odds = self.mlb_config.get("show_odds", False) self.show_odds = self.mlb_config.get("show_odds", False)
self.favorite_teams = self.mlb_config.get('favorite_teams', []) self.favorite_teams = self.mlb_config.get('favorite_teams', [])
self.show_records = self.mlb_config.get('show_records', False) 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.odds_manager = OddsManager(self.cache_manager, self.config_manager)
self.logger = logging.getLogger(__name__)
# Logo handling # Logo handling
self.logo_dir = self.mlb_config.get('logo_dir', os.path.join('assets', 'sports', 'mlb_logos')) 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)) 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.""" """Manager for displaying live MLB games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)
@@ -1157,7 +1162,7 @@ class MLBLiveManager(BaseMLBManager):
except Exception as e: except Exception as e:
logger.error(f"[MLB] Error displaying live game: {e}", exc_info=True) 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.""" """Manager for displaying recent MLB games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)
@@ -1318,7 +1323,7 @@ class MLBRecentManager(BaseMLBManager):
except Exception as e: except Exception as e:
logger.error(f"[MLB] Error displaying recent game: {e}", exc_info=True) 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.""" """Manager for displaying upcoming MLB games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)

View File

@@ -13,27 +13,31 @@ from urllib3.util.retry import Retry
from src.odds_manager import OddsManager from src.odds_manager import OddsManager
import pytz import pytz
# Import baseball and standard sports classes
from .base_classes.baseball import Baseball, BaseballLive
from .base_classes.sports import SportsRecent, SportsUpcoming
# Get logger # Get logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Constants for NCAA Baseball API URL # Constants for NCAA Baseball API URL
ESPN_NCAABB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard" ESPN_NCAABB_SCOREBOARD_URL = "https://site.api.espn.com/apis/site/v2/sports/baseball/college-baseball/scoreboard"
class BaseNCAABaseballManager: class BaseNCAABaseballManager(Baseball):
"""Base class for NCAA Baseball managers with common functionality.""" """Base class for NCAA Baseball managers using new baseball architecture."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
self.config = config # Initialize with sport_key for NCAA Baseball
self.display_manager = display_manager super().__init__(config, display_manager, cache_manager, logger, "ncaa_baseball")
# Store reference to config instead of creating new ConfigManager
self.config_manager = None # Not used in this class # NCAA Baseball-specific configuration
self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {}) self.ncaa_baseball_config = config.get('ncaa_baseball_scoreboard', {})
self.show_odds = self.ncaa_baseball_config.get('show_odds', False) self.show_odds = self.ncaa_baseball_config.get('show_odds', False)
self.show_records = self.ncaa_baseball_config.get('show_records', False) self.show_records = self.ncaa_baseball_config.get('show_records', False)
self.favorite_teams = self.ncaa_baseball_config.get('favorite_teams', []) 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.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 # Logo handling
self.logo_dir = self.ncaa_baseball_config.get('logo_dir', os.path.join('assets', 'sports', 'ncaa_logos')) 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) self.logger.error(f"[NCAABaseball] Error fetching NCAA Baseball data from ESPN API: {e}", exc_info=True)
return {} return {}
class NCAABaseballLiveManager(BaseNCAABaseballManager): class NCAABaseballLiveManager(BaseNCAABaseballManager, BaseballLive):
"""Manager for displaying live NCAA Baseball games.""" """Manager for displaying live NCAA Baseball games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)
@@ -850,7 +854,7 @@ class NCAABaseballLiveManager(BaseNCAABaseballManager):
except Exception as e: except Exception as e:
logger.error(f"[NCAABaseball] Error displaying live game: {e}", exc_info=True) 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.""" """Manager for displaying recent NCAA Baseball games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)
@@ -974,7 +978,7 @@ class NCAABaseballRecentManager(BaseNCAABaseballManager):
except Exception as e: except Exception as e:
logger.error(f"[NCAABaseball] Error displaying recent game: {e}", exc_info=True) 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.""" """Manager for displaying upcoming NCAA Baseball games."""
def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager): def __init__(self, config: Dict[str, Any], display_manager, cache_manager: CacheManager):
super().__init__(config, display_manager, cache_manager) super().__init__(config, display_manager, cache_manager)

View File

@@ -29,6 +29,9 @@ class BaseNCAAFBManager(Football): # Renamed class
self.logger = logging.getLogger('NCAAFB') # Changed logger name 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") 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 # Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {}) display_modes = self.mode_config.get("display_modes", {})
self.recent_enabled = display_modes.get("ncaa_fb_recent", False) self.recent_enabled = display_modes.get("ncaa_fb_recent", False)
@@ -140,7 +143,7 @@ class BaseNCAAFBManager(Football): # Renamed class
# Submit background fetch request # Submit background fetch request
request_id = self.background_service.submit_fetch_request( request_id = self.background_service.submit_fetch_request(
sport="nfl", sport="ncaa_fb",
year=season_year, year=season_year,
url=ESPN_NCAAFB_SCOREBOARD_URL, url=ESPN_NCAAFB_SCOREBOARD_URL,
cache_key=cache_key, cache_key=cache_key,

View File

@@ -34,6 +34,9 @@ class BaseNCAAMHockeyManager(Hockey): # Renamed class
self.logger = logging.getLogger('NCAAMH') # Changed logger name 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") 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 # Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {}) display_modes = self.mode_config.get("display_modes", {})
self.recent_enabled = display_modes.get("ncaam_hockey_recent", False) self.recent_enabled = display_modes.get("ncaam_hockey_recent", False)

View File

@@ -26,6 +26,9 @@ class BaseNFLManager(Football): # Renamed class
self.logger = logging.getLogger('NFL') # Changed logger name 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") 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 # Check display modes to determine what data to fetch
display_modes = self.mode_config.get("display_modes", {}) display_modes = self.mode_config.get("display_modes", {})
self.recent_enabled = display_modes.get("nfl_recent", False) self.recent_enabled = display_modes.get("nfl_recent", False)

View File

@@ -162,8 +162,8 @@ class OddsTickerManager:
'league': 'mlb', 'league': 'mlb',
'logo_league': 'mlb', # ESPN API league identifier for logo downloading 'logo_league': 'mlb', # ESPN API league identifier for logo downloading
'logo_dir': 'assets/sports/mlb_logos', 'logo_dir': 'assets/sports/mlb_logos',
'favorite_teams': config.get('mlb', {}).get('favorite_teams', []), 'favorite_teams': config.get('mlb_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('mlb', {}).get('enabled', False) 'enabled': config.get('mlb_scoreboard', {}).get('enabled', False)
}, },
'ncaa_fb': { 'ncaa_fb': {
'sport': 'football', 'sport': 'football',
@@ -178,8 +178,8 @@ class OddsTickerManager:
'league': 'milb', 'league': 'milb',
'logo_league': 'milb', # ESPN API league identifier for logo downloading (if supported) 'logo_league': 'milb', # ESPN API league identifier for logo downloading (if supported)
'logo_dir': 'assets/sports/milb_logos', 'logo_dir': 'assets/sports/milb_logos',
'favorite_teams': config.get('milb', {}).get('favorite_teams', []), 'favorite_teams': config.get('milb_scoreboard', {}).get('favorite_teams', []),
'enabled': config.get('milb', {}).get('enabled', False) 'enabled': config.get('milb_scoreboard', {}).get('enabled', False)
}, },
'nhl': { 'nhl': {
'sport': 'hockey', 'sport': 'hockey',

View File

@@ -184,7 +184,7 @@ def test_configuration():
with open(config_path, 'r') as f: with open(config_path, 'r') as f:
config = json.load(f) config = json.load(f)
milb_config = config.get('milb', {}) milb_config = config.get('milb_scoreboard', {})
print(f"✅ Configuration file loaded successfully") print(f"✅ Configuration file loaded successfully")
print(f"MiLB enabled: {milb_config.get('enabled', False)}") print(f"MiLB enabled: {milb_config.get('enabled', False)}")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -31,8 +31,8 @@ def test_config_values():
("NCAA Football", config.get('ncaa_fb_scoreboard', {})), ("NCAA Football", config.get('ncaa_fb_scoreboard', {})),
("NCAA Baseball", config.get('ncaa_baseball_scoreboard', {})), ("NCAA Baseball", config.get('ncaa_baseball_scoreboard', {})),
("NCAA Basketball", config.get('ncaam_basketball_scoreboard', {})), ("NCAA Basketball", config.get('ncaam_basketball_scoreboard', {})),
("MLB", config.get('mlb', {})), ("MLB", config.get('mlb_scoreboard', {})),
("MiLB", config.get('milb', {})), ("MiLB", config.get('milb_scoreboard', {})),
("Soccer", config.get('soccer_scoreboard', {})) ("Soccer", config.get('soccer_scoreboard', {}))
] ]
@@ -84,8 +84,8 @@ def test_config_consistency():
("NCAA Football", config.get('ncaa_fb_scoreboard', {})), ("NCAA Football", config.get('ncaa_fb_scoreboard', {})),
("NCAA Baseball", config.get('ncaa_baseball_scoreboard', {})), ("NCAA Baseball", config.get('ncaa_baseball_scoreboard', {})),
("NCAA Basketball", config.get('ncaam_basketball_scoreboard', {})), ("NCAA Basketball", config.get('ncaam_basketball_scoreboard', {})),
("MLB", config.get('mlb', {})), ("MLB", config.get('mlb_scoreboard', {})),
("MiLB", config.get('milb', {})), ("MiLB", config.get('milb_scoreboard', {})),
("Soccer", config.get('soccer_scoreboard', {})) ("Soccer", config.get('soccer_scoreboard', {}))
] ]

View File

@@ -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)

View File

@@ -23,7 +23,7 @@ def test_milb_api_accuracy():
try: try:
with open('config/config.json', 'r') as f: with open('config/config.json', 'r') as f:
config = json.load(f) config = json.load(f)
milb_config = config.get('milb', {}) milb_config = config.get('milb_scoreboard', {})
favorite_teams = milb_config.get('favorite_teams', []) favorite_teams = milb_config.get('favorite_teams', [])
print(f"Favorite teams: {favorite_teams}") print(f"Favorite teams: {favorite_teams}")
except Exception as e: except Exception as e:

View File

@@ -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)

View File

@@ -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)

1
test_config_loading.py Normal file
View File

@@ -0,0 +1 @@

1
test_config_simple.py Normal file
View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@