From 3169d1a1ff894b9a0e845fd59c363550a9ec9578 Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 29 Mar 2026 14:06:45 -0400 Subject: [PATCH] fix: resolve 4 logic bugs found in code review - utc_to_eastern: use zoneinfo instead of hardcoded EDT offset (-4) so start times are correct in both EST and EDT - standings: fetch before truncate so a failed API call doesn't wipe existing standings data - routes: call parse_games() once per request instead of three times - scheduler: wrap run_pending() in try/except so an unhandled exception doesn't kill the background thread Co-Authored-By: Claude Sonnet 4.6 --- app/games.py | 10 ++++++---- app/routes.py | 20 ++++---------------- app/scheduler.py | 5 ++++- app/standings.py | 2 +- tests/test_games.py | 8 +++++++- tests/test_scheduler.py | 17 +++++++++++++++++ tests/test_standings.py | 21 +++++++++++++++++---- 7 files changed, 56 insertions(+), 27 deletions(-) diff --git a/app/games.py b/app/games.py index 8257411..e868dda 100644 --- a/app/games.py +++ b/app/games.py @@ -1,9 +1,12 @@ import logging import sqlite3 -from datetime import datetime, timedelta +from datetime import datetime, timezone +from zoneinfo import ZoneInfo from app.config import DB_PATH +EASTERN = ZoneInfo("America/New_York") + logger = logging.getLogger(__name__) @@ -226,6 +229,5 @@ def get_team_standings(team_name): def utc_to_eastern(utc_time): utc_datetime = datetime.strptime(utc_time, "%Y-%m-%dT%H:%M:%SZ") - est_offset = timedelta(hours=-4) - est_datetime = utc_datetime + est_offset - return est_datetime.strftime("%I:%M %p") + eastern_datetime = utc_datetime.replace(tzinfo=timezone.utc).astimezone(EASTERN) + return eastern_datetime.strftime("%I:%M %p") diff --git a/app/routes.py b/app/routes.py index ddf01ea..e37856d 100644 --- a/app/routes.py +++ b/app/routes.py @@ -25,24 +25,12 @@ def get_scoreboard(): ) if scoreboard_data: - live_games = [ - game - for game in parse_games(scoreboard_data) - if game["Game State"] == "LIVE" - ] - pre_games = [ - game for game in parse_games(scoreboard_data) if game["Game State"] == "PRE" - ] - final_games = [ - game - for game in parse_games(scoreboard_data) - if game["Game State"] == "FINAL" - ] + games = parse_games(scoreboard_data) return jsonify( { - "live_games": live_games, - "pre_games": pre_games, - "final_games": final_games, + "live_games": [g for g in games if g["Game State"] == "LIVE"], + "pre_games": [g for g in games if g["Game State"] == "PRE"], + "final_games": [g for g in games if g["Game State"] == "FINAL"], } ) else: diff --git a/app/scheduler.py b/app/scheduler.py index 88dec7e..2432c9c 100644 --- a/app/scheduler.py +++ b/app/scheduler.py @@ -14,5 +14,8 @@ def start_scheduler(): schedule.every(10).seconds.do(refresh_scores) logger.info("Background scheduler started") while True: - schedule.run_pending() + try: + schedule.run_pending() + except Exception: + logger.exception("Scheduler encountered an unexpected error") time.sleep(1) diff --git a/app/standings.py b/app/standings.py index 19908fb..b5b59c9 100644 --- a/app/standings.py +++ b/app/standings.py @@ -67,8 +67,8 @@ def fetch_standings(): def refresh_standings(): conn = sqlite3.connect(DB_PATH) create_standings_table(conn) - truncate_standings_table(conn) standings = fetch_standings() if standings: + truncate_standings_table(conn) insert_standings(conn, standings) conn.close() diff --git a/tests/test_games.py b/tests/test_games.py index 6c888ae..8bc350f 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -101,10 +101,16 @@ class TestGetGameOutcome: class TestUtcToEstTime: - def test_converts_utc_to_est(self): + def test_converts_utc_to_edt(self): + # April is EDT (UTC-4): 23:00 UTC → 07:00 PM EDT result = utc_to_eastern("2024-04-10T23:00:00Z") assert result == "07:00 PM" + def test_converts_utc_to_est(self): + # January is EST (UTC-5): 23:00 UTC → 06:00 PM EST + result = utc_to_eastern("2024-01-15T23:00:00Z") + assert result == "06:00 PM" + class TestParseGames: def test_returns_empty_list_for_none(self): diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 54718cc..5c19d90 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -39,3 +39,20 @@ class TestStartScheduler: start_scheduler() assert mock_schedule.run_pending.call_count >= 2 + + def test_continues_after_exception_in_run_pending(self, mocker): + mock_schedule = mocker.patch("app.scheduler.schedule") + call_count = {"n": 0} + + def raise_then_stop(_): + call_count["n"] += 1 + if call_count["n"] >= 2: + raise StopIteration + + mock_schedule.run_pending.side_effect = RuntimeError("boom") + mocker.patch("app.scheduler.time.sleep", side_effect=raise_then_stop) + + with pytest.raises(StopIteration): + start_scheduler() + + assert mock_schedule.run_pending.call_count >= 2 diff --git a/tests/test_standings.py b/tests/test_standings.py index 58eb5f1..42ece95 100644 --- a/tests/test_standings.py +++ b/tests/test_standings.py @@ -209,12 +209,25 @@ class TestRefreshStandings: assert count == 2 def test_does_not_insert_when_fetch_fails(self, mocker, tmp_path): - mocker.patch("app.standings.fetch_standings", return_value=None) - mocker.patch("app.standings.DB_PATH", str(tmp_path / "test.db")) + db_path = str(tmp_path / "test.db") + mocker.patch("app.standings.DB_PATH", db_path) + # Seed with existing data before the failed refresh + seed = [ + { + "team_common_name": "Bruins", + "league_sequence": 1, + "league_l10_sequence": 2, + } + ] + mocker.patch("app.standings.fetch_standings", return_value=seed) refresh_standings() - conn = sqlite3.connect(str(tmp_path / "test.db")) + # Now simulate a fetch failure — existing data must be preserved + mocker.patch("app.standings.fetch_standings", return_value=None) + refresh_standings() + + conn = sqlite3.connect(db_path) count = conn.execute("SELECT COUNT(*) FROM standings").fetchone()[0] conn.close() - assert count == 0 + assert count == 1