Commit 232e808bd8

232e808bd84bf16ad2459a2324a43c0e0ba7133b

parent: e4a4929d48

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-22 23:41 UTC

Untrack build artifacts

build/ and nba_scores.egg-info/ were committed — 21 files of generated output
that made every diff on the package unreadable.

.gitignore already listed nba-scores.egg-info with a hyphen; setuptools writes
the directory with an underscore, which is why it was never matched.

Layout: unified · split

.gitignore +3
@@ -2,5 +2,8 @@
2venv/ 2venv/
3dist/ 3dist/
4nba-scores.egg-info* 4nba-scores.egg-info*
5nba_scores.egg-info/
5nba/__pycache__/ 6nba/__pycache__/
6nba/tui/__pycache__/ 7nba/tui/__pycache__/
8build/
9*.py[cod]
build/lib/nba/__init__.py deleted
build/lib/nba/__main__.py deleted −8
@@ -1,8 +0,0 @@
1"""
2Entry point for the app.
3"""
4
5if __name__ == "__main__":
6 from nba.cli import nba
7
8 nba()
build/lib/nba/__pycache__/bracket.cpython-314.pyc deleted

Binary file not shown.

build/lib/nba/box_score.py deleted −90
@@ -1,90 +0,0 @@
1"""
2Fetches and formats live box scores for individual games.
3"""
4
5import json
6
7from tabulate import tabulate
8from nba_api.live.nba.endpoints.boxscore import BoxScore
9
10BOLD = "\033[1m"
11END = "\033[0m"
12
13_HEADERS = [
14 "Player",
15 "Pos",
16 "Min",
17 "Pts",
18 "Reb",
19 "Ast",
20 "Stl",
21 "Blk",
22 "TO",
23 "FG",
24 "3P",
25 "FT",
26 "+/-",
27]
28
29
30def fetch_box_score(game_id: str) -> dict:
31 endpoint = BoxScore(game_id=game_id)
32 return json.loads(endpoint.get_json())
33
34
35def _parse_minutes(raw: str) -> str:
36 """Convert 'PT35M24.00S' → '35:24'."""
37 if not raw:
38 return "0:00"
39 try:
40 raw = raw.replace("PT", "").replace("S", "")
41 mins, secs = raw.split("M")
42 return f"{int(mins)}:{int(float(secs)):02d}"
43 except Exception:
44 return raw
45
46
47def _player_rows(players: list) -> list:
48 rows = []
49 for p in players:
50 if p.get("status") == "INACTIVE":
51 continue
52 s = p.get("statistics", {})
53 rows.append(
54 [
55 p.get("name", ""),
56 p.get("position", ""),
57 _parse_minutes(s.get("minutes", "")),
58 s.get("points", 0),
59 s.get("reboundsTotal", 0),
60 s.get("assists", 0),
61 s.get("steals", 0),
62 s.get("blocks", 0),
63 s.get("turnovers", 0),
64 f"{s.get('fieldGoalsMade', 0)}/{s.get('fieldGoalsAttempted', 0)}",
65 f"{s.get('threePointersMade', 0)}/{s.get('threePointersAttempted', 0)}",
66 f"{s.get('freeThrowsMade', 0)}/{s.get('freeThrowsAttempted', 0)}",
67 s.get("plusMinusPoints", 0),
68 ]
69 )
70 return rows
71
72
73def get_box_score_tables(data: dict) -> tuple:
74 """Return (home_table_str, away_table_str)."""
75 game = data["game"]
76 home = game["homeTeam"]
77 away = game["awayTeam"]
78
79 home_rows = _player_rows(home.get("players", []))
80 away_rows = _player_rows(away.get("players", []))
81
82 home_table = (
83 f"{BOLD}{home['teamCity']} {home['teamName']} ({home['score']}){END}\n"
84 + tabulate(home_rows, headers=_HEADERS, tablefmt="grid")
85 )
86 away_table = (
87 f"{BOLD}{away['teamCity']} {away['teamName']} ({away['score']}){END}\n"
88 + tabulate(away_rows, headers=_HEADERS, tablefmt="grid")
89 )
90 return home_table, away_table
build/lib/nba/bracket.py deleted −309
@@ -1,309 +0,0 @@
1"""
2Fetches and formats the NBA playoff bracket.
3"""
4
5import json
6import re
7from collections import defaultdict
8from typing import Optional
9
10from nba_api.stats.endpoints.commonplayoffseries import CommonPlayoffSeries
11from nba_api.stats.endpoints.leaguegamelog import LeagueGameLog
12from nba_api.stats.static import teams
13
14BOLD = "\033[1m"
15END = "\033[0m"
16GREEN = "\033[32m"
17RED = "\033[91m"
18YELLOW = "\033[33m"
19CYAN = "\033[36m"
20ANSI_RE = re.compile(r"\033\[[0-9;]*m")
21
22ROUND_LABELS = {
23 1: "First Round",
24 2: "Conference Semifinals",
25 3: "Conference Finals",
26 4: "NBA Finals",
27}
28
29
30def fetch_bracket() -> dict:
31 """
32 Fetch playoff series and completed playoff games.
33
34 CommonPlayoffSeries provides the bracket's scheduled series and games. The
35 playoff game log is used to compute each series record from completed games.
36 """
37 series_endpoint = CommonPlayoffSeries()
38 game_log_endpoint = LeagueGameLog(season_type_all_star="Playoffs")
39 return {
40 "series": json.loads(series_endpoint.get_json()),
41 "game_log": json.loads(game_log_endpoint.get_json()),
42 }
43
44
45def get_bracket_table(data: dict) -> str:
46 """Return a formatted playoff bracket."""
47 result_sets = data["series"]["resultSets"]
48 series_set = next((r for r in result_sets if r["name"] == "PlayoffSeries"), None)
49 if series_set is None or not series_set["rowSet"]:
50 return "No playoff bracket data available."
51
52 headers = series_set["headers"]
53 idx = {header: i for i, header in enumerate(headers)}
54 team_map = _team_map()
55 series_games = _group_series(series_set["rowSet"], idx)
56 series_wins = _series_wins(data["game_log"], series_games)
57 summaries = _series_summaries(team_map, series_games, series_wins)
58
59 return "\n".join(
60 [
61 _center(f"{BOLD}NBA Playoff Bracket{END}", 74),
62 f"{GREEN}* Advanced{END} {RED}x Eliminated{END} "
63 f"{CYAN}> Series lead{END} {YELLOW}! Can clinch next win{END}",
64 "",
65 _render_conference("Western", summaries),
66 "",
67 _render_finals(summaries),
68 "",
69 _render_conference("Eastern", summaries),
70 ]
71 )
72
73
74def _team_map() -> dict:
75 return {
76 team["id"]: {
77 "abbr": team["abbreviation"],
78 "name": f"{team['city']} {team['nickname']}",
79 }
80 for team in teams.get_teams()
81 }
82
83
84def _group_series(rows: list, idx: dict) -> dict:
85 series_games = defaultdict(list)
86 for row in rows:
87 series_id = row[idx["SERIES_ID"]]
88 series_games[series_id].append(
89 {
90 "game_id": row[idx["GAME_ID"]],
91 "game_num": row[idx["GAME_NUM"]],
92 "home_team_id": row[idx["HOME_TEAM_ID"]],
93 "visitor_team_id": row[idx["VISITOR_TEAM_ID"]],
94 }
95 )
96
97 return {
98 series_id: sorted(games, key=lambda game: game["game_num"])
99 for series_id, games in series_games.items()
100 }
101
102
103def _series_wins(game_log: dict, series_games: dict) -> dict:
104 game_to_series = {
105 game["game_id"]: series_id
106 for series_id, games in series_games.items()
107 for game in games
108 }
109 wins = defaultdict(lambda: defaultdict(int))
110
111 for series_id in series_games:
112 wins[series_id]["completed_games"] = set()
113
114 result_set = game_log["resultSets"][0]
115 idx = {header: i for i, header in enumerate(result_set["headers"])}
116 for row in result_set["rowSet"]:
117 game_id = row[idx["GAME_ID"]]
118 series_id = game_to_series.get(game_id)
119 if series_id is None or row[idx["WL"]] != "W":
120 continue
121
122 team_id = row[idx["TEAM_ID"]]
123 wins[series_id][team_id] += 1
124 wins[series_id]["completed_games"].add(game_id)
125
126 return wins
127
128
129def _series_summaries(team_map: dict, series_games: dict, series_wins: dict) -> dict:
130 summaries = {}
131 for series_id, games in series_games.items():
132 first_game = games[0]
133 home_id = first_game["home_team_id"]
134 visitor_id = first_game["visitor_team_id"]
135 wins = series_wins[series_id]
136 home_wins = wins.get(home_id, 0)
137 visitor_wins = wins.get(visitor_id, 0)
138
139 summaries[series_id] = {
140 "conference": _conference(series_id),
141 "round": _round_number(series_id),
142 "slot": _series_slot(series_id),
143 "home_id": home_id,
144 "visitor_id": visitor_id,
145 "home_abbr": _abbr(team_map, home_id),
146 "visitor_abbr": _abbr(team_map, visitor_id),
147 "home_wins": home_wins,
148 "visitor_wins": visitor_wins,
149 "winner": _winner(team_map, home_id, visitor_id, home_wins, visitor_wins),
150 }
151 return summaries
152
153
154def _render_conference(conference: str, summaries: dict) -> str:
155 first_round = _conference_round(summaries, conference, 1)
156 semifinals = _conference_round(summaries, conference, 2)
157 finals = _conference_round(summaries, conference, 3)
158 final = finals[0] if finals else None
159
160 lines = [
161 f"{BOLD}{conference.upper()} CONFERENCE{END}",
162 "First Round Semifinals Conference Finals",
163 ]
164
165 lines.extend(
166 [
167 f"{_series_box(_slot(first_round, 0), 24)} ┐",
168 f"{_blank(24)} ├── {_series_box(_slot(semifinals, 0), 24)} ┐",
169 f"{_series_box(_slot(first_round, 1), 24)} ┘ {_blank(24)} │",
170 f"{_blank(24)} {_blank(24)} ├── {_series_box(final, 24)}",
171 f"{_series_box(_slot(first_round, 2), 24)} ┐ {_blank(24)} │",
172 f"{_blank(24)} ├── {_series_box(_slot(semifinals, 1), 24)} ┘",
173 f"{_series_box(_slot(first_round, 3), 24)} ┘",
174 ]
175 )
176
177 return "\n".join(lines)
178
179
180def _render_finals(summaries: dict) -> str:
181 finals = [summary for summary in summaries.values() if summary["round"] == 4]
182 final = finals[0] if finals else None
183
184 lines = [
185 f"{BOLD}NBA FINALS{END}",
186 "West Champion East Champion",
187 f"{_series_box(final, 24)}",
188 ]
189 if final is None:
190 lines.append("Winner TBD")
191 elif final["winner"]:
192 lines.append(f"{BOLD}{GREEN}{final['winner']} wins the Finals{END}")
193 return "\n".join(lines)
194
195
196def _conference_round(summaries: dict, conference: str, round_number: int) -> list:
197 return sorted(
198 [
199 summary
200 for summary in summaries.values()
201 if summary["conference"] == conference[:4]
202 and summary["round"] == round_number
203 ],
204 key=lambda summary: summary["slot"],
205 )
206
207
208def _slot(items: list, idx: int) -> Optional[dict]:
209 return items[idx] if idx < len(items) else None
210
211
212def _series_box(summary: Optional[dict], width: int) -> str:
213 if summary is None:
214 return _blank(width, "TBD")
215
216 visitor = _team_line(
217 summary["visitor_abbr"],
218 summary["visitor_wins"],
219 summary["home_wins"],
220 summary["winner"] == summary["visitor_abbr"],
221 summary["winner"] == summary["home_abbr"],
222 )
223 home = _team_line(
224 summary["home_abbr"],
225 summary["home_wins"],
226 summary["visitor_wins"],
227 summary["winner"] == summary["home_abbr"],
228 summary["winner"] == summary["visitor_abbr"],
229 )
230 return _pad_ansi(f"{visitor} / {home}", width)
231
232
233def _team_line(
234 abbr: str,
235 wins: int,
236 opponent_wins: int,
237 won_series: bool,
238 lost_series: bool,
239) -> str:
240 label = f"{abbr} {wins}"
241 if won_series:
242 return f"{BOLD}{GREEN}{label}*{END}"
243 if lost_series:
244 return f"{RED}{label}x{END}"
245 if wins == 3:
246 return f"{BOLD}{YELLOW}{label}!{END}"
247 if wins > opponent_wins:
248 return f"{CYAN}{label}>{END}"
249 return label
250
251
252def _winner(
253 team_map: dict,
254 home_id: int,
255 visitor_id: int,
256 home_wins: int,
257 visitor_wins: int,
258) -> str:
259 if home_wins >= 4:
260 return _abbr(team_map, home_id)
261 if visitor_wins >= 4:
262 return _abbr(team_map, visitor_id)
263 return ""
264
265
266def _blank(width: int, text: str = "") -> str:
267 return text.ljust(width)
268
269
270def _center(text: str, width: int) -> str:
271 return text.center(width)
272
273
274def _pad_ansi(text: str, width: int) -> str:
275 visible_length = len(ANSI_RE.sub("", text))
276 return text + " " * max(width - visible_length, 0)
277
278
279def _round_number(series_id: str) -> int:
280 try:
281 return int(series_id[-3:-1])
282 except ValueError:
283 return 0
284
285
286def _series_slot(series_id: str) -> int:
287 try:
288 return int(series_id[-1])
289 except ValueError:
290 return 0
291
292
293def _conference(series_id: str) -> str:
294 round_number = _round_number(series_id)
295 slot = _series_slot(series_id)
296
297 if round_number == 4:
298 return "NBA"
299 if round_number == 3:
300 return "East" if slot == 0 else "West"
301 if round_number == 2:
302 return "East" if slot <= 1 else "West"
303 if round_number == 1:
304 return "East" if slot <= 3 else "West"
305 return ""
306
307
308def _abbr(team_map: dict, team_id: int) -> str:
309 return team_map.get(team_id, {}).get("abbr", str(team_id))
build/lib/nba/cli.py deleted −50
@@ -1,50 +0,0 @@
1"""
2This script uses argparse to parse command line arguments.
3
4It imports the required modules and sets up a parser with basic options for demonstration purposes.
5"""
6
7import argparse
8from nba import fetch_data, scores, standings
9
10
11def nba() -> None:
12 """
13 Parse command-line arguments and display either scoreboard or standings.
14 """
15 parser = argparse.ArgumentParser(description="NBA Scoreboard and Standings")
16 parser.add_argument(
17 "--scores", "-sc", action="store_true", help="Display the scoreboard"
18 )
19 parser.add_argument(
20 "--standings", "-st", action="store_true", help="Display the standings"
21 )
22 parser.add_argument("--tui", action="store_true", help="Launch the interactive TUI")
23 parser.add_argument(
24 "--refresh",
25 type=int,
26 default=60,
27 metavar="SECONDS",
28 help="Auto-refresh interval in TUI mode (default: 60, minimum: 10)",
29 )
30 args = parser.parse_args()
31
32 if args.tui:
33 from nba.tui.app import NBAApp
34
35 initial_tab = "standings" if args.standings else "scores"
36 refresh_interval = max(args.refresh, 10)
37 NBAApp(initial_tab=initial_tab, refresh_interval=refresh_interval).run()
38 return
39
40 # Legacy static mode
41 games, ranks = fetch_data.fetch_data()
42
43 if args.scores:
44 scores.build_scoreboard(games, ranks)
45 elif args.standings:
46 standings.build_standings(ranks)
47 else:
48 print(
49 "Please specify --scores or --standings (or use --tui for interactive mode)"
50 )
build/lib/nba/fetch_data.py deleted −30
@@ -1,30 +0,0 @@
1"""
2Fetches data for use in other modules.
3"""
4
5import json
6from nba_api.live.nba.endpoints import scoreboard
7from nba_api.stats.endpoints import leaguestandings
8
9
10def fetch_data() -> tuple:
11 """
12 Fetches live NBA scoreboard data and standings from the NBA API.
13
14 Returns:
15 games (dict): JSON parsed games data.
16 standings (dict): JSON parsed team standings data.
17 """
18 # Get today's scoreboard data
19 games_endpoint = scoreboard.ScoreBoard()
20 games_json = games_endpoint.get_json()
21
22 # Get league standings
23 standings_endpoint = leaguestandings.LeagueStandings()
24 standings_json = standings_endpoint.get_json()
25
26 # Parse the JSON strings into Python dictionaries
27 games = json.loads(games_json)
28 standings = json.loads(standings_json)
29
30 return games, standings
build/lib/nba/leaders.py deleted −72
@@ -1,72 +0,0 @@
1"""
2Fetches and formats NBA statistical leaders.
3"""
4
5import json
6
7from tabulate import tabulate
8from nba_api.stats.endpoints.leagueleaders import LeagueLeaders
9
10BOLD = "\033[1m"
11END = "\033[0m"
12
13# Ordered list of (api_abbreviation, display_label)
14CATEGORIES = [
15 ("PTS", "Points"),
16 ("REB", "Rebounds"),
17 ("AST", "Assists"),
18 ("STL", "Steals"),
19 ("BLK", "Blocks"),
20 ("EFF", "Efficiency"),
21 ("FG_PCT", "FG%"),
22 ("FT_PCT", "FT%"),
23 ("FG3_PCT", "3P%"),
24]
25
26# Extra columns to show alongside RANK, PLAYER, TEAM, GP for each category
27_EXTRA_COLS = {
28 "PTS": ["PTS", "FGM", "FGA", "FG_PCT", "FTM", "FTA", "FT_PCT"],
29 "REB": ["REB", "OREB", "DREB", "GP"],
30 "AST": ["AST", "TOV", "AST_TOV", "GP"],
31 "STL": ["STL", "TOV", "GP"],
32 "BLK": ["BLK", "PF", "GP"],
33 "EFF": ["EFF", "PTS", "REB", "AST", "GP"],
34 "FG_PCT": ["FG_PCT", "FGM", "FGA", "PTS"],
35 "FT_PCT": ["FT_PCT", "FTM", "FTA", "PTS"],
36 "FG3_PCT": ["FG3_PCT", "FG3M", "FG3A", "PTS"],
37}
38
39_DISPLAY_NAMES = {
40 "FG_PCT": "FG%",
41 "FT_PCT": "FT%",
42 "FG3_PCT": "3P%",
43 "FG3M": "3PM",
44 "FG3A": "3PA",
45 "AST_TOV": "AST/TO",
46}
47
48
49def fetch_leaders(category: str = "PTS") -> dict:
50 endpoint = LeagueLeaders(
51 stat_category_abbreviation=category,
52 season_type_all_star="Regular Season",
53 )
54 return json.loads(endpoint.get_json())
55
56
57def get_leaders_table(data: dict, category: str = "PTS") -> str:
58 result = data["resultSet"]
59 headers = result["headers"]
60 rows = result["rowSet"]
61
62 base = ["RANK", "PLAYER", "TEAM", "GP"]
63 extra = [c for c in _EXTRA_COLS.get(category, [category]) if c not in base]
64 wanted = base + extra
65
66 idx = {h: i for i, h in enumerate(headers)}
67 table_data = [[row[idx[col]] for col in wanted if col in idx] for row in rows[:25]]
68 display_headers = [_DISPLAY_NAMES.get(c, c) for c in wanted if c in idx]
69
70 cat_label = dict(CATEGORIES).get(category, category)
71 title = f"{BOLD}League Leaders — {cat_label}{END}"
72 return title + "\n" + tabulate(table_data, headers=display_headers, tablefmt="grid")
build/lib/nba/playoff.py deleted −93
@@ -1,93 +0,0 @@
1"""
2Fetches and formats the NBA playoff picture.
3"""
4
5import json
6
7from tabulate import tabulate
8from nba_api.stats.endpoints.playoffpicture import PlayoffPicture
9
10BOLD = "\033[1m"
11END = "\033[0m"
12RED = "\033[91m"
13GREEN = "\033[32m"
14YELLOW = "\033[33m"
15
16
17def fetch_playoff_picture() -> dict:
18 endpoint = PlayoffPicture()
19 return json.loads(endpoint.get_json())
20
21
22def _clinch_status(row: list, idx: dict) -> str:
23 """Return a color-coded status string from clinch/elimination columns."""
24
25 def val(col):
26 return row[idx[col]] if col in idx else None
27
28 if val("CLINCHED_CONFERENCE"):
29 return f"{BOLD}{GREEN}z-Clinched Conf{END}"
30 if val("CLINCHED_DIVISION") or val("CLINCHED_PLAYOFFS"):
31 return f"{GREEN}x-Clinched{END}"
32 if val("Clinched_Play_In"):
33 return f"{YELLOW}pi-Play-In{END}"
34 if val("ELIMINATED_PLAYOFFS"):
35 return f"{RED}e-Eliminated{END}"
36 return ""
37
38
39def _build_conference_table(result_sets: list, name: str) -> str:
40 """Build a formatted playoff standings table for one conference."""
41 rs = next((r for r in result_sets if r["name"] == name), None)
42 if rs is None or not rs["rowSet"]:
43 return "No data available."
44
45 headers = rs["headers"]
46 rows = rs["rowSet"]
47 idx = {h: i for i, h in enumerate(headers)}
48
49 def get(row, col, default=""):
50 return row[idx[col]] if col in idx else default
51
52 table_data = []
53 for row in rows:
54 wins = get(row, "WINS")
55 losses = get(row, "LOSSES")
56 pct = get(row, "PCT")
57 pct_str = f"{float(pct):.3f}" if pct not in ("", None) else ""
58 table_data.append(
59 [
60 get(row, "RANK"),
61 get(row, "TEAM"),
62 f"{wins}-{losses}",
63 pct_str,
64 get(row, "GB"),
65 get(row, "HOME"),
66 get(row, "AWAY"),
67 get(row, "CONF"),
68 _clinch_status(row, idx),
69 ]
70 )
71
72 display_headers = [
73 "#",
74 "Team",
75 "W-L",
76 "PCT",
77 "GB",
78 "HOME",
79 "AWAY",
80 "CONF",
81 "Status",
82 ]
83 conf_label = "Eastern" if "East" in name else "Western"
84 title = f"{BOLD}{conf_label} Conference Playoff Picture:{END}"
85 return title + "\n" + tabulate(table_data, headers=display_headers, tablefmt="grid")
86
87
88def get_west_playoff_table(data: dict) -> str:
89 return _build_conference_table(data["resultSets"], "WestConfStandings")
90
91
92def get_east_playoff_table(data: dict) -> str:
93 return _build_conference_table(data["resultSets"], "EastConfStandings")
build/lib/nba/scores.py deleted −106
@@ -1,106 +0,0 @@
1"""
2Tabulates a scoreboard for today's games.
3"""
4
5from tabulate import tabulate
6
7# ANSI escape codes for text formatting
8BOLD = "\033[1m"
9END = "\033[0m"
10RED = "\033[91m"
11GREEN = "\033[32m"
12
13
14# Function to get team record from standings
15def get_team_record(team_name, standings) -> str:
16 """
17 Retrieves a team's win-loss record from the standings data.
18
19 Args:
20 team_name (str): Name of the team.
21 standings (dict): Team standings data.
22
23 Returns:
24 record (str): Team's win-loss record in 'W-L' format. Defaults to 'N/A'.
25 """
26 for result_set in standings["resultSets"]:
27 if result_set["name"] == "Standings":
28 for team in result_set["rowSet"]:
29 if team[4] == team_name:
30 return f"{team[12]}-{team[13]}"
31 return "N/A"
32
33
34def get_scoreboard_table(games, standings) -> str:
35 """
36 Builds and returns the current day's games as a formatted table string.
37
38 Args:
39 games (dict): JSON parsed games data.
40 standings (dict): Team standings data.
41
42 Returns:
43 str: Formatted table string with ANSI color codes.
44 """
45 scoreboard_data = games["scoreboard"]
46 game_list = scoreboard_data["games"]
47
48 if not game_list:
49 return "No games scheduled today."
50
51 # Prepare the table data
52 table_data = []
53 for game in game_list:
54 home_team = game["homeTeam"]["teamName"]
55 away_team = game["awayTeam"]["teamName"]
56 game_status = game["gameStatusText"]
57 home_score = game["homeTeam"]["score"]
58 away_score = game["awayTeam"]["score"]
59
60 home_record = get_team_record(home_team, standings)
61 away_record = get_team_record(away_team, standings)
62
63 # Determine the winning team
64 if home_score > away_score:
65 home_team_bold = f"{BOLD}{GREEN}{home_team} ({home_record}){END}{END}"
66 away_team_bold = f"{away_team} ({away_record}){END}"
67 home_score_bold = f"{BOLD}{GREEN}{home_score}{END}{END}"
68 away_score_bold = f"{away_score}{END}"
69 elif away_score > home_score:
70 home_team_bold = f"{home_team} ({home_record}){END}"
71 away_team_bold = f"{BOLD}{GREEN}{away_team} ({away_record}){END}{END}"
72 home_score_bold = f"{home_score}{END}"
73 away_score_bold = f"{BOLD}{GREEN}{away_score}{END}{END}"
74 else:
75 home_team_bold = f"{home_team} ({home_record})"
76 away_team_bold = f"{away_team} ({away_record})"
77 home_score_bold = f"{home_score}"
78 away_score_bold = f"{away_score}"
79
80 # Determine games still in progress
81 if game_status != "Final":
82 game_status = f"{RED}{game_status}{END}"
83
84 table_data.append(
85 [
86 f"{home_team_bold}\n{away_team_bold}",
87 f"{home_score_bold}\n{away_score_bold}",
88 f"{BOLD}{game_status}{END}",
89 ]
90 )
91
92 # Define the table headers
93 headers = ["Team", "Score", "Game Status"]
94
95 return tabulate(table_data, headers=headers, tablefmt="grid")
96
97
98def build_scoreboard(games, standings) -> None:
99 """
100 Prints the current day's games in a table format.
101
102 Args:
103 games (dict): JSON parsed games data.
104 standings (dict): Team standings data.
105 """
106 print(get_scoreboard_table(games, standings))
build/lib/nba/standings.py deleted −91
@@ -1,91 +0,0 @@
1"""
2Tabulate the current conference standings.
3"""
4
5from tabulate import tabulate
6
7# ANSI escape codes for text formatting
8BOLD = "\033[1m"
9END = "\033[0m"
10RED = "\033[91m"
11GREEN = "\033[32m"
12
13
14def _build_conference_table(standings, conference: str) -> str:
15 """Build a formatted table string for one conference."""
16 data = []
17 rank = 1
18
19 for result_set in standings["resultSets"]:
20 if result_set["name"] == "Standings":
21 for team in result_set["rowSet"]:
22 if team[5] != conference:
23 continue
24 wins = team[12]
25 losses = team[13]
26 win_pct = team[14]
27 streak = team[35]
28
29 strk_color = (
30 f"{RED}{streak}{END}"
31 if int(streak) < 0
32 else f"{GREEN}{streak}{END}"
33 )
34
35 data.append(
36 [
37 f"{rank}",
38 team[4],
39 f"{wins}-{losses}",
40 f"{win_pct:.3f}",
41 team[37],
42 strk_color,
43 team[19],
44 team[17],
45 team[18],
46 ]
47 )
48 rank += 1
49
50 headers = ["Rank", "Team", "W-L", "PCT", "GB", "STRK", "L10", "HOME", "AWAY"]
51 label = "Eastern" if conference == "East" else "Western"
52 return f"{BOLD}{label} Conference Standings:{END}\n" + tabulate(
53 data, headers=headers, tablefmt="grid"
54 )
55
56
57def get_east_standings_table(standings) -> str:
58 """Returns the Eastern Conference standings as a formatted string."""
59 return _build_conference_table(standings, "East")
60
61
62def get_west_standings_table(standings) -> str:
63 """Returns the Western Conference standings as a formatted string."""
64 return _build_conference_table(standings, "West")
65
66
67def get_standings_tables(standings) -> str:
68 """
69 Builds and returns both conference standings as a formatted string.
70
71 Args:
72 standings (dict): Team standings data.
73
74 Returns:
75 str: Formatted standings string with ANSI color codes for both conferences.
76 """
77 return (
78 get_east_standings_table(standings)
79 + "\n\n"
80 + get_west_standings_table(standings)
81 )
82
83
84def build_standings(standings) -> None:
85 """
86 Prints team standings in two separate tables.
87
88 Args:
89 standings (dict): Team standings data.
90 """
91 print(get_standings_tables(standings))
build/lib/nba/tui/__init__.py deleted
build/lib/nba/tui/app.py deleted −247
@@ -1,247 +0,0 @@
1"""
2Textual TUI application for NBA scores and standings.
3"""
4
5from __future__ import annotations
6
7import asyncio
8from pathlib import Path
9
10from rich.text import Text
11from textual.app import App, ComposeResult
12from textual.containers import Horizontal
13from textual.widgets import Footer, Header, Static, TabbedContent, TabPane
14
15from nba import fetch_data
16from nba import bracket as bracket_mod
17from nba import leaders as leaders_mod
18from nba import playoff as playoff_mod
19from nba import box_score as box_score_mod
20from nba.scores import get_scoreboard_table
21from nba.standings import get_east_standings_table, get_west_standings_table
22from nba.tui.widgets import CountdownBar, ScoresWidget
23
24
25class NBAApp(App):
26 """Live NBA scores and standings TUI with auto-refresh."""
27
28 CSS_PATH = Path(__file__).parent / "styles.tcss"
29
30 BINDINGS = [
31 ("q", "quit", "Quit"),
32 ("s", "show_scores", "Scores"),
33 ("t", "show_standings", "Standings"),
34 ("l", "show_leaders", "Leaders"),
35 ("p", "show_playoff", "Playoff"),
36 ("k", "show_bracket", "Bracket"),
37 ("b", "show_boxscore", "Box Score"),
38 ("r", "refresh_now", "Refresh"),
39 ("comma", "prev_category", "◀ Cat"),
40 ("full_stop", "next_category", "Cat ▶"),
41 ]
42
43 TITLE = "NBA Scores"
44
45 def __init__(self, initial_tab: str = "scores", refresh_interval: int = 60) -> None:
46 super().__init__()
47 self.initial_tab = initial_tab
48 self.refresh_interval = refresh_interval
49 self._games: dict | None = None
50 self._ranks: dict | None = None
51 self._leaders_data: dict | None = None
52 self._leaders_cat_idx: int = 0
53 self._playoff_data: dict | None = None
54 self._bracket_data: dict | None = None
55
56 def compose(self) -> ComposeResult:
57 yield Header()
58 with TabbedContent(initial=self.initial_tab):
59 with TabPane("Scores", id="scores"):
60 yield ScoresWidget("Loading...", id="scores-content")
61 with TabPane("Standings", id="standings"):
62 with Horizontal(id="standings-container"):
63 yield Static("Loading...", id="west-content")
64 yield Static("Loading...", id="east-content")
65 with TabPane("Leaders", id="leaders"):
66 yield Static("Loading...", id="leaders-content")
67 with TabPane("Playoff", id="playoff"):
68 with Horizontal(id="playoff-container"):
69 yield Static("Loading...", id="playoff-west-content")
70 yield Static("Loading...", id="playoff-east-content")
71 with TabPane("Bracket", id="bracket"):
72 yield Static("Loading...", id="bracket-content")
73 with TabPane("Box Score", id="boxscore"):
74 with Horizontal(id="boxscore-container"):
75 yield Static(
76 "Press 1–9 to load a game from the Scores tab.",
77 id="home-content",
78 )
79 yield Static("", id="away-content")
80 yield CountdownBar(self.refresh_interval, id="countdown")
81 yield Footer()
82
83 async def on_mount(self) -> None:
84 await self._do_refresh()
85 self.set_interval(self.refresh_interval, self._do_refresh)
86
87 # ------------------------------------------------------------------ #
88 # Refresh logic #
89 # ------------------------------------------------------------------ #
90
91 async def _do_refresh(self) -> None:
92 """Fetch scores, standings, leaders, playoff picture, and bracket in parallel."""
93 loop = asyncio.get_event_loop()
94 cat = leaders_mod.CATEGORIES[self._leaders_cat_idx][0]
95
96 results = await asyncio.gather(
97 loop.run_in_executor(None, fetch_data.fetch_data),
98 loop.run_in_executor(None, lambda: leaders_mod.fetch_leaders(cat)),
99 loop.run_in_executor(None, playoff_mod.fetch_playoff_picture),
100 loop.run_in_executor(None, bracket_mod.fetch_bracket),
101 return_exceptions=True,
102 )
103
104 games_ranks, leaders_data, playoff_data, bracket_data = results
105
106 if not isinstance(games_ranks, Exception):
107 self._games, self._ranks = games_ranks
108 if not isinstance(leaders_data, Exception):
109 self._leaders_data = leaders_data
110 if not isinstance(playoff_data, Exception):
111 self._playoff_data = playoff_data
112 if not isinstance(bracket_data, Exception):
113 self._bracket_data = bracket_data
114
115 self._update_widgets()
116 self.query_one(CountdownBar).reset(self.refresh_interval)
117
118 def _update_widgets(self) -> None:
119 if self._games and self._ranks:
120 self.query_one(ScoresWidget).update(
121 Text.from_ansi(get_scoreboard_table(self._games, self._ranks))
122 )
123 self.query_one("#west-content", Static).update(
124 Text.from_ansi(get_west_standings_table(self._ranks))
125 )
126 self.query_one("#east-content", Static).update(
127 Text.from_ansi(get_east_standings_table(self._ranks))
128 )
129
130 if self._leaders_data:
131 cat = leaders_mod.CATEGORIES[self._leaders_cat_idx][0]
132 self.query_one("#leaders-content", Static).update(
133 Text.from_ansi(leaders_mod.get_leaders_table(self._leaders_data, cat))
134 )
135
136 if self._playoff_data:
137 self.query_one("#playoff-west-content", Static).update(
138 Text.from_ansi(playoff_mod.get_west_playoff_table(self._playoff_data))
139 )
140 self.query_one("#playoff-east-content", Static).update(
141 Text.from_ansi(playoff_mod.get_east_playoff_table(self._playoff_data))
142 )
143
144 if self._bracket_data:
145 self.query_one("#bracket-content", Static).update(
146 Text.from_ansi(bracket_mod.get_bracket_table(self._bracket_data))
147 )
148
149 # ------------------------------------------------------------------ #
150 # Key handlers #
151 # ------------------------------------------------------------------ #
152
153 def on_key(self, event) -> None:
154 """Handle 1–9 to select a game for the box score tab."""
155 char = event.character
156 if char and char.isdigit() and char != "0":
157 asyncio.create_task(self._load_box_score(int(char) - 1))
158
159 # ------------------------------------------------------------------ #
160 # Actions #
161 # ------------------------------------------------------------------ #
162
163 async def action_refresh_now(self) -> None:
164 await self._do_refresh()
165
166 def action_show_scores(self) -> None:
167 self.query_one(TabbedContent).active = "scores"
168
169 def action_show_standings(self) -> None:
170 self.query_one(TabbedContent).active = "standings"
171
172 def action_show_leaders(self) -> None:
173 self.query_one(TabbedContent).active = "leaders"
174
175 def action_show_playoff(self) -> None:
176 self.query_one(TabbedContent).active = "playoff"
177
178 def action_show_bracket(self) -> None:
179 self.query_one(TabbedContent).active = "bracket"
180
181 def action_show_boxscore(self) -> None:
182 self.query_one(TabbedContent).active = "boxscore"
183
184 async def action_prev_category(self) -> None:
185 self._leaders_cat_idx = (self._leaders_cat_idx - 1) % len(
186 leaders_mod.CATEGORIES
187 )
188 await self._refresh_leaders()
189
190 async def action_next_category(self) -> None:
191 self._leaders_cat_idx = (self._leaders_cat_idx + 1) % len(
192 leaders_mod.CATEGORIES
193 )
194 await self._refresh_leaders()
195
196 # ------------------------------------------------------------------ #
197 # Helpers #
198 # ------------------------------------------------------------------ #
199
200 async def _refresh_leaders(self) -> None:
201 cat, label = leaders_mod.CATEGORIES[self._leaders_cat_idx]
202 self.query_one("#leaders-content", Static).update(f"Loading {label} leaders...")
203 loop = asyncio.get_event_loop()
204 try:
205 data = await loop.run_in_executor(
206 None, lambda: leaders_mod.fetch_leaders(cat)
207 )
208 self._leaders_data = data
209 self.query_one("#leaders-content", Static).update(
210 Text.from_ansi(leaders_mod.get_leaders_table(data, cat))
211 )
212 except Exception as exc:
213 self.query_one("#leaders-content", Static).update(
214 f"Error loading leaders: {exc}"
215 )
216
217 async def _load_box_score(self, game_idx: int) -> None:
218 if not self._games:
219 return
220 games = self._games["scoreboard"]["games"]
221 if game_idx >= len(games):
222 return
223
224 game = games[game_idx]
225 game_id = game["gameId"]
226 home = game["homeTeam"]["teamName"]
227 away = game["awayTeam"]["teamName"]
228
229 self.query_one("#home-content", Static).update(
230 f"Loading box score: {away} @ {home}..."
231 )
232 self.query_one("#away-content", Static).update("")
233 self.query_one(TabbedContent).active = "boxscore"
234
235 loop = asyncio.get_event_loop()
236 try:
237 data = await loop.run_in_executor(
238 None, lambda: box_score_mod.fetch_box_score(game_id)
239 )
240 home_table, away_table = box_score_mod.get_box_score_tables(data)
241 self.query_one("#home-content", Static).update(Text.from_ansi(home_table))
242 self.query_one("#away-content", Static).update(Text.from_ansi(away_table))
243 except Exception as exc:
244 self.query_one("#home-content", Static).update(
245 f"Error loading box score: {exc}"
246 )
247 self.query_one("#away-content", Static).update("")
build/lib/nba/tui/styles.tcss deleted −59
@@ -1,59 +0,0 @@
1Screen {
2 background: $background;
3}
4
5TabbedContent {
6 height: 1fr;
7}
8
9TabPane {
10 overflow: auto auto;
11 padding: 1 2;
12}
13
14/* Scores tab */
15#scores-content {
16 width: auto;
17}
18
19/* Standings, Playoff, Bracket, and Box Score tabs */
20#standings-container,
21#playoff-container,
22#boxscore-container {
23 height: 1fr;
24 width: 100%;
25}
26
27#east-content,
28#west-content,
29#playoff-east-content,
30#playoff-west-content,
31#bracket-content {
32 width: 1fr;
33 overflow: auto auto;
34 padding-right: 2;
35}
36
37/* Leaders tab */
38#leaders-content {
39 width: auto;
40 overflow: auto auto;
41}
42
43/* Box Score tab */
44#home-content,
45#away-content {
46 width: 1fr;
47 overflow: auto auto;
48 padding-right: 2;
49}
50
51/* Countdown / status bar */
52#countdown {
53 dock: bottom;
54 height: 1;
55 background: $panel;
56 color: $text-muted;
57 content-align: right middle;
58 padding-right: 2;
59}
build/lib/nba/tui/widgets.py deleted −51
@@ -1,51 +0,0 @@
1"""
2Custom Textual widgets for the NBA scores TUI.
3"""
4
5from textual.reactive import reactive
6from textual.widgets import Static
7
8
9class ScoresWidget(Static):
10 """Displays the NBA scoreboard as a scrollable ANSI-formatted table."""
11
12
13class StandingsWidget(Static):
14 """Displays the NBA standings as a scrollable ANSI-formatted table."""
15
16
17class CountdownBar(Static):
18 """
19 Docked status bar that counts down to the next auto-refresh.
20
21 Maintains its own 1-second ticker and re-renders via a reactive attribute.
22 """
23
24 seconds: reactive[int] = reactive(60)
25
26 def __init__(self, interval: int, **kwargs) -> None:
27 super().__init__(**kwargs)
28 self._interval = interval
29 self.seconds = interval
30
31 def on_mount(self) -> None:
32 self.set_interval(1, self._tick)
33
34 def _tick(self) -> None:
35 if self.seconds > 0:
36 self.seconds -= 1
37
38 def watch_seconds(self, value: int) -> None:
39 if value <= 0:
40 self.update("Refreshing...")
41 else:
42 self.update(
43 f"Next refresh in {value}s | "
44 "\\[1-9] Box Score | \\[k] Bracket | "
45 "\\[</>] Leaders Cat | \\[r] Refresh | \\[q] Quit"
46 )
47
48 def reset(self, interval: int) -> None:
49 """Reset the countdown to the given interval."""
50 self._interval = interval
51 self.seconds = interval
nba_scores.egg-info/PKG-INFO deleted −112
@@ -1,112 +0,0 @@
1Metadata-Version: 2.4
2Name: nba-scores
3Version: 0.2.3
4Summary: A CLI interface to get current NBA scores and standings.
5Author-email: Christian Cleberg <hello@cleberg.net>
6License-Expression: GPL-3.0-or-later
7Project-URL: Homepage, https://git.cleberg.net/nba-scores.git
8Project-URL: Issues, https://git.cleberg.net/nba-scores.git
9Classifier: Programming Language :: Python :: 3
10Classifier: Operating System :: OS Independent
11Requires-Python: >=3.9
12Description-Content-Type: text/markdown
13License-File: LICENSE
14Requires-Dist: nba_api
15Requires-Dist: tabulate
16Requires-Dist: argparse
17Requires-Dist: textual<7.0,>=0.89.1
18Dynamic: license-file
19
20# NBA
21
22![](./screenshots/preview.png)
23
24NBA is a Python package that provides a command-line interface to current NBA
25scores, standings, statistical leaders, playoff picture, and live box scores.
26It supports both a static one-shot mode and an interactive TUI with
27auto-refresh.
28
29# Table of Contents
30
31- [Installation](#installation)
32- [Usage](#usage)
33 - [Interactive TUI](#interactive-tui)
34 - [Static output](#static-output)
35- [Contributing](#contributing)
36
37# Installation
38
39[Back to top](#table-of-contents)
40
41```shell
42git clone https://git.cleberg.net/nba-scores.git
43cd nba-scores
44pipx install .
45```
46
47# Usage
48
49[Back to top](#table-of-contents)
50
51## Interactive TUI
52
53Launch the full interactive terminal UI:
54
55```shell
56nba --tui
57```
58
59Optional flags:
60
61| Flag | Description |
62|--------------------|--------------------------------------------------------|
63| `--tui` | Launch the interactive TUI |
64| `--scores` | Open TUI on the Scores tab (default) |
65| `--standings` | Open TUI on the Standings tab |
66| `--refresh N` | Auto-refresh interval in seconds (default: 60, min: 10)|
67
68### TUI tabs
69
70| Tab | Key | Description |
71|-----------------|-----|----------------------------------------------------------|
72| Scores | `s` | Today's games with live scores and status |
73| Standings | `t` | East / West conference standings, side by side |
74| Leaders | `l` | Top 25 players by stat category |
75| Playoff Picture | `p` | Conference seeding with clinch and elimination status |
76| Bracket | `k` | Playoff series matchups and series records |
77| Box Score | `b` | Per-player live stats for a selected game |
78
79### TUI key bindings
80
81| Key | Action |
82|---------|------------------------------------------------|
83| `1`–`9` | Load box score for game N from the Scores tab |
84| `,` | Cycle to the previous leaders stat category |
85| `.` | Cycle to the next leaders stat category |
86| `r` | Refresh all data immediately |
87| `q` | Quit |
88
89### Leaders stat categories
90
91Cycles through: Points, Rebounds, Assists, Steals, Blocks, Efficiency, FG%, FT%, 3P%
92
93## Static output
94
95Print scores or standings directly to the terminal without launching the TUI:
96
97| Argument | Shortcut | Description |
98|---------------|----------|-----------------------------------|
99| `--scores` | `-sc` | Print today's scoreboard |
100| `--standings` | `-st` | Print current conference standings|
101
102```shell
103nba --scores
104nba --standings
105```
106
107# Contributing
108
109[Back to top](#table-of-contents)
110
111Any and all contributions are welcome. Feel free to fork the project,
112add features, and submit a pull request.
nba_scores.egg-info/SOURCES.txt deleted −23
@@ -1,23 +0,0 @@
1LICENSE
2README.md
3pyproject.toml
4nba/__init__.py
5nba/__main__.py
6nba/box_score.py
7nba/bracket.py
8nba/cli.py
9nba/fetch_data.py
10nba/leaders.py
11nba/playoff.py
12nba/scores.py
13nba/standings.py
14nba/tui/__init__.py
15nba/tui/app.py
16nba/tui/styles.tcss
17nba/tui/widgets.py
18nba_scores.egg-info/PKG-INFO
19nba_scores.egg-info/SOURCES.txt
20nba_scores.egg-info/dependency_links.txt
21nba_scores.egg-info/entry_points.txt
22nba_scores.egg-info/requires.txt
23nba_scores.egg-info/top_level.txt\ No newline at end of file
nba_scores.egg-info/dependency_links.txt deleted −1
@@ -1 +0,0 @@
1
nba_scores.egg-info/entry_points.txt deleted −2
@@ -1,2 +0,0 @@
1[console_scripts]
2nba = nba.cli:nba
nba_scores.egg-info/requires.txt deleted −4
@@ -1,4 +0,0 @@
1nba_api
2tabulate
3argparse
4textual<7.0,>=0.89.1
nba_scores.egg-info/top_level.txt deleted −1
@@ -1 +0,0 @@
1nba