summaryrefslogtreecommitdiff
path: root/nba/box_score.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/box_score.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/box_score.py')
-rw-r--r--nba/box_score.py81
1 files changed, 81 insertions, 0 deletions
diff --git a/nba/box_score.py b/nba/box_score.py
new file mode 100644
index 0000000..8c2ef01
--- /dev/null
+++ b/nba/box_score.py
@@ -0,0 +1,81 @@
+"""
+Fetches and formats live box scores for individual games.
+"""
+
+import json
+
+from tabulate import tabulate
+from nba_api.live.nba.endpoints.boxscore import BoxScore
+
+BOLD = "\033[1m"
+END = "\033[0m"
+
+_HEADERS = ["Player", "Pos", "Min", "Pts", "Reb", "Ast", "Stl", "Blk", "TO", "FG", "3P", "FT", "+/-"]
+
+
+def fetch_box_score(game_id: str) -> dict:
+ endpoint = BoxScore(game_id=game_id)
+ return json.loads(endpoint.get_json())
+
+
+def _parse_minutes(raw: str) -> str:
+ """Convert 'PT35M24.00S' → '35:24'."""
+ if not raw:
+ return "0:00"
+ try:
+ raw = raw.replace("PT", "").replace("S", "")
+ mins, secs = raw.split("M")
+ return f"{int(mins)}:{int(float(secs)):02d}"
+ except Exception:
+ return raw
+
+
+def _player_rows(players: list) -> list:
+ rows = []
+ for p in players:
+ if p.get("status") == "INACTIVE":
+ continue
+ s = p.get("statistics", {})
+ rows.append([
+ p.get("name", ""),
+ p.get("position", ""),
+ _parse_minutes(s.get("minutes", "")),
+ s.get("points", 0),
+ s.get("reboundsTotal", 0),
+ s.get("assists", 0),
+ s.get("steals", 0),
+ s.get("blocks", 0),
+ s.get("turnovers", 0),
+ f"{s.get('fieldGoalsMade', 0)}/{s.get('fieldGoalsAttempted', 0)}",
+ f"{s.get('threePointersMade', 0)}/{s.get('threePointersAttempted', 0)}",
+ f"{s.get('freeThrowsMade', 0)}/{s.get('freeThrowsAttempted', 0)}",
+ s.get("plusMinusPoints", 0),
+ ])
+ return rows
+
+
+def get_box_score_tables(data: dict) -> tuple:
+ """Return (home_table_str, away_table_str)."""
+ game = data["game"]
+ home = game["homeTeam"]
+ away = game["awayTeam"]
+
+ home_title = (
+ f"{BOLD}{home['teamCity']} {home['teamName']} "
+ f"{home['score']} — {away['score']} "
+ f"{away['teamCity']} {away['teamName']}{END} "
+ f" [{game.get('gameStatusText', '')}]"
+ )
+
+ home_rows = _player_rows(home.get("players", []))
+ away_rows = _player_rows(away.get("players", []))
+
+ home_table = (
+ f"{BOLD}{home['teamCity']} {home['teamName']} ({home['score']}){END}\n"
+ + tabulate(home_rows, headers=_HEADERS, tablefmt="grid")
+ )
+ away_table = (
+ f"{BOLD}{away['teamCity']} {away['teamName']} ({away['score']}){END}\n"
+ + tabulate(away_rows, headers=_HEADERS, tablefmt="grid")
+ )
+ return home_table, away_table