mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
* 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
240 lines
8.5 KiB
Python
240 lines
8.5 KiB
Python
#!/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)
|