diff options
Diffstat (limited to 'build/lib/nba/tui/app.py')
| -rw-r--r-- | build/lib/nba/tui/app.py | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/build/lib/nba/tui/app.py b/build/lib/nba/tui/app.py new file mode 100644 index 0000000..bfdec49 --- /dev/null +++ b/build/lib/nba/tui/app.py @@ -0,0 +1,221 @@ +""" +Textual TUI application for NBA scores and standings. +""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from rich.text import Text +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Footer, Header, Static, TabbedContent, TabPane + +from nba import fetch_data +from nba import leaders as leaders_mod +from nba import playoff as playoff_mod +from nba import box_score as box_score_mod +from nba.scores import get_scoreboard_table +from nba.standings import get_east_standings_table, get_west_standings_table +from nba.tui.widgets import CountdownBar, ScoresWidget + + +class NBAApp(App): + """Live NBA scores and standings TUI with auto-refresh.""" + + CSS_PATH = Path(__file__).parent / "styles.tcss" + + BINDINGS = [ + ("q", "quit", "Quit"), + ("s", "show_scores", "Scores"), + ("t", "show_standings", "Standings"), + ("l", "show_leaders", "Leaders"), + ("p", "show_playoff", "Playoff"), + ("b", "show_boxscore", "Box Score"), + ("r", "refresh_now", "Refresh"), + ("comma", "prev_category", "◀ Cat"), + ("full_stop", "next_category", "Cat ▶"), + ] + + TITLE = "NBA Scores" + + def __init__(self, initial_tab: str = "scores", refresh_interval: int = 60) -> None: + super().__init__() + self.initial_tab = initial_tab + self.refresh_interval = refresh_interval + self._games: dict | None = None + self._ranks: dict | None = None + self._leaders_data: dict | None = None + self._leaders_cat_idx: int = 0 + self._playoff_data: dict | None = None + + def compose(self) -> ComposeResult: + yield Header() + with TabbedContent(initial=self.initial_tab): + with TabPane("Scores", id="scores"): + yield ScoresWidget("Loading...", id="scores-content") + with TabPane("Standings", id="standings"): + with Horizontal(id="standings-container"): + yield Static("Loading...", id="west-content") + yield Static("Loading...", id="east-content") + with TabPane("Leaders", id="leaders"): + yield Static("Loading...", id="leaders-content") + with TabPane("Playoff", id="playoff"): + with Horizontal(id="playoff-container"): + yield Static("Loading...", id="playoff-west-content") + yield Static("Loading...", id="playoff-east-content") + with TabPane("Box Score", id="boxscore"): + with Horizontal(id="boxscore-container"): + yield Static( + "Press 1–9 to load a game from the Scores tab.", + id="home-content", + ) + yield Static("", id="away-content") + yield CountdownBar(self.refresh_interval, id="countdown") + yield Footer() + + async def on_mount(self) -> None: + await self._do_refresh() + self.set_interval(self.refresh_interval, self._do_refresh) + + # ------------------------------------------------------------------ # + # Refresh logic # + # ------------------------------------------------------------------ # + + async def _do_refresh(self) -> None: + """Fetch scores, standings, leaders, and playoff picture in parallel.""" + loop = asyncio.get_event_loop() + cat = leaders_mod.CATEGORIES[self._leaders_cat_idx][0] + + results = await asyncio.gather( + loop.run_in_executor(None, fetch_data.fetch_data), + loop.run_in_executor(None, lambda: leaders_mod.fetch_leaders(cat)), + loop.run_in_executor(None, playoff_mod.fetch_playoff_picture), + return_exceptions=True, + ) + + games_ranks, leaders_data, playoff_data = results + + if not isinstance(games_ranks, Exception): + self._games, self._ranks = games_ranks + if not isinstance(leaders_data, Exception): + self._leaders_data = leaders_data + if not isinstance(playoff_data, Exception): + self._playoff_data = playoff_data + + self._update_widgets() + self.query_one(CountdownBar).reset(self.refresh_interval) + + def _update_widgets(self) -> None: + if self._games and self._ranks: + self.query_one(ScoresWidget).update( + Text.from_ansi(get_scoreboard_table(self._games, self._ranks)) + ) + self.query_one("#west-content", Static).update( + Text.from_ansi(get_west_standings_table(self._ranks)) + ) + self.query_one("#east-content", Static).update( + Text.from_ansi(get_east_standings_table(self._ranks)) + ) + + if self._leaders_data: + cat = leaders_mod.CATEGORIES[self._leaders_cat_idx][0] + self.query_one("#leaders-content", Static).update( + Text.from_ansi(leaders_mod.get_leaders_table(self._leaders_data, cat)) + ) + + if self._playoff_data: + self.query_one("#playoff-west-content", Static).update( + Text.from_ansi(playoff_mod.get_west_playoff_table(self._playoff_data)) + ) + self.query_one("#playoff-east-content", Static).update( + Text.from_ansi(playoff_mod.get_east_playoff_table(self._playoff_data)) + ) + + # ------------------------------------------------------------------ # + # Key handlers # + # ------------------------------------------------------------------ # + + def on_key(self, event) -> None: + """Handle 1–9 to select a game for the box score tab.""" + char = event.character + if char and char.isdigit() and char != "0": + asyncio.create_task(self._load_box_score(int(char) - 1)) + + # ------------------------------------------------------------------ # + # Actions # + # ------------------------------------------------------------------ # + + async def action_refresh_now(self) -> None: + await self._do_refresh() + + def action_show_scores(self) -> None: + self.query_one(TabbedContent).active = "scores" + + def action_show_standings(self) -> None: + self.query_one(TabbedContent).active = "standings" + + def action_show_leaders(self) -> None: + self.query_one(TabbedContent).active = "leaders" + + def action_show_playoff(self) -> None: + self.query_one(TabbedContent).active = "playoff" + + def action_show_boxscore(self) -> None: + self.query_one(TabbedContent).active = "boxscore" + + async def action_prev_category(self) -> None: + self._leaders_cat_idx = (self._leaders_cat_idx - 1) % len(leaders_mod.CATEGORIES) + await self._refresh_leaders() + + async def action_next_category(self) -> None: + self._leaders_cat_idx = (self._leaders_cat_idx + 1) % len(leaders_mod.CATEGORIES) + await self._refresh_leaders() + + # ------------------------------------------------------------------ # + # Helpers # + # ------------------------------------------------------------------ # + + async def _refresh_leaders(self) -> None: + cat, label = leaders_mod.CATEGORIES[self._leaders_cat_idx] + self.query_one("#leaders-content", Static).update(f"Loading {label} leaders...") + loop = asyncio.get_event_loop() + try: + data = await loop.run_in_executor(None, lambda: leaders_mod.fetch_leaders(cat)) + self._leaders_data = data + self.query_one("#leaders-content", Static).update( + Text.from_ansi(leaders_mod.get_leaders_table(data, cat)) + ) + except Exception as exc: + self.query_one("#leaders-content", Static).update(f"Error loading leaders: {exc}") + + async def _load_box_score(self, game_idx: int) -> None: + if not self._games: + return + games = self._games["scoreboard"]["games"] + if game_idx >= len(games): + return + + game = games[game_idx] + game_id = game["gameId"] + home = game["homeTeam"]["teamName"] + away = game["awayTeam"]["teamName"] + + self.query_one("#home-content", Static).update( + f"Loading box score: {away} @ {home}..." + ) + self.query_one("#away-content", Static).update("") + self.query_one(TabbedContent).active = "boxscore" + + loop = asyncio.get_event_loop() + try: + data = await loop.run_in_executor( + None, lambda: box_score_mod.fetch_box_score(game_id) + ) + home_table, away_table = box_score_mod.get_box_score_tables(data) + self.query_one("#home-content", Static).update(Text.from_ansi(home_table)) + self.query_one("#away-content", Static).update(Text.from_ansi(away_table)) + except Exception as exc: + self.query_one("#home-content", Static).update(f"Error loading box score: {exc}") + self.query_one("#away-content", Static).update("") |
