mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 05:13: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:
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
|
||||
Reference in New Issue
Block a user