cfbtui 1.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. cfbtui/__init__.py +4 -0
  2. cfbtui/__main__.py +223 -0
  3. cfbtui/app.py +623 -0
  4. cfbtui/clipboard.py +16 -0
  5. cfbtui/config.py +304 -0
  6. cfbtui/espn/__init__.py +1 -0
  7. cfbtui/espn/client.py +207 -0
  8. cfbtui/espn/models.py +352 -0
  9. cfbtui/espn/parse.py +604 -0
  10. cfbtui/espn/periods.py +46 -0
  11. cfbtui/news.py +159 -0
  12. cfbtui/poll.py +336 -0
  13. cfbtui/presence.py +41 -0
  14. cfbtui/selfupdate.py +414 -0
  15. cfbtui/share.py +128 -0
  16. cfbtui/state/__init__.py +1 -0
  17. cfbtui/state/events.py +222 -0
  18. cfbtui/state/store.py +338 -0
  19. cfbtui/state/upset.py +91 -0
  20. cfbtui/ui/__init__.py +0 -0
  21. cfbtui/ui/app.tcss +19 -0
  22. cfbtui/ui/filters.py +122 -0
  23. cfbtui/ui/glyphs.py +256 -0
  24. cfbtui/ui/labels.py +79 -0
  25. cfbtui/ui/pulse.py +45 -0
  26. cfbtui/ui/screens/__init__.py +1 -0
  27. cfbtui/ui/screens/conferences.py +57 -0
  28. cfbtui/ui/screens/detail.py +453 -0
  29. cfbtui/ui/screens/help.py +48 -0
  30. cfbtui/ui/screens/news.py +298 -0
  31. cfbtui/ui/screens/rankings.py +176 -0
  32. cfbtui/ui/screens/settings.py +88 -0
  33. cfbtui/ui/story.py +139 -0
  34. cfbtui/ui/theme.py +118 -0
  35. cfbtui/ui/widgets/__init__.py +0 -0
  36. cfbtui/ui/widgets/game_card.py +392 -0
  37. cfbtui/ui/widgets/score_grid.py +271 -0
  38. cfbtui/ui/widgets/status_bar.py +131 -0
  39. cfbtui/ui/widgets/ticker.py +241 -0
  40. cfbtui/update.py +160 -0
  41. cfbtui/version.py +113 -0
  42. cfbtui-1.13.1.dist-info/METADATA +193 -0
  43. cfbtui-1.13.1.dist-info/RECORD +47 -0
  44. cfbtui-1.13.1.dist-info/WHEEL +5 -0
  45. cfbtui-1.13.1.dist-info/entry_points.txt +2 -0
  46. cfbtui-1.13.1.dist-info/licenses/LICENSE +21 -0
  47. cfbtui-1.13.1.dist-info/top_level.txt +1 -0
