diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 59e6a069..da565883 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -206,8 +206,8 @@ class BaseNCAAFBManager: # Renamed class def _fetch_ncaa_fb_api_data(self, use_cache: bool = True) -> Optional[Dict]: """ - Fetches the full season schedule for NCAAFB, caches it, and then filters - for relevant games based on the current configuration. + Fetches the full season schedule for NCAAFB using week-by-week approach to ensure + we get all games, then caches the complete dataset. """ now = datetime.now(pytz.utc) current_year = now.year @@ -226,19 +226,48 @@ class BaseNCAAFBManager: # Renamed class continue self.logger.info(f"[NCAAFB] Fetching full {year} season schedule from ESPN API...") + year_events = [] + + # Fetch week by week to ensure we get complete season data + # College football typically has weeks 1-15 plus postseason + for week in range(1, 16): + try: + url = f"https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?seasontype=2&week={week}" + response = self.session.get(url, headers=self.headers, timeout=15) + response.raise_for_status() + data = response.json() + week_events = data.get('events', []) + year_events.extend(week_events) + + # Log progress for first few weeks + if week <= 3: + self.logger.debug(f"[NCAAFB] Week {week}: fetched {len(week_events)} events") + + # If no events found for this week, we might be past the season + if not week_events and week > 10: + self.logger.debug(f"[NCAAFB] No events found for week {week}, ending season fetch") + break + + except requests.exceptions.RequestException as e: + self.logger.warning(f"[NCAAFB] Error fetching week {week} for {year}: {e}") + continue + + # Also fetch postseason games (bowl games, playoffs) try: - url = f"https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates={year}&seasontype=2" + url = f"https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?seasontype=3" response = self.session.get(url, headers=self.headers, timeout=15) response.raise_for_status() data = response.json() - events = data.get('events', []) - if use_cache: - self.cache_manager.set(cache_key, events) - self.logger.info(f"[NCAAFB] Successfully fetched and cached {len(events)} events for {year} season.") - all_events.extend(events) + postseason_events = data.get('events', []) + year_events.extend(postseason_events) + self.logger.debug(f"[NCAAFB] Postseason: fetched {len(postseason_events)} events") except requests.exceptions.RequestException as e: - self.logger.error(f"[NCAAFB] API error fetching full schedule for {year}: {e}") - continue + self.logger.warning(f"[NCAAFB] Error fetching postseason for {year}: {e}") + + if use_cache: + self.cache_manager.set(cache_key, year_events) + self.logger.info(f"[NCAAFB] Successfully fetched and cached {len(year_events)} events for {year} season.") + all_events.extend(year_events) if not all_events: self.logger.warning("[NCAAFB] No events found in schedule data.") @@ -992,9 +1021,9 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class events = data['events'] # self.logger.info(f"[NCAAFB Recent] Processing {len(events)} events from shared data.") # Changed log prefix - # Define date range for "recent" games (last 14 days) + # Define date range for "recent" games (last 21 days to capture games from 3 weeks ago) now = datetime.now(timezone.utc) - recent_cutoff = now - timedelta(days=14) + recent_cutoff = now - timedelta(days=21) # Process games and filter for final games, date range & favorite teams processed_games = [] diff --git a/test_core_logic.py b/test_core_logic.py new file mode 100644 index 00000000..1a79d7a8 --- /dev/null +++ b/test_core_logic.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +Test the core logic of the web interface without Flask dependencies. +""" +import json + +class DictWrapper: + """Wrapper to make dictionary accessible via dot notation for Jinja2 templates.""" + def __init__(self, data=None): + # Store the original data + object.__setattr__(self, '_data', data if isinstance(data, dict) else {}) + + # Set attributes from the dictionary + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, dict): + object.__setattr__(self, key, DictWrapper(value)) + elif isinstance(value, list): + object.__setattr__(self, key, value) + else: + object.__setattr__(self, key, value) + + def __getattr__(self, name): + # Return a new empty DictWrapper for missing attributes + # This allows chaining like main_config.display.hardware.rows + return DictWrapper({}) + + def __str__(self): + # Return empty string for missing values to avoid template errors + data = object.__getattribute__(self, '_data') + if not data: + return '' + return str(data) + + def __int__(self): + # Return 0 for missing numeric values + data = object.__getattribute__(self, '_data') + if not data: + return 0 + try: + return int(data) + except (ValueError, TypeError): + return 0 + + def __bool__(self): + # Return False for missing boolean values + data = object.__getattribute__(self, '_data') + if not data: + return False + return bool(data) + + def get(self, key, default=None): + # Support .get() method like dictionaries + data = object.__getattribute__(self, '_data') + if data and key in data: + return data[key] + return default + +def safe_get(obj, key_path, default=''): + """Safely access nested dictionary values using dot notation.""" + try: + keys = key_path.split('.') + current = obj + for key in keys: + if hasattr(current, key): + current = getattr(current, key) + elif isinstance(current, dict) and key in current: + current = current[key] + else: + return default + return current if current is not None else default + except (AttributeError, KeyError, TypeError): + return default + +def safe_config_get(config, *keys, default=''): + """Safely get nested config values with fallback.""" + try: + current = config + for key in keys: + if hasattr(current, key): + current = getattr(current, key) + # Check if we got an empty DictWrapper + if isinstance(current, DictWrapper): + data = object.__getattribute__(current, '_data') + if not data: # Empty DictWrapper means missing config + return default + elif isinstance(current, dict) and key in current: + current = current[key] + else: + return default + + # Final check for empty values + if current is None or (hasattr(current, '_data') and not object.__getattribute__(current, '_data')): + return default + return current + except (AttributeError, KeyError, TypeError): + return default + +def simulate_template_rendering(): + """Simulate how the template would render configuration values.""" + print("Simulating template rendering with actual config...") + + # Load actual config + with open('config/config.json', 'r') as f: + config_data = json.load(f) + + main_config = DictWrapper(config_data) + + # Simulate template expressions that would be used + template_tests = [ + # Input field values + ("safe_config_get(main_config, 'display', 'hardware', 'rows', default=32)", 32), + ("safe_config_get(main_config, 'display', 'hardware', 'cols', default=64)", 64), + ("safe_config_get(main_config, 'display', 'hardware', 'brightness', default=95)", 95), + ("safe_config_get(main_config, 'display', 'hardware', 'chain_length', default=2)", 2), + ("safe_config_get(main_config, 'display', 'hardware', 'parallel', default=1)", 1), + ("safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm')", 'adafruit-hat-pwm'), + + # Checkbox states + ("safe_config_get(main_config, 'display', 'hardware', 'disable_hardware_pulsing', default=False)", False), + ("safe_config_get(main_config, 'display', 'hardware', 'inverse_colors', default=False)", False), + ("safe_config_get(main_config, 'display', 'hardware', 'show_refresh_rate', default=False)", False), + ("safe_config_get(main_config, 'display', 'use_short_date_format', default=True)", True), + ] + + all_passed = True + for expression, expected in template_tests: + try: + result = eval(expression) + status = "✓" if result == expected else "✗" + print(f" {status} {expression.split('(')[0]}(...): {result} (expected: {expected})") + if result != expected: + all_passed = False + except Exception as e: + print(f" ✗ {expression}: ERROR - {e}") + all_passed = False + + return all_passed + +if __name__ == "__main__": + print("=" * 70) + print("Testing Core Web Interface Logic") + print("=" * 70) + + success = simulate_template_rendering() + + print("\n" + "=" * 70) + if success: + print("✓ ALL TEMPLATE SIMULATIONS PASSED!") + print("✓ The web interface should correctly display all config values!") + else: + print("✗ SOME TEMPLATE SIMULATIONS FAILED!") + print("✗ There may be issues with config display in the web interface!") + print("=" * 70)