Commit 14d8039e4a

14d8039e4a6ae27c1a7441a375b87617fd5aea4d

parent: 4d40494f3e

Verified · cmc

cmc <hello@cleberg.net> · 2026-05-04 22:03 UTC

feat: add Bracket tab for playoffs

Layout: unified · split

README.md +1
@@ -54,6 +54,7 @@ Optional flags:
5454| Standings | `t` | East / West conference standings, side by side |
5555| Leaders | `l` | Top 25 players by stat category |
5656| Playoff Picture | `p` | Conference seeding with clinch and elimination status |
57| Bracket | `k` | Playoff series matchups and series records |
5758| Box Score | `b` | Per-player live stats for a selected game |
5859
5960### TUI key bindings
build/lib/nba/bracket.py added +281
@@ -0,0 +1,281 @@
1"""
2Fetches and formats the NBA playoff bracket.
3"""
4
5import json
6from collections import defaultdict
7from typing import Optional
8
9from nba_api.stats.endpoints.commonplayoffseries import CommonPlayoffSeries
10from nba_api.stats.endpoints.leaguegamelog import LeagueGameLog
11from nba_api.stats.static import teams
12
13BOLD = "\033[1m"
14END = "\033[0m"
15GREEN = "\033[32m"
16
17ROUND_LABELS = {
18 1: "First Round",
19 2: "Conference Semifinals",
20 3: "Conference Finals",
21 4: "NBA Finals",
22}
23
24
25def fetch_bracket() -> dict:
26 """
27 Fetch playoff series and completed playoff games.
28
29 CommonPlayoffSeries provides the bracket's scheduled series and games. The
30 playoff game log is used to compute each series record from completed games.
31 """
32 series_endpoint = CommonPlayoffSeries()
33 game_log_endpoint = LeagueGameLog(season_type_all_star="Playoffs")
34 return {
35 "series": json.loads(series_endpoint.get_json()),
36 "game_log": json.loads(game_log_endpoint.get_json()),
37 }
38
39
40def get_bracket_table(data: dict) -> str:
41 """Return a formatted playoff bracket."""
42 result_sets = data["series"]["resultSets"]
43 series_set = next((r for r in result_sets if r["name"] == "PlayoffSeries"), None)
44 if series_set is None or not series_set["rowSet"]:
45 return "No playoff bracket data available."
46
47 headers = series_set["headers"]
48 idx = {header: i for i, header in enumerate(headers)}
49 team_map = _team_map()
50 series_games = _group_series(series_set["rowSet"], idx)
51 series_wins = _series_wins(data["game_log"], series_games)
52 summaries = _series_summaries(team_map, series_games, series_wins)
53
54 return "\n".join(
55 [
56 _center(f"{BOLD}NBA Playoff Bracket{END}", 74),
57 "",
58 _render_conference("Western", summaries),
59 "",
60 _render_finals(summaries),
61 "",
62 _render_conference("Eastern", summaries),
63 ]
64 )
65
66
67def _team_map() -> dict:
68 return {
69 team["id"]: {
70 "abbr": team["abbreviation"],
71 "name": f"{team['city']} {team['nickname']}",
72 }
73 for team in teams.get_teams()
74 }
75
76
77def _group_series(rows: list, idx: dict) -> dict:
78 series_games = defaultdict(list)
79 for row in rows:
80 series_id = row[idx["SERIES_ID"]]
81 series_games[series_id].append(
82 {
83 "game_id": row[idx["GAME_ID"]],
84 "game_num": row[idx["GAME_NUM"]],
85 "home_team_id": row[idx["HOME_TEAM_ID"]],
86 "visitor_team_id": row[idx["VISITOR_TEAM_ID"]],
87 }
88 )
89
90 return {
91 series_id: sorted(games, key=lambda game: game["game_num"])
92 for series_id, games in series_games.items()
93 }
94
95
96def _series_wins(game_log: dict, series_games: dict) -> dict:
97 game_to_series = {
98 game["game_id"]: series_id
99 for series_id, games in series_games.items()
100 for game in games
101 }
102 wins = defaultdict(lambda: defaultdict(int))
103
104 for series_id in series_games:
105 wins[series_id]["completed_games"] = set()
106
107 result_set = game_log["resultSets"][0]
108 idx = {header: i for i, header in enumerate(result_set["headers"])}
109 for row in result_set["rowSet"]:
110 game_id = row[idx["GAME_ID"]]
111 series_id = game_to_series.get(game_id)
112 if series_id is None or row[idx["WL"]] != "W":
113 continue
114
115 team_id = row[idx["TEAM_ID"]]
116 wins[series_id][team_id] += 1
117 wins[series_id]["completed_games"].add(game_id)
118
119 return wins
120
121
122def _series_summaries(team_map: dict, series_games: dict, series_wins: dict) -> dict:
123 summaries = {}
124 for series_id, games in series_games.items():
125 first_game = games[0]
126 home_id = first_game["home_team_id"]
127 visitor_id = first_game["visitor_team_id"]
128 wins = series_wins[series_id]
129 home_wins = wins.get(home_id, 0)
130 visitor_wins = wins.get(visitor_id, 0)
131
132 summaries[series_id] = {
133 "conference": _conference(series_id),
134 "round": _round_number(series_id),
135 "slot": _series_slot(series_id),
136 "home_id": home_id,
137 "visitor_id": visitor_id,
138 "home_abbr": _abbr(team_map, home_id),
139 "visitor_abbr": _abbr(team_map, visitor_id),
140 "home_wins": home_wins,
141 "visitor_wins": visitor_wins,
142 "winner": _winner(team_map, home_id, visitor_id, home_wins, visitor_wins),
143 }
144 return summaries
145
146
147def _render_conference(conference: str, summaries: dict) -> str:
148 first_round = _conference_round(summaries, conference, 1)
149 semifinals = _conference_round(summaries, conference, 2)
150 finals = _conference_round(summaries, conference, 3)
151 final = finals[0] if finals else None
152
153 lines = [
154 f"{BOLD}{conference.upper()} CONFERENCE{END}",
155 "First Round Semifinals Conference Finals",
156 ]
157
158 lines.extend(
159 [
160 f"{_series_box(_slot(first_round, 0), 24)} ┐",
161 f"{_blank(24)} ├── {_series_box(_slot(semifinals, 0), 24)} ┐",
162 f"{_series_box(_slot(first_round, 1), 24)} ┘ {_blank(24)} │",
163 f"{_blank(24)} {_blank(24)} ├── {_series_box(final, 24)}",
164 f"{_series_box(_slot(first_round, 2), 24)} ┐ {_blank(24)} │",
165 f"{_blank(24)} ├── {_series_box(_slot(semifinals, 1), 24)} ┘",
166 f"{_series_box(_slot(first_round, 3), 24)} ┘",
167 ]
168 )
169
170 return "\n".join(lines)
171
172
173def _render_finals(summaries: dict) -> str:
174 finals = [summary for summary in summaries.values() if summary["round"] == 4]
175 final = finals[0] if finals else None
176
177 lines = [
178 f"{BOLD}NBA FINALS{END}",
179 "West Champion East Champion",
180 f"{_series_box(final, 24)}",
181 ]
182 if final is None:
183 lines.append("Winner TBD")
184 elif final["winner"]:
185 lines.append(f"{GREEN}{final['winner']} wins the Finals{END}")
186 return "\n".join(lines)
187
188
189def _conference_round(summaries: dict, conference: str, round_number: int) -> list:
190 return sorted(
191 [
192 summary
193 for summary in summaries.values()
194 if summary["conference"] == conference[:4]
195 and summary["round"] == round_number
196 ],
197 key=lambda summary: summary["slot"],
198 )
199
200
201def _slot(items: list, idx: int) -> Optional[dict]:
202 return items[idx] if idx < len(items) else None
203
204
205def _series_box(summary: Optional[dict], width: int) -> str:
206 if summary is None:
207 return _blank(width, "TBD")
208
209 visitor = _team_line(
210 summary["visitor_abbr"],
211 summary["visitor_wins"],
212 summary["winner"] == summary["visitor_abbr"],
213 )
214 home = _team_line(
215 summary["home_abbr"],
216 summary["home_wins"],
217 summary["winner"] == summary["home_abbr"],
218 )
219 return f"{visitor} / {home}".ljust(width)
220
221
222def _team_line(abbr: str, wins: int, won_series: bool) -> str:
223 label = f"{abbr} {wins}"
224 if won_series:
225 return f"{label}*"
226 return label
227
228
229def _winner(
230 team_map: dict,
231 home_id: int,
232 visitor_id: int,
233 home_wins: int,
234 visitor_wins: int,
235) -> str:
236 if home_wins >= 4:
237 return _abbr(team_map, home_id)
238 if visitor_wins >= 4:
239 return _abbr(team_map, visitor_id)
240 return ""
241
242
243def _blank(width: int, text: str = "") -> str:
244 return text.ljust(width)
245
246
247def _center(text: str, width: int) -> str:
248 return text.center(width)
249
250
251def _round_number(series_id: str) -> int:
252 try:
253 return int(series_id[-3:-1])
254 except ValueError:
255 return 0
256
257
258def _series_slot(series_id: str) -> int:
259 try:
260 return int(series_id[-1])
261 except ValueError:
262 return 0
263
264
265def _conference(series_id: str) -> str:
266 round_number = _round_number(series_id)
267 slot = _series_slot(series_id)
268
269 if round_number == 4:
270 return "NBA"
271 if round_number == 3:
272 return "East" if slot == 0 else "West"
273 if round_number == 2:
274 return "East" if slot <= 1 else "West"
275 if round_number == 1:
276 return "East" if slot <= 3 else "West"
277 return ""
278
279
280def _abbr(team_map: dict, team_id: int) -> str:
281 return team_map.get(team_id, {}).get("abbr", str(team_id))
build/lib/nba/tui/app.py +18 −2
@@ -13,6 +13,7 @@ from textual.containers import Horizontal
1313from textual.widgets import Footer, Header, Static, TabbedContent, TabPane
1414
1515from nba import fetch_data
16from nba import bracket as bracket_mod
1617from nba import leaders as leaders_mod
1718from nba import playoff as playoff_mod
1819from nba import box_score as box_score_mod
@@ -32,6 +33,7 @@ class NBAApp(App):
3233 ("t", "show_standings", "Standings"),
3334 ("l", "show_leaders", "Leaders"),
3435 ("p", "show_playoff", "Playoff"),
36 ("k", "show_bracket", "Bracket"),
3537 ("b", "show_boxscore", "Box Score"),
3638 ("r", "refresh_now", "Refresh"),
3739 ("comma", "prev_category", "◀ Cat"),
@@ -49,6 +51,7 @@ class NBAApp(App):
4951 self._leaders_data: dict | None = None
5052 self._leaders_cat_idx: int = 0
5153 self._playoff_data: dict | None = None
54 self._bracket_data: dict | None = None
5255
5356 def compose(self) -> ComposeResult:
5457 yield Header()
@@ -65,6 +68,8 @@ class NBAApp(App):
6568 with Horizontal(id="playoff-container"):
6669 yield Static("Loading...", id="playoff-west-content")
6770 yield Static("Loading...", id="playoff-east-content")
71 with TabPane("Bracket", id="bracket"):
72 yield Static("Loading...", id="bracket-content")
6873 with TabPane("Box Score", id="boxscore"):
6974 with Horizontal(id="boxscore-container"):
7075 yield Static(
@@ -84,7 +89,7 @@ class NBAApp(App):
8489 # ------------------------------------------------------------------ #
8590
8691 async def _do_refresh(self) -> None:
87 """Fetch scores, standings, leaders, and playoff picture in parallel."""
92 """Fetch scores, standings, leaders, playoff picture, and bracket in parallel."""
8893 loop = asyncio.get_event_loop()
8994 cat = leaders_mod.CATEGORIES[self._leaders_cat_idx][0]
9095
@@ -92,10 +97,11 @@ class NBAApp(App):
9297 loop.run_in_executor(None, fetch_data.fetch_data),
9398 loop.run_in_executor(None, lambda: leaders_mod.fetch_leaders(cat)),
9499 loop.run_in_executor(None, playoff_mod.fetch_playoff_picture),
100 loop.run_in_executor(None, bracket_mod.fetch_bracket),
95101 return_exceptions=True,
96102 )
97103
98 games_ranks, leaders_data, playoff_data = results
104 games_ranks, leaders_data, playoff_data, bracket_data = results
99105
100106 if not isinstance(games_ranks, Exception):
101107 self._games, self._ranks = games_ranks
@@ -103,6 +109,8 @@ class NBAApp(App):
103109 self._leaders_data = leaders_data
104110 if not isinstance(playoff_data, Exception):
105111 self._playoff_data = playoff_data
112 if not isinstance(bracket_data, Exception):
113 self._bracket_data = bracket_data
106114
107115 self._update_widgets()
108116 self.query_one(CountdownBar).reset(self.refresh_interval)
@@ -133,6 +141,11 @@ class NBAApp(App):
133141 Text.from_ansi(playoff_mod.get_east_playoff_table(self._playoff_data))
134142 )
135143
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
136149 # ------------------------------------------------------------------ #
137150 # Key handlers #
138151 # ------------------------------------------------------------------ #
@@ -162,6 +175,9 @@ class NBAApp(App):
162175 def action_show_playoff(self) -> None:
163176 self.query_one(TabbedContent).active = "playoff"
164177
178 def action_show_bracket(self) -> None:
179 self.query_one(TabbedContent).active = "bracket"
180
165181 def action_show_boxscore(self) -> None:
166182 self.query_one(TabbedContent).active = "boxscore"
167183
build/lib/nba/tui/styles.tcss +3 −2
@@ -16,7 +16,7 @@ TabPane {
1616 width: auto;
1717}
1818
19/* Standings & Playoff tabs — two columns side by side */
19/* Standings, Playoff, Bracket, and Box Score tabs */
2020#standings-container,
2121#playoff-container,
2222#boxscore-container {
@@ -27,7 +27,8 @@ TabPane {
2727#east-content,
2828#west-content,
2929#playoff-east-content,
30#playoff-west-content {
30#playoff-west-content,
31#bracket-content {
3132 width: 1fr;
3233 overflow: auto auto;
3334 padding-right: 2;
build/lib/nba/tui/widgets.py +2 −1
@@ -41,7 +41,8 @@ class CountdownBar(Static):
4141 else:
4242 self.update(
4343 f"Next refresh in {value}s | "
44 "\\[1-9] Box Score | \\[</>] Leaders Cat | \\[r] Refresh | \\[q] Quit"
44 "\\[1-9] Box Score | \\[k] Bracket | "
45 "\\[</>] Leaders Cat | \\[r] Refresh | \\[q] Quit"
4546 )
4647
4748 def reset(self, interval: int) -> None:
nba/bracket.py added +281
@@ -0,0 +1,281 @@
1"""
2Fetches and formats the NBA playoff bracket.
3"""
4
5import json
6from collections import defaultdict
7from typing import Optional
8
9from nba_api.stats.endpoints.commonplayoffseries import CommonPlayoffSeries
10from nba_api.stats.endpoints.leaguegamelog import LeagueGameLog
11from nba_api.stats.static import teams
12
13BOLD = "\033[1m"
14END = "\033[0m"
15GREEN = "\033[32m"
16
17ROUND_LABELS = {
18 1: "First Round",
19 2: "Conference Semifinals",
20 3: "Conference Finals",
21 4: "NBA Finals",
22}
23
24
25def fetch_bracket() -> dict:
26 """
27 Fetch playoff series and completed playoff games.
28
29 CommonPlayoffSeries provides the bracket's scheduled series and games. The
30 playoff game log is used to compute each series record from completed games.
31 """
32 series_endpoint = CommonPlayoffSeries()
33 game_log_endpoint = LeagueGameLog(season_type_all_star="Playoffs")
34 return {
35 "series": json.loads(series_endpoint.get_json()),
36 "game_log": json.loads(game_log_endpoint.get_json()),
37 }
38
39
40def get_bracket_table(data: dict) -> str:
41 """Return a formatted playoff bracket."""
42 result_sets = data["series"]["resultSets"]
43 series_set = next((r for r in result_sets if r["name"] == "PlayoffSeries"), None)
44 if series_set is None or not series_set["rowSet"]:
45 return "No playoff bracket data available."
46
47 headers = series_set["headers"]
48 idx = {header: i for i, header in enumerate(headers)}
49 team_map = _team_map()
50 series_games = _group_series(series_set["rowSet"], idx)
51 series_wins = _series_wins(data["game_log"], series_games)
52 summaries = _series_summaries(team_map, series_games, series_wins)
53
54 return "\n".join(
55 [
56 _center(f"{BOLD}NBA Playoff Bracket{END}", 74),
57 "",
58 _render_conference("Western", summaries),
59 "",
60 _render_finals(summaries),
61 "",
62 _render_conference("Eastern", summaries),
63 ]
64 )
65
66
67def _team_map() -> dict:
68 return {
69 team["id"]: {
70 "abbr": team["abbreviation"],
71 "name": f"{team['city']} {team['nickname']}",
72 }
73 for team in teams.get_teams()
74 }
75
76
77def _group_series(rows: list, idx: dict) -> dict:
78 series_games = defaultdict(list)
79 for row in rows:
80 series_id = row[idx["SERIES_ID"]]
81 series_games[series_id].append(
82 {
83 "game_id": row[idx["GAME_ID"]],
84 "game_num": row[idx["GAME_NUM"]],
85 "home_team_id": row[idx["HOME_TEAM_ID"]],
86 "visitor_team_id": row[idx["VISITOR_TEAM_ID"]],
87 }
88 )
89
90 return {
91 series_id: sorted(games, key=lambda game: game["game_num"])
92 for series_id, games in series_games.items()
93 }
94
95
96def _series_wins(game_log: dict, series_games: dict) -> dict:
97 game_to_series = {
98 game["game_id"]: series_id
99 for series_id, games in series_games.items()
100 for game in games
101 }
102 wins = defaultdict(lambda: defaultdict(int))
103
104 for series_id in series_games:
105 wins[series_id]["completed_games"] = set()
106
107 result_set = game_log["resultSets"][0]
108 idx = {header: i for i, header in enumerate(result_set["headers"])}
109 for row in result_set["rowSet"]:
110 game_id = row[idx["GAME_ID"]]
111 series_id = game_to_series.get(game_id)
112 if series_id is None or row[idx["WL"]] != "W":
113 continue
114
115 team_id = row[idx["TEAM_ID"]]
116 wins[series_id][team_id] += 1
117 wins[series_id]["completed_games"].add(game_id)
118
119 return wins
120
121
122def _series_summaries(team_map: dict, series_games: dict, series_wins: dict) -> dict:
123 summaries = {}
124 for series_id, games in series_games.items():
125 first_game = games[0]
126 home_id = first_game["home_team_id"]
127 visitor_id = first_game["visitor_team_id"]
128 wins = series_wins[series_id]
129 home_wins = wins.get(home_id, 0)
130 visitor_wins = wins.get(visitor_id, 0)
131
132 summaries[series_id] = {
133 "conference": _conference(series_id),
134 "round": _round_number(series_id),
135 "slot": _series_slot(series_id),
136 "home_id": home_id,
137 "visitor_id": visitor_id,
138 "home_abbr": _abbr(team_map, home_id),
139 "visitor_abbr": _abbr(team_map, visitor_id),
140 "home_wins": home_wins,
141 "visitor_wins": visitor_wins,
142 "winner": _winner(team_map, home_id, visitor_id, home_wins, visitor_wins),
143 }
144 return summaries
145
146
147def _render_conference(conference: str, summaries: dict) -> str:
148 first_round = _conference_round(summaries, conference, 1)
149 semifinals = _conference_round(summaries, conference, 2)
150 finals = _conference_round(summaries, conference, 3)
151 final = finals[0] if finals else None
152
153 lines = [
154 f"{BOLD}{conference.upper()} CONFERENCE{END}",
155 "First Round Semifinals Conference Finals",
156 ]
157
158 lines.extend(
159 [
160 f"{_series_box(_slot(first_round, 0), 24)} ┐",
161 f"{_blank(24)} ├── {_series_box(_slot(semifinals, 0), 24)} ┐",
162 f"{_series_box(_slot(first_round, 1), 24)} ┘ {_blank(24)} │",
163 f"{_blank(24)} {_blank(24)} ├── {_series_box(final, 24)}",
164 f"{_series_box(_slot(first_round, 2), 24)} ┐ {_blank(24)} │",
165 f"{_blank(24)} ├── {_series_box(_slot(semifinals, 1), 24)} ┘",
166 f"{_series_box(_slot(first_round, 3), 24)} ┘",
167 ]
168 )
169
170 return "\n".join(lines)
171
172
173def _render_finals(summaries: dict) -> str:
174 finals = [summary for summary in summaries.values() if summary["round"] == 4]
175 final = finals[0] if finals else None
176
177 lines = [
178 f"{BOLD}NBA FINALS{END}",
179 "West Champion East Champion",
180 f"{_series_box(final, 24)}",
181 ]
182 if final is None:
183 lines.append("Winner TBD")
184 elif final["winner"]:
185 lines.append(f"{GREEN}{final['winner']} wins the Finals{END}")
186 return "\n".join(lines)
187
188
189def _conference_round(summaries: dict, conference: str, round_number: int) -> list:
190 return sorted(
191 [
192 summary
193 for summary in summaries.values()
194 if summary["conference"] == conference[:4]
195 and summary["round"] == round_number
196 ],
197 key=lambda summary: summary["slot"],
198 )
199
200
201def _slot(items: list, idx: int) -> Optional[dict]:
202 return items[idx] if idx < len(items) else None
203
204
205def _series_box(summary: Optional[dict], width: int) -> str:
206 if summary is None:
207 return _blank(width, "TBD")
208
209 visitor = _team_line(
210 summary["visitor_abbr"],
211 summary["visitor_wins"],
212 summary["winner"] == summary["visitor_abbr"],
213 )
214 home = _team_line(
215 summary["home_abbr"],
216 summary["home_wins"],
217 summary["winner"] == summary["home_abbr"],
218 )
219 return f"{visitor} / {home}".ljust(width)
220
221
222def _team_line(abbr: str, wins: int, won_series: bool) -> str:
223 label = f"{abbr} {wins}"
224 if won_series:
225 return f"{label}*"
226 return label
227
228
229def _winner(
230 team_map: dict,
231 home_id: int,
232 visitor_id: int,
233 home_wins: int,
234 visitor_wins: int,
235) -> str:
236 if home_wins >= 4:
237 return _abbr(team_map, home_id)
238 if visitor_wins >= 4:
239 return _abbr(team_map, visitor_id)
240 return ""
241
242
243def _blank(width: int, text: str = "") -> str:
244 return text.ljust(width)
245
246
247def _center(text: str, width: int) -> str:
248 return text.center(width)
249
250
251def _round_number(series_id: str) -> int:
252 try:
253 return int(series_id[-3:-1])
254 except ValueError:
255 return 0
256
257
258def _series_slot(series_id: str) -> int:
259 try:
260 return int(series_id[-1])
261 except ValueError:
262 return 0
263
264
265def _conference(series_id: str) -> str:
266 round_number = _round_number(series_id)
267 slot = _series_slot(series_id)
268
269 if round_number == 4:
270 return "NBA"
271 if round_number == 3:
272 return "East" if slot == 0 else "West"
273 if round_number == 2:
274 return "East" if slot <= 1 else "West"
275 if round_number == 1:
276 return "East" if slot <= 3 else "West"
277 return ""
278
279
280def _abbr(team_map: dict, team_id: int) -> str:
281 return team_map.get(team_id, {}).get("abbr", str(team_id))
nba/tui/app.py +18 −2
@@ -13,6 +13,7 @@ from textual.containers import Horizontal
1313from textual.widgets import Footer, Header, Static, TabbedContent, TabPane
1414
1515from nba import fetch_data
16from nba import bracket as bracket_mod
1617from nba import leaders as leaders_mod
1718from nba import playoff as playoff_mod
1819from nba import box_score as box_score_mod
@@ -32,6 +33,7 @@ class NBAApp(App):
3233 ("t", "show_standings", "Standings"),
3334 ("l", "show_leaders", "Leaders"),
3435 ("p", "show_playoff", "Playoff"),
36 ("k", "show_bracket", "Bracket"),
3537 ("b", "show_boxscore", "Box Score"),
3638 ("r", "refresh_now", "Refresh"),
3739 ("comma", "prev_category", "◀ Cat"),
@@ -49,6 +51,7 @@ class NBAApp(App):
4951 self._leaders_data: dict | None = None
5052 self._leaders_cat_idx: int = 0
5153 self._playoff_data: dict | None = None
54 self._bracket_data: dict | None = None
5255
5356 def compose(self) -> ComposeResult:
5457 yield Header()
@@ -65,6 +68,8 @@ class NBAApp(App):
6568 with Horizontal(id="playoff-container"):
6669 yield Static("Loading...", id="playoff-west-content")
6770 yield Static("Loading...", id="playoff-east-content")
71 with TabPane("Bracket", id="bracket"):
72 yield Static("Loading...", id="bracket-content")
6873 with TabPane("Box Score", id="boxscore"):
6974 with Horizontal(id="boxscore-container"):
7075 yield Static(
@@ -84,7 +89,7 @@ class NBAApp(App):
8489 # ------------------------------------------------------------------ #
8590
8691 async def _do_refresh(self) -> None:
87 """Fetch scores, standings, leaders, and playoff picture in parallel."""
92 """Fetch scores, standings, leaders, playoff picture, and bracket in parallel."""
8893 loop = asyncio.get_event_loop()
8994 cat = leaders_mod.CATEGORIES[self._leaders_cat_idx][0]
9095
@@ -92,10 +97,11 @@ class NBAApp(App):
9297 loop.run_in_executor(None, fetch_data.fetch_data),
9398 loop.run_in_executor(None, lambda: leaders_mod.fetch_leaders(cat)),
9499 loop.run_in_executor(None, playoff_mod.fetch_playoff_picture),
100 loop.run_in_executor(None, bracket_mod.fetch_bracket),
95101 return_exceptions=True,
96102 )
97103
98 games_ranks, leaders_data, playoff_data = results
104 games_ranks, leaders_data, playoff_data, bracket_data = results
99105
100106 if not isinstance(games_ranks, Exception):
101107 self._games, self._ranks = games_ranks
@@ -103,6 +109,8 @@ class NBAApp(App):
103109 self._leaders_data = leaders_data
104110 if not isinstance(playoff_data, Exception):
105111 self._playoff_data = playoff_data
112 if not isinstance(bracket_data, Exception):
113 self._bracket_data = bracket_data
106114
107115 self._update_widgets()
108116 self.query_one(CountdownBar).reset(self.refresh_interval)
@@ -133,6 +141,11 @@ class NBAApp(App):
133141 Text.from_ansi(playoff_mod.get_east_playoff_table(self._playoff_data))
134142 )
135143
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
136149 # ------------------------------------------------------------------ #
137150 # Key handlers #
138151 # ------------------------------------------------------------------ #
@@ -162,6 +175,9 @@ class NBAApp(App):
162175 def action_show_playoff(self) -> None:
163176 self.query_one(TabbedContent).active = "playoff"
164177
178 def action_show_bracket(self) -> None:
179 self.query_one(TabbedContent).active = "bracket"
180
165181 def action_show_boxscore(self) -> None:
166182 self.query_one(TabbedContent).active = "boxscore"
167183
nba/tui/styles.tcss +3 −2
@@ -16,7 +16,7 @@ TabPane {
1616 width: auto;
1717}
1818
19/* Standings & Playoff tabs — two columns side by side */
19/* Standings, Playoff, Bracket, and Box Score tabs */
2020#standings-container,
2121#playoff-container,
2222#boxscore-container {
@@ -27,7 +27,8 @@ TabPane {
2727#east-content,
2828#west-content,
2929#playoff-east-content,
30#playoff-west-content {
30#playoff-west-content,
31#bracket-content {
3132 width: 1fr;
3233 overflow: auto auto;
3334 padding-right: 2;
nba/tui/widgets.py +2 −1
@@ -41,7 +41,8 @@ class CountdownBar(Static):
4141 else:
4242 self.update(
4343 f"Next refresh in {value}s | "
44 "\\[1-9] Box Score | \\[</>] Leaders Cat | \\[r] Refresh | \\[q] Quit"
44 "\\[1-9] Box Score | \\[k] Bracket | "
45 "\\[</>] Leaders Cat | \\[r] Refresh | \\[q] Quit"
4546 )
4647
4748 def reset(self, interval: int) -> None:
nba_scores.egg-info/PKG-INFO +4 −11
@@ -1,8 +1,8 @@
11Metadata-Version: 2.4
22Name: nba-scores
3Version: 0.1.7
3Version: 0.2.2
44Summary: A CLI interface to get current NBA scores and standings.
5Author-email: Christian Cleberg <hello@cmc.pub>
5Author-email: Christian Cleberg <hello@cleberg.net>
66License-Expression: GPL-3.0-or-later
77Project-URL: Homepage, https://git.cleberg.net/nba-scores.git
88Project-URL: Issues, https://git.cleberg.net/nba-scores.git
@@ -38,18 +38,10 @@ auto-refresh.
3838
3939[Back to top](#table-of-contents)
4040
41## PyPi
42
43```shell
44pipx install nba-scores
45```
46
47## Manual
48
4941```shell
5042git clone https://git.cleberg.net/nba-scores.git
5143cd nba-scores
52uv sync
44pipx install .
5345```
5446
5547# Usage
@@ -81,6 +73,7 @@ Optional flags:
8173| Standings | `t` | East / West conference standings, side by side |
8274| Leaders | `l` | Top 25 players by stat category |
8375| Playoff Picture | `p` | Conference seeding with clinch and elimination status |
76| Bracket | `k` | Playoff series matchups and series records |
8477| Box Score | `b` | Per-player live stats for a selected game |
8578
8679### TUI key bindings
nba_scores.egg-info/SOURCES.txt +1
@@ -4,6 +4,7 @@ pyproject.toml
44nba/__init__.py
55nba/__main__.py
66nba/box_score.py
7nba/bracket.py
78nba/cli.py
89nba/fetch_data.py
910nba/leaders.py
pyproject.toml +2 −2
@@ -1,8 +1,8 @@
11[project]
22name = "nba-scores"
3version = "0.1.7"
3version = "0.2.2"
44authors = [
5 { name="Christian Cleberg", email="hello@cmc.pub" },
5 { name="Christian Cleberg", email="hello@cleberg.net" },
66]
77description = "A CLI interface to get current NBA scores and standings."
88readme = "README.md"
uv.lock +1 −1
@@ -361,7 +361,7 @@ wheels = [
361361
362362[[package]]
363363name = "nba-scores"
364version = "0.1.7"
364version = "0.2.1"
365365source = { editable = "." }
366366dependencies = [
367367 { name = "argparse" },