diff --git a/app/bracket_view.py b/app/bracket_view.py index 539b0f1..726a8b1 100644 --- a/app/bracket_view.py +++ b/app/bracket_view.py @@ -34,13 +34,13 @@ def build_bracket_view(year, bracket_payload, fetched_at=None): def slot(letter): return _matchup(year, letter, series_by_letter.get(letter)) - east_r1 = [slot(l) for l in EAST_R1] - west_r1 = [slot(l) for l in WEST_R1] - east_r2 = [slot(l) for l in EAST_R2] - west_r2 = [slot(l) for l in WEST_R2] - east_cf = [slot(l) for l in EAST_CF] - west_cf = [slot(l) for l in WEST_CF] - cup = [slot(l) for l in CUP_FINAL] + east_r1 = [slot(ltr) for ltr in EAST_R1] + west_r1 = [slot(ltr) for ltr in WEST_R1] + east_r2 = [slot(ltr) for ltr in EAST_R2] + west_r2 = [slot(ltr) for ltr in WEST_R2] + east_cf = [slot(ltr) for ltr in EAST_CF] + west_cf = [slot(ltr) for ltr in WEST_CF] + cup = [slot(ltr) for ltr in CUP_FINAL] return { "year": year, diff --git a/app/playoff.py b/app/playoff.py index 805f683..aa7c418 100644 --- a/app/playoff.py +++ b/app/playoff.py @@ -30,9 +30,11 @@ def series_id(game): return None start = game.get("startTimeUTC") or "" try: - year = datetime.fromisoformat(start.replace("Z", "+00:00")).astimezone( - EASTERN - ).year + year = ( + datetime.fromisoformat(start.replace("Z", "+00:00")) + .astimezone(EASTERN) + .year + ) except (ValueError, AttributeError): year = datetime.now(EASTERN).year return f"{year}-{letter.upper()}" diff --git a/app/playoff_cache.py b/app/playoff_cache.py index 9c13e58..aafbca5 100644 --- a/app/playoff_cache.py +++ b/app/playoff_cache.py @@ -21,8 +21,8 @@ logger = logging.getLogger(__name__) EASTERN = ZoneInfo("America/New_York") -BRACKET_TTL = 3600 # refresh at this cadence via scheduler -SERIES_TTL = 300 # lazy cache for per-series schedule fetches +BRACKET_TTL = 3600 # refresh at this cadence via scheduler +SERIES_TTL = 300 # lazy cache for per-series schedule fetches MAX_STALE_SECONDS = 86400 # 24h SERIES_ID_RE = re.compile(r"^(20\d{2})-([A-P])$") @@ -78,6 +78,7 @@ def _get(cache_key): # ── Bracket ──────────────────────────────────────────────────────── + def bracket_key(year): return f"bracket:{year}" @@ -109,6 +110,7 @@ def get_bracket(year=None): # ── Per-series schedule ──────────────────────────────────────────── + def series_key(season, letter): return f"series:{season}:{letter.upper()}" @@ -140,7 +142,9 @@ def fetch_series(series_id): if time.time() - fetched < SERIES_TTL: return payload - url = f"https://api-web.nhle.com/v1/schedule/playoff-series/{season}/{letter.lower()}" + url = ( + f"https://api-web.nhle.com/v1/schedule/playoff-series/{season}/{letter.lower()}" + ) try: resp = requests.get(url, timeout=10) resp.raise_for_status() @@ -189,7 +193,9 @@ def record_start_date_if_missing(scoreboard_games, now=None): now = now or datetime.now(EASTERN) today = now.date().isoformat() - _put(META_KEY, {"first_date": today, "recorded_at_utc": now.astimezone().isoformat()}) + _put( + META_KEY, {"first_date": today, "recorded_at_utc": now.astimezone().isoformat()} + ) return today diff --git a/tests/test_bracket_view.py b/tests/test_bracket_view.py index f45e0cf..50896c5 100644 --- a/tests/test_bracket_view.py +++ b/tests/test_bracket_view.py @@ -1,8 +1,19 @@ from app.bracket_view import build_bracket_view -def _series(letter, top_abbrev, top_id, top_wins, bot_abbrev, bot_id, bot_wins, - rnd=1, winning_id=None, top_seed="D1", bot_seed="WC1"): +def _series( + letter, + top_abbrev, + top_id, + top_wins, + bot_abbrev, + bot_id, + bot_wins, + rnd=1, + winning_id=None, + top_seed="D1", + bot_seed="WC1", +): return { "seriesLetter": letter, "playoffRound": rnd, diff --git a/tests/test_games.py b/tests/test_games.py index 7f7998d..8b8bef7 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -756,9 +756,7 @@ class TestPlayoffEnrichment: } def test_regular_season_game_has_empty_playoff_fields(self, mocker): - mocker.patch( - "app.games.get_team_standings", return_value=self._FULL_STANDINGS - ) + mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS) result = parse_games({"games": [make_game()]}) g = result[0] assert g["Is Playoff"] is False @@ -791,9 +789,7 @@ class TestPlayoffEnrichment: assert "GAME 7" in result[0]["Series Badges"] def test_pinned_game_sorts_first(self, mocker): - mocker.patch( - "app.games.get_team_standings", return_value=self._FULL_STANDINGS - ) + mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS) # Low-priority Game 7 PRE should sort above a high-priority non-playoff LIVE g7_pre = make_playoff_game( top_wins=3, @@ -835,9 +831,7 @@ class TestPlayoffEnrichment: assert result[0]["OT Label"] == "OT" def test_regular_season_ot_not_flagged_as_playoff_ot(self, mocker): - mocker.patch( - "app.games.get_team_standings", return_value=self._FULL_STANDINGS - ) + mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS) game = make_game( game_state="LIVE", period=4, seconds_remaining=180, game_type=2 ) diff --git a/tests/test_playoff_cache.py b/tests/test_playoff_cache.py index a10846a..a923f4e 100644 --- a/tests/test_playoff_cache.py +++ b/tests/test_playoff_cache.py @@ -1,4 +1,3 @@ -import json import time from datetime import datetime from zoneinfo import ZoneInfo @@ -29,6 +28,7 @@ class _Resp: def raise_for_status(self): if self.status_code >= 400: import requests + raise requests.HTTPError(f"HTTP {self.status_code}") @@ -65,8 +65,10 @@ class TestBracket: def test_refresh_failure_returns_none(self, tmp_db, monkeypatch): import requests + def raiser(*a, **kw): raise requests.ConnectionError("boom") + monkeypatch.setattr("app.playoff_cache.requests.get", raiser) assert playoff_cache.refresh_bracket(2026) is None @@ -94,12 +96,14 @@ class TestFetchSeries: def should_not_be_called(*a, **kw): raise AssertionError("network should not be called within TTL") + monkeypatch.setattr("app.playoff_cache.requests.get", should_not_be_called) assert playoff_cache.fetch_series("2026-A") == payload_cached def test_stale_cache_falls_back_on_failure(self, tmp_db, monkeypatch): import requests + key = playoff_cache.series_key("20252026", "A") playoff_cache._put(key, {"from": "stale"}) @@ -114,12 +118,14 @@ class TestFetchSeries: def raiser(*a, **kw): raise requests.ConnectionError("network gone") + monkeypatch.setattr("app.playoff_cache.requests.get", raiser) assert playoff_cache.fetch_series("2026-A") == {"from": "stale"} def test_ancient_stale_cache_returns_none(self, tmp_db, monkeypatch): import requests + key = playoff_cache.series_key("20252026", "A") playoff_cache._put(key, {"from": "ancient"}) @@ -146,9 +152,7 @@ class TestRecordStartDate: def test_records_on_first_playoff_sighting(self, tmp_db): now = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN) - result = playoff_cache.record_start_date_if_missing( - [{"gameType": 3}], now=now - ) + result = playoff_cache.record_start_date_if_missing([{"gameType": 3}], now=now) assert result == "2026-04-18" assert playoff_cache.get_playoff_start_date().isoformat() == "2026-04-18" diff --git a/tests/test_playoff_series.py b/tests/test_playoff_series.py index 382c3f8..6b6fdaf 100644 --- a/tests/test_playoff_series.py +++ b/tests/test_playoff_series.py @@ -233,7 +233,10 @@ class TestTodayMeta: assert meta["round_label"] is None def test_playoff_games_on_mode(self): - games = [make_playoff_game(series_letter="A"), make_playoff_game(series_letter="B")] + games = [ + make_playoff_game(series_letter="A"), + make_playoff_game(series_letter="B"), + ] meta = today_meta(games) assert meta["playoff_mode"] is True assert meta["series_active"] == 2 diff --git a/tests/test_routes.py b/tests/test_routes.py index 6d269ae..0517ea0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -188,8 +188,16 @@ class TestSeriesDetailRoute: "startTimeUTC": "2026-04-18T23:00:00Z", "gameState": "OFF", "periodDescriptor": {"number": 3, "periodType": "REG"}, - "awayTeam": {"abbrev": "OTT", "score": 2, "commonName": {"default": "Senators"}}, - "homeTeam": {"abbrev": "TOR", "score": 6, "commonName": {"default": "Maple Leafs"}}, + "awayTeam": { + "abbrev": "OTT", + "score": 2, + "commonName": {"default": "Senators"}, + }, + "homeTeam": { + "abbrev": "TOR", + "score": 6, + "commonName": {"default": "Maple Leafs"}, + }, "gameOutcome": {"lastPeriodType": "REG"}, }, { @@ -212,12 +220,14 @@ class TestSeriesDetailRoute: def test_valid_format_but_missing_cache_404(self, flask_client, monkeypatch): import app.routes as routes + monkeypatch.setattr(routes, "fetch_series", lambda sid: None) response = flask_client.get("/series/2026-A") assert response.status_code == 404 def test_renders_with_cached_payload(self, flask_client, monkeypatch): import app.routes as routes + monkeypatch.setattr(routes, "fetch_series", lambda sid: self._SAMPLE_PAYLOAD) response = flask_client.get("/series/2026-A") @@ -265,6 +275,7 @@ class TestBracketRoute: def test_bracket_renders_with_cache(self, flask_client, monkeypatch): import app.routes as routes + monkeypatch.setattr(routes, "get_bracket", lambda y: (self._BRACKET, 123)) monkeypatch.setattr( routes, @@ -283,6 +294,7 @@ class TestBracketRoute: self, flask_client, monkeypatch ): import app.routes as routes + monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None)) called = {"n": 0} @@ -298,6 +310,7 @@ class TestBracketRoute: def test_bracket_returns_404_when_no_data(self, flask_client, monkeypatch): import app.routes as routes + monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None)) monkeypatch.setattr(routes, "refresh_bracket", lambda year=None: None) response = flask_client.get("/bracket")