summaryrefslogtreecommitdiff
path: root/nba/playoff.py
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-15 22:28:45 -0500
committerChristian Cleberg <[email protected]>2026-04-15 22:28:45 -0500
commit4cbe592e0a0fae13f0a89918a66fd983dd84464f (patch)
treec2aac9d6e3c7b0f44adf07b86385f2ce06f1cf10 /nba/playoff.py
parent00f695c765aead8aa0160a3c676cf85716178beb (diff)
downloadnba-scores-4cbe592e0a0fae13f0a89918a66fd983dd84464f.tar.gz
nba-scores-4cbe592e0a0fae13f0a89918a66fd983dd84464f.tar.bz2
nba-scores-4cbe592e0a0fae13f0a89918a66fd983dd84464f.zip
feat: add Textual TUI with auto-refresh, leaders, playoff picture, and box score
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.
Diffstat (limited to 'nba/playoff.py')
-rw-r--r--nba/playoff.py80
1 files changed, 80 insertions, 0 deletions
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")