-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathf1_cli.py
More file actions
152 lines (123 loc) · 5.3 KB
/
Copy pathf1_cli.py
File metadata and controls
152 lines (123 loc) · 5.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
F1 StratLab — Interactive CLI Launcher
Usage
-----
python scripts/f1_cli.py
Menu
----
1 Single Driver — lap-by-lap strategy for one driver
2 Head-to-Head — two drivers, same race, shown back-to-back
3 Quit
All UI logic lives in scripts/cli/:
theme.py — colors, console, ASCII banner
pickers.py — Rich prompts (mode / race / driver / laps / provider)
runner.py — subprocess helpers + mode handlers
"""
from __future__ import annotations
import logging as _logging
import sys
import warnings
from pathlib import Path
# Ensure UTF-8 on Windows terminals
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
if hasattr(sys.stderr, "reconfigure"):
try:
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# Silence noisy library output before any heavy imports
warnings.filterwarnings("ignore", message=".*builtin type.*__module__.*")
_logging.getLogger("transformers").setLevel(_logging.ERROR)
_logging.getLogger("setfit").setLevel(_logging.ERROR)
_logging.getLogger("sentence_transformers").setLevel(_logging.ERROR)
_logging.getLogger("torch").setLevel(_logging.ERROR)
# ── Paths ──────────────────────────────────────────────────────────────────────
_SCRIPT_DIR = Path(__file__).resolve().parent
_REPO_ROOT = next(
(p for p in [_SCRIPT_DIR, *_SCRIPT_DIR.parents] if (p / ".git").exists()),
_SCRIPT_DIR.parent,
)
# Add scripts/ to sys.path so `from cli.*` resolves correctly
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
# Also add the repo root (or tool install parent) so `from src.f1_strat_manager…`
# resolves when running from a clone. uv tool install already installs src/* as
# an importable package, so this is a no-op in that case.
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
try:
from dotenv import load_dotenv
_env = _REPO_ROOT / ".env"
if _env.exists():
load_dotenv(_env)
except ImportError:
pass
# ── CLI package imports ────────────────────────────────────────────────────────
from cli.pickers import ask_again, discover_races, pick_mode # noqa: E402
from cli.runner import run_h2h, run_single # noqa: E402
from cli.theme import F1_GRAY, console, make_banner # noqa: E402
from rich.rule import Rule # noqa: E402
# ─────────────────────────────────────────────────────────────────────────────
# Main loop
# ─────────────────────────────────────────────────────────────────────────────
def _run_wizard() -> None:
"""Inner wizard loop. Wrapped by :func:`main` for Ctrl+C handling."""
console.print()
console.print(make_banner())
# First-run bootstrap: download race data + model weights from HF Hub when
# the cache is fresh. Editable-dev checkouts (repo has data/raw/2025/<gp>/
# + data/models/...) short-circuit is_first_run() so this is a no-op for
# the repo contributor workflow.
try:
from src.f1_strat_manager.data_cache import ensure_setup, is_first_run
if is_first_run():
ensure_setup()
except ImportError:
# Package not installed (extremely rare — only if the user copied
# scripts/ in isolation). Fall through and let discover_races error.
pass
races = discover_races(_REPO_ROOT, year=2025)
if not races:
console.print(
f"\n [{F1_GRAY}]No races found in data/raw/2025/. "
f"Run scripts/download_data.py first.[/{F1_GRAY}]\n"
)
return
while True:
mode = pick_mode()
if mode == "quit":
break
elif mode == "single":
run_single(races, _REPO_ROOT, _SCRIPT_DIR)
elif mode == "h2h":
run_h2h(races, _REPO_ROOT, _SCRIPT_DIR)
if not ask_again():
break
console.print()
console.print(Rule(style=F1_GRAY))
console.print()
console.print(make_banner())
console.print()
console.print(f" [{F1_GRAY}]Goodbye. Chequered flag.[/{F1_GRAY}]")
console.print()
def main() -> None:
"""Console-script entry point (see ``[project.scripts]`` in pyproject.toml).
Wraps :func:`_run_wizard` in a :class:`KeyboardInterrupt` guard so
Ctrl+C anywhere in the wizard (arrow pickers, ``Prompt.ask`` calls,
the simulation subprocess) prints a single-line ``Interrupted.`` in
italic dim and exits with status 130, the conventional SIGINT code,
instead of leaking a stack trace.
"""
try:
_run_wizard()
except KeyboardInterrupt:
console.print()
console.print(f" [italic {F1_GRAY}]Interrupted.[/italic {F1_GRAY}]")
console.print()
sys.exit(130)
if __name__ == "__main__":
main()