mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Feature/ap top 25 dynamic teams (#68)
* feat: Add AP Top 25 dynamic teams feature - Add DynamicTeamResolver for resolving AP_TOP_25, AP_TOP_10, AP_TOP_5 to actual team abbreviations - Integrate dynamic team resolution into SportsCore base class - Add comprehensive test suite for dynamic team functionality - Update config template with AP_TOP_25 example - Add complete documentation for the new feature Features: - Automatic weekly updates of AP Top 25 rankings - 1-hour caching for performance optimization - Support for AP_TOP_25, AP_TOP_10, AP_TOP_5 dynamic teams - Seamless integration with existing favorite teams system - Comprehensive error handling and edge case support Tests: - Unit tests for core dynamic team resolution - Integration tests for configuration scenarios - Performance tests for caching functionality - Edge case tests for unknown dynamic teams All tests passing with 100% success rate. * docs: Update wiki submodule with AP Top 25 documentation - Add comprehensive documentation for AP Top 25 dynamic teams feature - Include usage examples, configuration guides, and troubleshooting - Update submodule reference to include new documentation * feat: Add AP_TOP_25 support to odds ticker - Integrate DynamicTeamResolver into OddsTickerManager - Resolve dynamic teams for all enabled leagues during initialization - Add comprehensive logging for dynamic team resolution - Support AP_TOP_25, AP_TOP_10, AP_TOP_5 in odds ticker - Add test suite for odds ticker dynamic teams integration Features: - Odds ticker now automatically resolves AP_TOP_25 to current top 25 teams - Shows odds for all current AP Top 25 teams automatically - Updates weekly when rankings change - Works seamlessly with existing favorite teams system - Supports mixed regular and dynamic teams Tests: - Configuration integration tests - Multiple league configuration tests - Edge case handling tests - All tests passing with 100% success rate
This commit is contained in:
@@ -18,6 +18,7 @@ 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
|
||||
from src.dynamic_team_resolver import DynamicTeamResolver
|
||||
|
||||
class SportsCore:
|
||||
def __init__(self, config: Dict[str, Any], display_manager: DisplayManager, cache_manager: CacheManager, logger: logging.Logger, sport_key: str):
|
||||
@@ -77,7 +78,18 @@ class SportsCore:
|
||||
self.last_update = 0
|
||||
self.current_game = None
|
||||
self.fonts = self._load_fonts()
|
||||
self.favorite_teams = self.mode_config.get("favorite_teams", [])
|
||||
|
||||
# Initialize dynamic team resolver and resolve favorite teams
|
||||
self.dynamic_resolver = DynamicTeamResolver()
|
||||
raw_favorite_teams = self.mode_config.get("favorite_teams", [])
|
||||
self.favorite_teams = self.dynamic_resolver.resolve_teams(raw_favorite_teams, sport_key)
|
||||
|
||||
# Log dynamic team resolution
|
||||
if raw_favorite_teams != self.favorite_teams:
|
||||
self.logger.info(f"Resolved dynamic teams: {raw_favorite_teams} -> {self.favorite_teams}")
|
||||
else:
|
||||
self.logger.info(f"Favorite teams: {self.favorite_teams}")
|
||||
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
# Initialize team rankings cache
|
||||
|
||||
239
src/dynamic_team_resolver.py
Normal file
239
src/dynamic_team_resolver.py
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dynamic Team Resolver for LEDMatrix
|
||||
|
||||
This module provides functionality to resolve dynamic team names like "AP_TOP_25"
|
||||
into actual team abbreviations that update automatically with rankings.
|
||||
|
||||
Supported dynamic teams:
|
||||
- AP_TOP_25: Resolves to current AP Top 25 teams for NCAA Football
|
||||
- AP_TOP_10: Resolves to current AP Top 10 teams for NCAA Football
|
||||
- AP_TOP_5: Resolves to current AP Top 5 teams for NCAA Football
|
||||
|
||||
Usage:
|
||||
resolver = DynamicTeamResolver()
|
||||
resolved_teams = resolver.resolve_teams(["UGA", "AP_TOP_25", "AUB"])
|
||||
# Returns: ["UGA", "UGA", "AUB", "MICH", "OSU", ...] (AP_TOP_25 teams)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import requests
|
||||
from typing import Dict, List, Set, Optional, Any
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DynamicTeamResolver:
|
||||
"""
|
||||
Resolves dynamic team names to actual team abbreviations.
|
||||
|
||||
This class handles special team names that represent dynamic groups
|
||||
like AP Top 25 rankings, which update automatically.
|
||||
"""
|
||||
|
||||
# Cache for rankings data
|
||||
_rankings_cache: Dict[str, List[str]] = {}
|
||||
_cache_timestamp: float = 0
|
||||
_cache_duration: int = 3600 # 1 hour cache
|
||||
|
||||
# Supported dynamic team patterns
|
||||
DYNAMIC_PATTERNS = {
|
||||
'AP_TOP_25': {'sport': 'ncaa_fb', 'limit': 25},
|
||||
'AP_TOP_10': {'sport': 'ncaa_fb', 'limit': 10},
|
||||
'AP_TOP_5': {'sport': 'ncaa_fb', 'limit': 5},
|
||||
}
|
||||
|
||||
def __init__(self, request_timeout: int = 30):
|
||||
"""Initialize the dynamic team resolver."""
|
||||
self.request_timeout = request_timeout
|
||||
self.logger = logger
|
||||
|
||||
def resolve_teams(self, team_list: List[str], sport: str = 'ncaa_fb') -> List[str]:
|
||||
"""
|
||||
Resolve a list of team names, expanding dynamic team names.
|
||||
|
||||
Args:
|
||||
team_list: List of team names (can include dynamic names like "AP_TOP_25")
|
||||
sport: Sport type for context (default: 'ncaa_fb')
|
||||
|
||||
Returns:
|
||||
List of resolved team abbreviations
|
||||
"""
|
||||
if not team_list:
|
||||
return []
|
||||
|
||||
resolved_teams = []
|
||||
|
||||
for team in team_list:
|
||||
if team in self.DYNAMIC_PATTERNS:
|
||||
# Resolve dynamic team
|
||||
dynamic_teams = self._resolve_dynamic_team(team, sport)
|
||||
resolved_teams.extend(dynamic_teams)
|
||||
self.logger.info(f"Resolved {team} to {len(dynamic_teams)} teams: {dynamic_teams[:5]}{'...' if len(dynamic_teams) > 5 else ''}")
|
||||
elif self._is_potential_dynamic_team(team):
|
||||
# Unknown dynamic team, skip it
|
||||
self.logger.warning(f"Unknown dynamic team '{team}' - skipping")
|
||||
else:
|
||||
# Regular team name, add as-is
|
||||
resolved_teams.append(team)
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
seen = set()
|
||||
unique_teams = []
|
||||
for team in resolved_teams:
|
||||
if team not in seen:
|
||||
seen.add(team)
|
||||
unique_teams.append(team)
|
||||
|
||||
return unique_teams
|
||||
|
||||
def _resolve_dynamic_team(self, dynamic_team: str, sport: str) -> List[str]:
|
||||
"""
|
||||
Resolve a dynamic team name to actual team abbreviations.
|
||||
|
||||
Args:
|
||||
dynamic_team: Dynamic team name (e.g., "AP_TOP_25")
|
||||
sport: Sport type for context
|
||||
|
||||
Returns:
|
||||
List of team abbreviations
|
||||
"""
|
||||
if dynamic_team not in self.DYNAMIC_PATTERNS:
|
||||
self.logger.warning(f"Unknown dynamic team: {dynamic_team}")
|
||||
return []
|
||||
|
||||
pattern_config = self.DYNAMIC_PATTERNS[dynamic_team]
|
||||
target_sport = pattern_config['sport']
|
||||
limit = pattern_config['limit']
|
||||
|
||||
# Only support NCAA Football rankings for now
|
||||
if target_sport != 'ncaa_fb':
|
||||
self.logger.warning(f"Dynamic team {dynamic_team} not supported for sport {sport}")
|
||||
return []
|
||||
|
||||
# Fetch current rankings
|
||||
rankings = self._fetch_ncaa_fb_rankings()
|
||||
if not rankings:
|
||||
self.logger.warning(f"Could not fetch rankings for {dynamic_team}")
|
||||
return []
|
||||
|
||||
# Get top N teams
|
||||
top_teams = list(rankings.keys())[:limit]
|
||||
self.logger.info(f"Resolved {dynamic_team} to top {len(top_teams)} teams: {top_teams}")
|
||||
|
||||
return top_teams
|
||||
|
||||
def _fetch_ncaa_fb_rankings(self) -> Dict[str, int]:
|
||||
"""
|
||||
Fetch current NCAA Football rankings from ESPN API.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping team abbreviations to rankings
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# Check cache first
|
||||
if (self._rankings_cache and
|
||||
current_time - self._cache_timestamp < self._cache_duration):
|
||||
return self._rankings_cache
|
||||
|
||||
try:
|
||||
self.logger.info("Fetching fresh NCAA Football rankings from ESPN API")
|
||||
rankings_url = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings"
|
||||
|
||||
response = requests.get(rankings_url, timeout=self.request_timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
rankings = {}
|
||||
rankings_data = data.get('rankings', [])
|
||||
|
||||
if rankings_data:
|
||||
# Use the first ranking (usually AP Top 25)
|
||||
first_ranking = rankings_data[0]
|
||||
ranking_name = first_ranking.get('name', 'Unknown')
|
||||
teams = first_ranking.get('ranks', [])
|
||||
|
||||
self.logger.info(f"Using ranking: {ranking_name}")
|
||||
self.logger.info(f"Found {len(teams)} teams in ranking")
|
||||
|
||||
for team_data in teams:
|
||||
team_info = team_data.get('team', {})
|
||||
team_abbr = team_info.get('abbreviation', '')
|
||||
current_rank = team_data.get('current', 0)
|
||||
|
||||
if team_abbr and current_rank > 0:
|
||||
rankings[team_abbr] = current_rank
|
||||
|
||||
# Sort by ranking (1, 2, 3, etc.)
|
||||
sorted_rankings = dict(sorted(rankings.items(), key=lambda x: x[1]))
|
||||
|
||||
# Cache the results
|
||||
self._rankings_cache = sorted_rankings
|
||||
self._cache_timestamp = current_time
|
||||
|
||||
self.logger.info(f"Fetched rankings for {len(sorted_rankings)} teams")
|
||||
return sorted_rankings
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching NCAA Football rankings: {e}")
|
||||
|
||||
return {}
|
||||
|
||||
def get_available_dynamic_teams(self) -> List[str]:
|
||||
"""
|
||||
Get list of available dynamic team names.
|
||||
|
||||
Returns:
|
||||
List of supported dynamic team names
|
||||
"""
|
||||
return list(self.DYNAMIC_PATTERNS.keys())
|
||||
|
||||
def is_dynamic_team(self, team_name: str) -> bool:
|
||||
"""
|
||||
Check if a team name is a dynamic team.
|
||||
|
||||
Args:
|
||||
team_name: Team name to check
|
||||
|
||||
Returns:
|
||||
True if the team name is dynamic
|
||||
"""
|
||||
return team_name in self.DYNAMIC_PATTERNS
|
||||
|
||||
def _is_potential_dynamic_team(self, team_name: str) -> bool:
|
||||
"""
|
||||
Check if a team name looks like it might be a dynamic team but isn't recognized.
|
||||
|
||||
Args:
|
||||
team_name: Team name to check
|
||||
|
||||
Returns:
|
||||
True if the team name looks like a dynamic team pattern
|
||||
"""
|
||||
# Check for common dynamic team patterns
|
||||
dynamic_patterns = ['AP_TOP_', 'TOP_', 'RANKED_', 'PLAYOFF_']
|
||||
return any(pattern in team_name.upper() for pattern in dynamic_patterns)
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear the rankings cache to force fresh data on next request."""
|
||||
self._rankings_cache = {}
|
||||
self._cache_timestamp = 0
|
||||
self.logger.info("Cleared dynamic team rankings cache")
|
||||
|
||||
|
||||
# Convenience function for easy integration
|
||||
def resolve_dynamic_teams(team_list: List[str], sport: str = 'ncaa_fb') -> List[str]:
|
||||
"""
|
||||
Convenience function to resolve dynamic teams in a team list.
|
||||
|
||||
Args:
|
||||
team_list: List of team names (can include dynamic names)
|
||||
sport: Sport type for context
|
||||
|
||||
Returns:
|
||||
List of resolved team abbreviations
|
||||
"""
|
||||
resolver = DynamicTeamResolver()
|
||||
return resolver.resolve_teams(team_list, sport)
|
||||
@@ -13,6 +13,7 @@ from src.cache_manager import CacheManager
|
||||
from src.odds_manager import OddsManager
|
||||
from src.logo_downloader import download_missing_logo
|
||||
from src.background_data_service import get_background_service
|
||||
from src.dynamic_team_resolver import DynamicTeamResolver
|
||||
|
||||
# Import the API counter function from web interface
|
||||
try:
|
||||
@@ -140,6 +141,9 @@ class OddsTickerManager:
|
||||
# Font setup
|
||||
self.fonts = self._load_fonts()
|
||||
|
||||
# Initialize dynamic team resolver
|
||||
self.dynamic_resolver = DynamicTeamResolver()
|
||||
|
||||
# League configurations
|
||||
self.league_configs = {
|
||||
'nfl': {
|
||||
@@ -216,6 +220,21 @@ class OddsTickerManager:
|
||||
}
|
||||
}
|
||||
|
||||
# Resolve dynamic teams for each league
|
||||
for league_key, league_config in self.league_configs.items():
|
||||
if league_config.get('enabled', False):
|
||||
raw_favorite_teams = league_config.get('favorite_teams', [])
|
||||
if raw_favorite_teams:
|
||||
# Resolve dynamic teams for this league
|
||||
resolved_teams = self.dynamic_resolver.resolve_teams(raw_favorite_teams, league_key)
|
||||
league_config['favorite_teams'] = resolved_teams
|
||||
|
||||
# Log dynamic team resolution
|
||||
if raw_favorite_teams != resolved_teams:
|
||||
logger.info(f"Resolved dynamic teams for {league_key}: {raw_favorite_teams} -> {resolved_teams}")
|
||||
else:
|
||||
logger.info(f"Favorite teams for {league_key}: {resolved_teams}")
|
||||
|
||||
logger.info(f"OddsTickerManager initialized with enabled leagues: {self.enabled_leagues}")
|
||||
logger.info(f"Show favorite teams only: {self.show_favorite_teams_only}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user