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.
- cfbtui/__init__.py +4 -0
- cfbtui/__main__.py +223 -0
- cfbtui/app.py +623 -0
- cfbtui/clipboard.py +16 -0
- cfbtui/config.py +304 -0
- cfbtui/espn/__init__.py +1 -0
- cfbtui/espn/client.py +207 -0
- cfbtui/espn/models.py +352 -0
- cfbtui/espn/parse.py +604 -0
- cfbtui/espn/periods.py +46 -0
- cfbtui/news.py +159 -0
- cfbtui/poll.py +336 -0
- cfbtui/presence.py +41 -0
- cfbtui/selfupdate.py +414 -0
- cfbtui/share.py +128 -0
- cfbtui/state/__init__.py +1 -0
- cfbtui/state/events.py +222 -0
- cfbtui/state/store.py +338 -0
- cfbtui/state/upset.py +91 -0
- cfbtui/ui/__init__.py +0 -0
- cfbtui/ui/app.tcss +19 -0
- cfbtui/ui/filters.py +122 -0
- cfbtui/ui/glyphs.py +256 -0
- cfbtui/ui/labels.py +79 -0
- cfbtui/ui/pulse.py +45 -0
- cfbtui/ui/screens/__init__.py +1 -0
- cfbtui/ui/screens/conferences.py +57 -0
- cfbtui/ui/screens/detail.py +453 -0
- cfbtui/ui/screens/help.py +48 -0
- cfbtui/ui/screens/news.py +298 -0
- cfbtui/ui/screens/rankings.py +176 -0
- cfbtui/ui/screens/settings.py +88 -0
- cfbtui/ui/story.py +139 -0
- cfbtui/ui/theme.py +118 -0
- cfbtui/ui/widgets/__init__.py +0 -0
- cfbtui/ui/widgets/game_card.py +392 -0
- cfbtui/ui/widgets/score_grid.py +271 -0
- cfbtui/ui/widgets/status_bar.py +131 -0
- cfbtui/ui/widgets/ticker.py +241 -0
- cfbtui/update.py +160 -0
- cfbtui/version.py +113 -0
- cfbtui-1.13.1.dist-info/METADATA +193 -0
- cfbtui-1.13.1.dist-info/RECORD +47 -0
- cfbtui-1.13.1.dist-info/WHEEL +5 -0
- cfbtui-1.13.1.dist-info/entry_points.txt +2 -0
- cfbtui-1.13.1.dist-info/licenses/LICENSE +21 -0
- cfbtui-1.13.1.dist-info/top_level.txt +1 -0
cfbtui/__init__.py
ADDED
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())
|