From 4cbe592e0a0fae13f0a89918a66fd983dd84464f Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 15 Apr 2026 22:28:45 -0500 Subject: feat: add Textual TUI with auto-refresh, leaders, playoff picture, and box score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces an interactive terminal UI via the `--tui` flag backed by the Textual framework. Scores and standings auto-refresh on a configurable interval (default 60s, minimum 10s) using asyncio with nba_api calls offloaded to a thread pool to keep the event loop unblocked. New tabs: - Scores / Standings (existing views, now live-updating) - Leaders: top-25 by stat category, cycle with < / > keys - Playoff Picture: conference seeding with clinch/elimination status - Box Score: per-player live stats, load any game with keys 1–9 Refactored scores.py and standings.py to separate rendering from I/O (get_scoreboard_table, get_east/west_standings_table), keeping the existing static CLI path fully intact. Added -sc / -st shortcut flags. Adds textual>=0.89.1,<7.0 dependency, build-system declaration for uv, and package-data entry for the bundled .tcss stylesheet. --- nba/playoff.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 nba/playoff.py (limited to 'nba/playoff.py') diff --git a/nba/playoff.py b/nba/playoff.py new file mode 100644 index 0000000..eb3b12b --- /dev/null +++ b/nba/playoff.py @@ -0,0 +1,80 @@ +""" +Fetches and formats the NBA playoff picture. +""" + +import json + +from tabulate import tabulate +from nba_api.stats.endpoints.playoffpicture import PlayoffPicture + +BOLD = "\033[1m" +END = "\033[0m" +RED = "\033[91m" +GREEN = "\033[32m" +YELLOW = "\033[33m" + + +def fetch_playoff_picture() -> dict: + endpoint = PlayoffPicture() + return json.loads(endpoint.get_json()) + + +def _clinch_status(row: list, idx: dict) -> str: + """Return a color-coded status string from clinch/elimination columns.""" + def val(col): + return row[idx[col]] if col in idx else None + + if val("CLINCHED_CONFERENCE"): + return f"{BOLD}{GREEN}z-Clinched Conf{END}" + if val("CLINCHED_DIVISION") or val("CLINCHED_PLAYOFFS"): + return f"{GREEN}x-Clinched{END}" + if val("Clinched_Play_In"): + return f"{YELLOW}pi-Play-In{END}" + if val("ELIMINATED_PLAYOFFS"): + return f"{RED}e-Eliminated{END}" + return "" + + +def _build_conference_table(result_sets: list, name: str) -> str: + """Build a formatted playoff standings table for one conference.""" + rs = next((r for r in result_sets if r["name"] == name), None) + if rs is None or not rs["rowSet"]: + return "No data available." + + headers = rs["headers"] + rows = rs["rowSet"] + idx = {h: i for i, h in enumerate(headers)} + + def get(row, col, default=""): + return row[idx[col]] if col in idx else default + + table_data = [] + for row in rows: + wins = get(row, "WINS") + losses = get(row, "LOSSES") + pct = get(row, "PCT") + pct_str = f"{float(pct):.3f}" if pct not in ("", None) else "" + table_data.append([ + get(row, "RANK"), + get(row, "TEAM"), + f"{wins}-{losses}", + pct_str, + get(row, "GB"), + get(row, "HOME"), + get(row, "AWAY"), + get(row, "CONF"), + _clinch_status(row, idx), + ]) + + display_headers = ["#", "Team", "W-L", "PCT", "GB", "HOME", "AWAY", "CONF", "Status"] + conf_label = "Eastern" if "East" in name else "Western" + title = f"{BOLD}{conf_label} Conference Playoff Picture:{END}" + return title + "\n" + tabulate(table_data, headers=display_headers, tablefmt="grid") + + +def get_west_playoff_table(data: dict) -> str: + return _build_conference_table(data["resultSets"], "WestConfStandings") + + +def get_east_playoff_table(data: dict) -> str: + return _build_conference_table(data["resultSets"], "EastConfStandings") -- cgit v1.2.3