mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
* 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:
213
.cursor/rules/github-branches-rule.mdc
Normal file
213
.cursor/rules/github-branches-rule.mdc
Normal 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
|
||||||
@@ -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,
|
||||||
|
|||||||
363
src/base_classes/api_extractors.py
Normal file
363
src/base_classes/api_extractors.py
Normal 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
|
||||||
165
src/base_classes/baseball.py
Normal file
165
src/base_classes/baseball.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
288
src/base_classes/data_sources.py
Normal file
288
src/base_classes/data_sources.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)}")
|
||||||
|
|||||||
256
test/test_baseball_architecture.py
Normal file
256
test/test_baseball_architecture.py
Normal 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)
|
||||||
236
test/test_baseball_managers_integration.py
Normal file
236
test/test_baseball_managers_integration.py
Normal 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)
|
||||||
243
test/test_baseball_managers_simple.py
Normal file
243
test/test_baseball_managers_simple.py
Normal 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)
|
||||||
@@ -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', {}))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
169
test/test_leaderboard_duration_fix.py
Normal file
169
test/test_leaderboard_duration_fix.py
Normal 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)
|
||||||
@@ -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:
|
||||||
|
|||||||
243
test/test_new_architecture.py
Normal file
243
test/test_new_architecture.py
Normal 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)
|
||||||
270
test/test_sports_integration.py
Normal file
270
test/test_sports_integration.py
Normal 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
1
test_config_loading.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
test_config_simple.py
Normal file
1
test_config_simple.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
test_config_validation.py
Normal file
1
test_config_validation.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user