diff options
| author | Christian Cleberg <[email protected]> | 2026-04-15 22:28:45 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-15 22:28:45 -0500 |
| commit | 4cbe592e0a0fae13f0a89918a66fd983dd84464f (patch) | |
| tree | c2aac9d6e3c7b0f44adf07b86385f2ce06f1cf10 /nba/box_score.py | |
| parent | 00f695c765aead8aa0160a3c676cf85716178beb (diff) | |
| download | nba-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.py | 81 |
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 |
