Files
NHL-Scoreboard/app/bracket_view.py
T
josh 930247b32f
CI / Lint (push) Successful in 8s
CI / Test (push) Successful in 10s
CI / Build & Push (push) Successful in 22s
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>
2026-04-19 12:48:42 -04:00

133 lines
4.0 KiB
Python

"""Normalize NHL /v1/playoff-bracket payloads for the bracket template.
The NHL bracket uses stable series letters:
A,B,C,D = Round 1 East E,F,G,H = Round 1 West
I,J = Round 2 East K,L = Round 2 West
M = Conf Final East N = Conf Final West
O = Stanley Cup Final
"""
from app.playoff import ROUND_LABELS
EAST_R1 = ["A", "B", "C", "D"]
WEST_R1 = ["E", "F", "G", "H"]
EAST_R2 = ["I", "J"]
WEST_R2 = ["K", "L"]
EAST_CF = ["M"]
WEST_CF = ["N"]
CUP_FINAL = ["O"]
def build_bracket_view(year, bracket_payload, fetched_at=None):
"""Shape the raw bracket API payload for bracket.html.
Returns a dict of rounds grouped by conference, plus a flat `matchups` list
keyed by letter for the mobile accordion. Missing letters render as empty
placeholder slots so the grid stays visually complete before upsets decide.
"""
series_by_letter = {}
for s in (bracket_payload or {}).get("series", []):
letter = s.get("seriesLetter")
if letter:
series_by_letter[letter] = s
def slot(letter):
return _matchup(year, letter, series_by_letter.get(letter))
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,
"fetched_at": fetched_at,
"bracket_logo": (bracket_payload or {}).get("bracketLogo"),
"east_r1": east_r1,
"west_r1": west_r1,
"east_r2": east_r2,
"west_r2": west_r2,
"east_cf": east_cf,
"west_cf": west_cf,
"cup": cup,
"rounds": [
{"label": ROUND_LABELS[1], "east": east_r1, "west": west_r1},
{"label": ROUND_LABELS[2], "east": east_r2, "west": west_r2},
{"label": ROUND_LABELS[3], "east": east_cf, "west": west_cf},
{"label": ROUND_LABELS[4], "cup": cup},
],
}
def _matchup(year, letter, series):
"""Render-ready dict for one bracket slot. Empty when the series is unknown."""
if not series:
return {
"letter": letter,
"series_id": f"{year}-{letter}",
"empty": True,
"top": None,
"bottom": None,
"top_wins": 0,
"bottom_wins": 0,
"round": None,
"winner_abbrev": None,
"state": "pending",
}
top = series.get("topSeedTeam") or {}
bot = series.get("bottomSeedTeam") or {}
top_wins = _to_int(series.get("topSeedWins"))
bot_wins = _to_int(series.get("bottomSeedWins"))
winning_id = series.get("winningTeamId")
winner_abbrev = None
if winning_id is not None:
if top.get("id") == winning_id:
winner_abbrev = top.get("abbrev")
elif bot.get("id") == winning_id:
winner_abbrev = bot.get("abbrev")
if winner_abbrev:
state = "complete"
elif top_wins > 0 or bot_wins > 0:
state = "active"
else:
state = "upcoming"
return {
"letter": letter,
"series_id": f"{year}-{letter}",
"empty": False,
"top": _team(top, series.get("topSeedRankAbbrev")),
"bottom": _team(bot, series.get("bottomSeedRankAbbrev")),
"top_wins": top_wins,
"bottom_wins": bot_wins,
"round": series.get("playoffRound"),
"winner_abbrev": winner_abbrev,
"state": state,
}
def _team(team, seed_abbrev=None):
if not team:
return None
return {
"id": team.get("id"),
"abbrev": team.get("abbrev"),
"name": (team.get("name") or {}).get("default"),
"common_name": (team.get("commonName") or {}).get("default"),
"logo": team.get("darkLogo") or team.get("logo"),
"seed": seed_abbrev,
}
def _to_int(v, default=0):
try:
return int(v)
except (TypeError, ValueError):
return default