style: apply ruff format and fix lint issues in playoff modules
- Rename single-letter `l` loop variables in bracket_view to satisfy E741 - Drop unused `json` import from test_playoff_cache (F401/F811) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+7
-7
@@ -34,13 +34,13 @@ def build_bracket_view(year, bracket_payload, fetched_at=None):
|
|||||||
def slot(letter):
|
def slot(letter):
|
||||||
return _matchup(year, letter, series_by_letter.get(letter))
|
return _matchup(year, letter, series_by_letter.get(letter))
|
||||||
|
|
||||||
east_r1 = [slot(l) for l in EAST_R1]
|
east_r1 = [slot(ltr) for ltr in EAST_R1]
|
||||||
west_r1 = [slot(l) for l in WEST_R1]
|
west_r1 = [slot(ltr) for ltr in WEST_R1]
|
||||||
east_r2 = [slot(l) for l in EAST_R2]
|
east_r2 = [slot(ltr) for ltr in EAST_R2]
|
||||||
west_r2 = [slot(l) for l in WEST_R2]
|
west_r2 = [slot(ltr) for ltr in WEST_R2]
|
||||||
east_cf = [slot(l) for l in EAST_CF]
|
east_cf = [slot(ltr) for ltr in EAST_CF]
|
||||||
west_cf = [slot(l) for l in WEST_CF]
|
west_cf = [slot(ltr) for ltr in WEST_CF]
|
||||||
cup = [slot(l) for l in CUP_FINAL]
|
cup = [slot(ltr) for ltr in CUP_FINAL]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"year": year,
|
"year": year,
|
||||||
|
|||||||
+5
-3
@@ -30,9 +30,11 @@ def series_id(game):
|
|||||||
return None
|
return None
|
||||||
start = game.get("startTimeUTC") or ""
|
start = game.get("startTimeUTC") or ""
|
||||||
try:
|
try:
|
||||||
year = datetime.fromisoformat(start.replace("Z", "+00:00")).astimezone(
|
year = (
|
||||||
EASTERN
|
datetime.fromisoformat(start.replace("Z", "+00:00"))
|
||||||
).year
|
.astimezone(EASTERN)
|
||||||
|
.year
|
||||||
|
)
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
year = datetime.now(EASTERN).year
|
year = datetime.now(EASTERN).year
|
||||||
return f"{year}-{letter.upper()}"
|
return f"{year}-{letter.upper()}"
|
||||||
|
|||||||
+10
-4
@@ -21,8 +21,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
EASTERN = ZoneInfo("America/New_York")
|
EASTERN = ZoneInfo("America/New_York")
|
||||||
|
|
||||||
BRACKET_TTL = 3600 # refresh at this cadence via scheduler
|
BRACKET_TTL = 3600 # refresh at this cadence via scheduler
|
||||||
SERIES_TTL = 300 # lazy cache for per-series schedule fetches
|
SERIES_TTL = 300 # lazy cache for per-series schedule fetches
|
||||||
MAX_STALE_SECONDS = 86400 # 24h
|
MAX_STALE_SECONDS = 86400 # 24h
|
||||||
|
|
||||||
SERIES_ID_RE = re.compile(r"^(20\d{2})-([A-P])$")
|
SERIES_ID_RE = re.compile(r"^(20\d{2})-([A-P])$")
|
||||||
@@ -78,6 +78,7 @@ def _get(cache_key):
|
|||||||
|
|
||||||
# ── Bracket ────────────────────────────────────────────────────────
|
# ── Bracket ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def bracket_key(year):
|
def bracket_key(year):
|
||||||
return f"bracket:{year}"
|
return f"bracket:{year}"
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ def get_bracket(year=None):
|
|||||||
|
|
||||||
# ── Per-series schedule ────────────────────────────────────────────
|
# ── Per-series schedule ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def series_key(season, letter):
|
def series_key(season, letter):
|
||||||
return f"series:{season}:{letter.upper()}"
|
return f"series:{season}:{letter.upper()}"
|
||||||
|
|
||||||
@@ -140,7 +142,9 @@ def fetch_series(series_id):
|
|||||||
if time.time() - fetched < SERIES_TTL:
|
if time.time() - fetched < SERIES_TTL:
|
||||||
return payload
|
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:
|
try:
|
||||||
resp = requests.get(url, timeout=10)
|
resp = requests.get(url, timeout=10)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
@@ -189,7 +193,9 @@ def record_start_date_if_missing(scoreboard_games, now=None):
|
|||||||
|
|
||||||
now = now or datetime.now(EASTERN)
|
now = now or datetime.now(EASTERN)
|
||||||
today = now.date().isoformat()
|
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
|
return today
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
from app.bracket_view import build_bracket_view
|
from app.bracket_view import build_bracket_view
|
||||||
|
|
||||||
|
|
||||||
def _series(letter, top_abbrev, top_id, top_wins, bot_abbrev, bot_id, bot_wins,
|
def _series(
|
||||||
rnd=1, winning_id=None, top_seed="D1", bot_seed="WC1"):
|
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 {
|
return {
|
||||||
"seriesLetter": letter,
|
"seriesLetter": letter,
|
||||||
"playoffRound": rnd,
|
"playoffRound": rnd,
|
||||||
|
|||||||
+3
-9
@@ -756,9 +756,7 @@ class TestPlayoffEnrichment:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_regular_season_game_has_empty_playoff_fields(self, mocker):
|
def test_regular_season_game_has_empty_playoff_fields(self, mocker):
|
||||||
mocker.patch(
|
mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS)
|
||||||
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
|
|
||||||
)
|
|
||||||
result = parse_games({"games": [make_game()]})
|
result = parse_games({"games": [make_game()]})
|
||||||
g = result[0]
|
g = result[0]
|
||||||
assert g["Is Playoff"] is False
|
assert g["Is Playoff"] is False
|
||||||
@@ -791,9 +789,7 @@ class TestPlayoffEnrichment:
|
|||||||
assert "GAME 7" in result[0]["Series Badges"]
|
assert "GAME 7" in result[0]["Series Badges"]
|
||||||
|
|
||||||
def test_pinned_game_sorts_first(self, mocker):
|
def test_pinned_game_sorts_first(self, mocker):
|
||||||
mocker.patch(
|
mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS)
|
||||||
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
|
|
||||||
)
|
|
||||||
# Low-priority Game 7 PRE should sort above a high-priority non-playoff LIVE
|
# Low-priority Game 7 PRE should sort above a high-priority non-playoff LIVE
|
||||||
g7_pre = make_playoff_game(
|
g7_pre = make_playoff_game(
|
||||||
top_wins=3,
|
top_wins=3,
|
||||||
@@ -835,9 +831,7 @@ class TestPlayoffEnrichment:
|
|||||||
assert result[0]["OT Label"] == "OT"
|
assert result[0]["OT Label"] == "OT"
|
||||||
|
|
||||||
def test_regular_season_ot_not_flagged_as_playoff_ot(self, mocker):
|
def test_regular_season_ot_not_flagged_as_playoff_ot(self, mocker):
|
||||||
mocker.patch(
|
mocker.patch("app.games.get_team_standings", return_value=self._FULL_STANDINGS)
|
||||||
"app.games.get_team_standings", return_value=self._FULL_STANDINGS
|
|
||||||
)
|
|
||||||
game = make_game(
|
game = make_game(
|
||||||
game_state="LIVE", period=4, seconds_remaining=180, game_type=2
|
game_state="LIVE", period=4, seconds_remaining=180, game_type=2
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
@@ -29,6 +28,7 @@ class _Resp:
|
|||||||
def raise_for_status(self):
|
def raise_for_status(self):
|
||||||
if self.status_code >= 400:
|
if self.status_code >= 400:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
raise requests.HTTPError(f"HTTP {self.status_code}")
|
raise requests.HTTPError(f"HTTP {self.status_code}")
|
||||||
|
|
||||||
|
|
||||||
@@ -65,8 +65,10 @@ class TestBracket:
|
|||||||
|
|
||||||
def test_refresh_failure_returns_none(self, tmp_db, monkeypatch):
|
def test_refresh_failure_returns_none(self, tmp_db, monkeypatch):
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
def raiser(*a, **kw):
|
def raiser(*a, **kw):
|
||||||
raise requests.ConnectionError("boom")
|
raise requests.ConnectionError("boom")
|
||||||
|
|
||||||
monkeypatch.setattr("app.playoff_cache.requests.get", raiser)
|
monkeypatch.setattr("app.playoff_cache.requests.get", raiser)
|
||||||
assert playoff_cache.refresh_bracket(2026) is None
|
assert playoff_cache.refresh_bracket(2026) is None
|
||||||
|
|
||||||
@@ -94,12 +96,14 @@ class TestFetchSeries:
|
|||||||
|
|
||||||
def should_not_be_called(*a, **kw):
|
def should_not_be_called(*a, **kw):
|
||||||
raise AssertionError("network should not be called within TTL")
|
raise AssertionError("network should not be called within TTL")
|
||||||
|
|
||||||
monkeypatch.setattr("app.playoff_cache.requests.get", should_not_be_called)
|
monkeypatch.setattr("app.playoff_cache.requests.get", should_not_be_called)
|
||||||
|
|
||||||
assert playoff_cache.fetch_series("2026-A") == payload_cached
|
assert playoff_cache.fetch_series("2026-A") == payload_cached
|
||||||
|
|
||||||
def test_stale_cache_falls_back_on_failure(self, tmp_db, monkeypatch):
|
def test_stale_cache_falls_back_on_failure(self, tmp_db, monkeypatch):
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
key = playoff_cache.series_key("20252026", "A")
|
key = playoff_cache.series_key("20252026", "A")
|
||||||
playoff_cache._put(key, {"from": "stale"})
|
playoff_cache._put(key, {"from": "stale"})
|
||||||
|
|
||||||
@@ -114,12 +118,14 @@ class TestFetchSeries:
|
|||||||
|
|
||||||
def raiser(*a, **kw):
|
def raiser(*a, **kw):
|
||||||
raise requests.ConnectionError("network gone")
|
raise requests.ConnectionError("network gone")
|
||||||
|
|
||||||
monkeypatch.setattr("app.playoff_cache.requests.get", raiser)
|
monkeypatch.setattr("app.playoff_cache.requests.get", raiser)
|
||||||
|
|
||||||
assert playoff_cache.fetch_series("2026-A") == {"from": "stale"}
|
assert playoff_cache.fetch_series("2026-A") == {"from": "stale"}
|
||||||
|
|
||||||
def test_ancient_stale_cache_returns_none(self, tmp_db, monkeypatch):
|
def test_ancient_stale_cache_returns_none(self, tmp_db, monkeypatch):
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
key = playoff_cache.series_key("20252026", "A")
|
key = playoff_cache.series_key("20252026", "A")
|
||||||
playoff_cache._put(key, {"from": "ancient"})
|
playoff_cache._put(key, {"from": "ancient"})
|
||||||
|
|
||||||
@@ -146,9 +152,7 @@ class TestRecordStartDate:
|
|||||||
|
|
||||||
def test_records_on_first_playoff_sighting(self, tmp_db):
|
def test_records_on_first_playoff_sighting(self, tmp_db):
|
||||||
now = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN)
|
now = datetime(2026, 4, 18, 20, 0, tzinfo=EASTERN)
|
||||||
result = playoff_cache.record_start_date_if_missing(
|
result = playoff_cache.record_start_date_if_missing([{"gameType": 3}], now=now)
|
||||||
[{"gameType": 3}], now=now
|
|
||||||
)
|
|
||||||
assert result == "2026-04-18"
|
assert result == "2026-04-18"
|
||||||
assert playoff_cache.get_playoff_start_date().isoformat() == "2026-04-18"
|
assert playoff_cache.get_playoff_start_date().isoformat() == "2026-04-18"
|
||||||
|
|
||||||
|
|||||||
@@ -233,7 +233,10 @@ class TestTodayMeta:
|
|||||||
assert meta["round_label"] is None
|
assert meta["round_label"] is None
|
||||||
|
|
||||||
def test_playoff_games_on_mode(self):
|
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)
|
meta = today_meta(games)
|
||||||
assert meta["playoff_mode"] is True
|
assert meta["playoff_mode"] is True
|
||||||
assert meta["series_active"] == 2
|
assert meta["series_active"] == 2
|
||||||
|
|||||||
+15
-2
@@ -188,8 +188,16 @@ class TestSeriesDetailRoute:
|
|||||||
"startTimeUTC": "2026-04-18T23:00:00Z",
|
"startTimeUTC": "2026-04-18T23:00:00Z",
|
||||||
"gameState": "OFF",
|
"gameState": "OFF",
|
||||||
"periodDescriptor": {"number": 3, "periodType": "REG"},
|
"periodDescriptor": {"number": 3, "periodType": "REG"},
|
||||||
"awayTeam": {"abbrev": "OTT", "score": 2, "commonName": {"default": "Senators"}},
|
"awayTeam": {
|
||||||
"homeTeam": {"abbrev": "TOR", "score": 6, "commonName": {"default": "Maple Leafs"}},
|
"abbrev": "OTT",
|
||||||
|
"score": 2,
|
||||||
|
"commonName": {"default": "Senators"},
|
||||||
|
},
|
||||||
|
"homeTeam": {
|
||||||
|
"abbrev": "TOR",
|
||||||
|
"score": 6,
|
||||||
|
"commonName": {"default": "Maple Leafs"},
|
||||||
|
},
|
||||||
"gameOutcome": {"lastPeriodType": "REG"},
|
"gameOutcome": {"lastPeriodType": "REG"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -212,12 +220,14 @@ class TestSeriesDetailRoute:
|
|||||||
|
|
||||||
def test_valid_format_but_missing_cache_404(self, flask_client, monkeypatch):
|
def test_valid_format_but_missing_cache_404(self, flask_client, monkeypatch):
|
||||||
import app.routes as routes
|
import app.routes as routes
|
||||||
|
|
||||||
monkeypatch.setattr(routes, "fetch_series", lambda sid: None)
|
monkeypatch.setattr(routes, "fetch_series", lambda sid: None)
|
||||||
response = flask_client.get("/series/2026-A")
|
response = flask_client.get("/series/2026-A")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
def test_renders_with_cached_payload(self, flask_client, monkeypatch):
|
def test_renders_with_cached_payload(self, flask_client, monkeypatch):
|
||||||
import app.routes as routes
|
import app.routes as routes
|
||||||
|
|
||||||
monkeypatch.setattr(routes, "fetch_series", lambda sid: self._SAMPLE_PAYLOAD)
|
monkeypatch.setattr(routes, "fetch_series", lambda sid: self._SAMPLE_PAYLOAD)
|
||||||
|
|
||||||
response = flask_client.get("/series/2026-A")
|
response = flask_client.get("/series/2026-A")
|
||||||
@@ -265,6 +275,7 @@ class TestBracketRoute:
|
|||||||
|
|
||||||
def test_bracket_renders_with_cache(self, flask_client, monkeypatch):
|
def test_bracket_renders_with_cache(self, flask_client, monkeypatch):
|
||||||
import app.routes as routes
|
import app.routes as routes
|
||||||
|
|
||||||
monkeypatch.setattr(routes, "get_bracket", lambda y: (self._BRACKET, 123))
|
monkeypatch.setattr(routes, "get_bracket", lambda y: (self._BRACKET, 123))
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
routes,
|
routes,
|
||||||
@@ -283,6 +294,7 @@ class TestBracketRoute:
|
|||||||
self, flask_client, monkeypatch
|
self, flask_client, monkeypatch
|
||||||
):
|
):
|
||||||
import app.routes as routes
|
import app.routes as routes
|
||||||
|
|
||||||
monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None))
|
monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None))
|
||||||
called = {"n": 0}
|
called = {"n": 0}
|
||||||
|
|
||||||
@@ -298,6 +310,7 @@ class TestBracketRoute:
|
|||||||
|
|
||||||
def test_bracket_returns_404_when_no_data(self, flask_client, monkeypatch):
|
def test_bracket_returns_404_when_no_data(self, flask_client, monkeypatch):
|
||||||
import app.routes as routes
|
import app.routes as routes
|
||||||
|
|
||||||
monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None))
|
monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None))
|
||||||
monkeypatch.setattr(routes, "refresh_bracket", lambda year=None: None)
|
monkeypatch.setattr(routes, "refresh_bracket", lambda year=None: None)
|
||||||
response = flask_client.get("/bracket")
|
response = flask_client.get("/bracket")
|
||||||
|
|||||||
Reference in New Issue
Block a user