cfbtui/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """cfbtui: a colorful college football scoreboard for the terminal."""
2
+
3
+ # FibVer. Do not edit by hand; use `python -m cfbtui.version bump <part> --write`.
4
+ __version__ = "1.13.1"
cfbtui/__main__.py ADDED
@@ -0,0 +1,223 @@
1
+ """Command-line entry point: ``python -m cfbtui`` or the ``cfbtui`` console script.
2
+
3
+ Flags override the config file for this run only and are never written back.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import contextlib
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ from functools import partial
13
+ from pathlib import Path
14
+
15
+ from cfbtui import __version__
16
+ from cfbtui.config import GROUPS, LAYOUTS, Config, LoadResult, apply_overrides, default_path, load_config
17
+ from cfbtui.espn.periods import LAST_REGULAR_WEEK, MAX_WEEK, MIN_WEEK, NAMED
18
+ from cfbtui.ui.glyphs import GLYPH_SET_NAMES
19
+
20
+
21
+ def _week(text: str) -> int:
22
+ if text.strip().lower() in NAMED:
23
+ return NAMED[text.strip().lower()]
24
+ try:
25
+ value = int(text)
26
+ except ValueError:
27
+ raise argparse.ArgumentTypeError(f"{text!r} is not a week number, 'bowls' or 'playoff'") from None
28
+ if not MIN_WEEK <= value <= MAX_WEEK:
29
+ raise argparse.ArgumentTypeError(f"week must be {MIN_WEEK} to {MAX_WEEK} (16 = bowls, 17 = playoff)")
30
+ return value
31
+
32
+
33
+ def build_parser() -> argparse.ArgumentParser:
34
+ parser = argparse.ArgumentParser(
35
+ prog="cfbtui",
36
+ description="A colorful college football scoreboard for the terminal.",
37
+ )
38
+ parser.add_argument("--version", action="version", version=f"cfbtui {__version__}")
39
+ parser.add_argument(
40
+ "--week",
41
+ type=_week,
42
+ metavar="N",
43
+ help=f"start on week N ({MIN_WEEK}-{LAST_REGULAR_WEEK}), 'bowls' or 'playoff' instead of the current week",
44
+ )
45
+ parser.add_argument("--group", type=str.lower, choices=sorted(GROUPS), help="FBS or FCS board")
46
+ parser.add_argument(
47
+ "--glyphs",
48
+ type=str.lower,
49
+ choices=GLYPH_SET_NAMES,
50
+ metavar="NAME",
51
+ help=f"glyph set: {', '.join(GLYPH_SET_NAMES)}",
52
+ )
53
+ parser.add_argument("--layout", type=str.lower, choices=LAYOUTS, help="card grid or one line per game")
54
+ parser.add_argument("--config", type=Path, metavar="PATH", help=f"config file (default: {default_path()})")
55
+ parser.add_argument("--no-config", action="store_true", help="ignore the config file and do not save to it")
56
+ parser.add_argument("--selftest", action="store_true", help=argparse.SUPPRESS)
57
+ parser.add_argument("--update-test", choices=("apply", "restart"), help=argparse.SUPPRESS)
58
+ parser.add_argument("--no-update-check", action="store_true", help="do not look for a newer release")
59
+ return parser
60
+
61
+
62
+ def resolve(argv: list[str] | None = None) -> tuple[Config, LoadResult, Path | None]:
63
+ """Parse flags, load the file, and layer the flags on top.
64
+
65
+ Returns the effective config, the raw load result (for its warnings) and
66
+ the path favorites will be saved to (``None`` under ``--no-config``).
67
+ """
68
+ args = build_parser().parse_args(argv)
69
+ if args.no_config:
70
+ loaded = LoadResult(Config())
71
+ save_path = None
72
+ else:
73
+ save_path = args.config if args.config is not None else default_path()
74
+ loaded = load_config(save_path)
75
+ config = apply_overrides(
76
+ loaded.config,
77
+ week=args.week,
78
+ group=args.group,
79
+ glyph_set=args.glyphs,
80
+ layout=args.layout,
81
+ check_updates=False if args.no_update_check else None,
82
+ )
83
+ return config, loaded, save_path
84
+
85
+
86
+ def selftest() -> int:
87
+ """Start the app headless against an empty board, so a frozen build proves its imports, CSS and tzdata."""
88
+ import asyncio
89
+ from zoneinfo import ZoneInfo
90
+
91
+ from cfbtui.app import CfbApp
92
+ from cfbtui.espn.models import Scoreboard, Summary
93
+
94
+ class EmptyBoard:
95
+ async def scoreboard(self, *, week: int | None = None, group: int = 80) -> Scoreboard:
96
+ return Scoreboard()
97
+
98
+ async def rankings(self) -> tuple:
99
+ return ()
100
+
101
+ async def summary(self, event_id: str) -> Summary:
102
+ raise NotImplementedError # the selftest never opens a game
103
+
104
+ async def news(self, limit: int = 30) -> tuple:
105
+ return ()
106
+
107
+ async def story(self, url: str) -> str:
108
+ raise NotImplementedError # the selftest never opens a story
109
+
110
+ async def run() -> None:
111
+ app = CfbApp(client=EmptyBoard(), config=Config())
112
+ async with app.run_test(size=(100, 30)) as pilot:
113
+ await pilot.pause()
114
+
115
+ ZoneInfo("America/New_York")
116
+ asyncio.run(run())
117
+ print("selftest ok")
118
+ return 0
119
+
120
+
121
+ def restart(target: Path, args: list[str], execve=os.execve, call=subprocess.call) -> None:
122
+ """Start the installed binary again with the same arguments and a clean PyInstaller environment.
123
+
124
+ Windows has no exec, so it waits for the child.
125
+ """
126
+ from cfbtui.selfupdate import child_env
127
+
128
+ if sys.platform == "win32":
129
+ raise SystemExit(call([str(target), *args], env=child_env()))
130
+ execve(target, [str(target), *args], child_env())
131
+
132
+
133
+ def install_update(updater, staged, app) -> bool:
134
+ """Swap the staged binary in after the app has exited, or discard it. Returns whether it was installed.
135
+
136
+ A staged update is installed only if cfbtui exited normally and auto-update is still on (or the user chose
137
+ Restart now). A failure leaves the old version in place and says why on stderr.
138
+ """
139
+ from cfbtui.selfupdate import UpdateError
140
+
141
+ if app.return_code not in (0, None) or not (app.config.updates.auto or app.restart_requested):
142
+ staged.discard()
143
+ return False
144
+ try:
145
+ updater.apply(staged)
146
+ except UpdateError as exc: # RollbackError says how to recover by hand
147
+ print(f"cfbtui: the update was not installed: {exc}", file=sys.stderr)
148
+ return False
149
+ return True
150
+
151
+
152
+ def update_test(mode: str, updater, argv: list[str]) -> int:
153
+ """Run the update path with no TUI (the end-to-end workflow cannot drive one): check, stage and probe
154
+ a newer release, install it, and for "restart" start the new binary. Exit 1 when the update fails."""
155
+ import asyncio
156
+ from types import SimpleNamespace
157
+
158
+ from cfbtui.selfupdate import UpdateError
159
+ from cfbtui.update import asset_name, check_for_update, decide
160
+
161
+ info = asyncio.run(check_for_update(__version__))
162
+ if info is None:
163
+ print(f"cfbtui {__version__}: up to date")
164
+ return 0
165
+ if decide(updater.kind, auto=True, no_config=False, current=__version__, info=info,
166
+ asset=asset_name()) != "download":
167
+ print("cfbtui: this release would not be downloaded", file=sys.stderr)
168
+ return 2
169
+ try:
170
+ staged = updater.stage(info)
171
+ except UpdateError as exc:
172
+ print(f"cfbtui: could not download {info.version}: {exc}", file=sys.stderr)
173
+ return 1
174
+ app = SimpleNamespace(return_code=0, config=Config(), restart_requested=mode == "restart")
175
+ if not install_update(updater, staged, app):
176
+ return 1
177
+ print(f"cfbtui {info.version} installed")
178
+ if mode == "restart":
179
+ restart(updater.target, argv)
180
+ return 0
181
+
182
+
183
+ def main(argv: list[str] | None = None) -> int:
184
+ if argv is None and "--selftest" in sys.argv[1:]:
185
+ return selftest()
186
+ config, loaded, save_path = resolve(argv)
187
+
188
+ # Imported lazily so `--version` stays instant and never touches the terminal.
189
+ from cfbtui.app import CfbApp
190
+ from cfbtui.selfupdate import Updater, cleanup_old, sweep
191
+ from cfbtui.update import check_for_update
192
+
193
+ update_check = partial(check_for_update, __version__) if config.updates.check else None
194
+ updater = Updater.detect(__version__)
195
+ if updater.kind == "binary":
196
+ with contextlib.suppress(Exception):
197
+ cleanup_old(updater.target) # drop the previous version, or restore it after a crashed swap
198
+ sweep(updater.target.parent) # and any download an earlier run left behind
199
+ mode = build_parser().parse_args(argv).update_test
200
+ if mode is not None:
201
+ if updater.kind != "binary":
202
+ print("cfbtui: --update-test needs a standalone binary", file=sys.stderr)
203
+ return 2
204
+ return update_test(mode, updater, sys.argv[1:] if argv is None else argv)
205
+ app_updater = None if update_check is None else updater # --no-update-check: no checking and no downloading
206
+ app = CfbApp(
207
+ config=config,
208
+ file_config=loaded.config,
209
+ config_path=save_path,
210
+ warnings=loaded.warnings,
211
+ update_check=update_check,
212
+ updater=app_updater,
213
+ )
214
+ app.run()
215
+ if app.staged is not None and app_updater is not None:
216
+ installed = install_update(app_updater, app.staged, app)
217
+ if installed and app.restart_requested:
218
+ restart(app_updater.target, sys.argv[1:] if argv is None else argv)
219
+ return 0
220
+
221
+
222
+ if __name__ == "__main__":
223
+ raise SystemExit(main())