mirror of
https://github.com/Horizontal42/f1-telemetry.git
synced 2026-11-10 23:32:03 +03:00
session type in rename, race position column, races/ folder structure
This commit is contained in:
parent
cd5976ce0c
commit
c7e1b88999
5 changed files with 105 additions and 32 deletions
|
|
@ -103,9 +103,7 @@ def main() -> None:
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
laps = [load_lap(os.path.join(race_dir, f)) for f in csv_files]
|
laps = [load_lap(os.path.join(race_dir, f)) for f in csv_files]
|
||||||
dirname = os.path.basename(race_dir.rstrip('/\\'))
|
dirname = os.path.basename(race_dir.rstrip('/\\'))
|
||||||
out = os.path.join(
|
out = os.path.join(race_dir, 'reports', dirname + '_race.md')
|
||||||
os.path.dirname(os.path.dirname(race_dir)), 'reports', dirname + '_race.md'
|
|
||||||
)
|
|
||||||
_print_result(*report_race.generate(laps, out, lang))
|
_print_result(*report_race.generate(laps, out, lang))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ from .rename import rename_unprocessed
|
||||||
|
|
||||||
# project root = parent of the tool\ dir
|
# project root = parent of the tool\ dir
|
||||||
_PROJECT_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
_PROJECT_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
_REPORTS_DIR = os.path.join(_PROJECT_ROOT, 'reports')
|
_RACES_DIR = os.path.join(_PROJECT_ROOT, 'races')
|
||||||
|
|
||||||
|
|
||||||
def _race_output_path(race_dir: str) -> str:
|
def _race_output_path(race_dir: str) -> str:
|
||||||
dirname = os.path.basename(race_dir.rstrip('/\\'))
|
dirname = os.path.basename(race_dir.rstrip('/\\'))
|
||||||
return os.path.join(_PROJECT_ROOT, 'reports', dirname + '_race.md')
|
return os.path.join(race_dir, 'reports', dirname + '_race.md')
|
||||||
|
|
||||||
|
|
||||||
class _App:
|
class _App:
|
||||||
|
|
@ -68,7 +68,7 @@ class _App:
|
||||||
self._btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)',
|
self._btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)',
|
||||||
command=self._on_rename)
|
command=self._on_rename)
|
||||||
self._btn_rename.pack(side='left', padx=4)
|
self._btn_rename.pack(side='left', padx=4)
|
||||||
ttk.Button(btn_frame, text='Open reports folder',
|
ttk.Button(btn_frame, text='Open races folder',
|
||||||
command=self._open_reports).pack(side='left', padx=4)
|
command=self._open_reports).pack(side='left', padx=4)
|
||||||
|
|
||||||
log_frame = ttk.LabelFrame(self._root, text='Log')
|
log_frame = ttk.LabelFrame(self._root, text='Log')
|
||||||
|
|
@ -192,7 +192,7 @@ class _App:
|
||||||
self._log_queue.put(None)
|
self._log_queue.put(None)
|
||||||
|
|
||||||
def _on_rename(self) -> None:
|
def _on_rename(self) -> None:
|
||||||
d = filedialog.askdirectory(title='Select folder to rename', initialdir=_PROJECT_ROOT)
|
d = filedialog.askdirectory(title='Select folder to rename', initialdir=_RACES_DIR)
|
||||||
if not d:
|
if not d:
|
||||||
return
|
return
|
||||||
self._set_busy(True)
|
self._set_busy(True)
|
||||||
|
|
@ -216,10 +216,8 @@ class _App:
|
||||||
self._log_queue.put(None)
|
self._log_queue.put(None)
|
||||||
|
|
||||||
def _open_reports(self) -> None:
|
def _open_reports(self) -> None:
|
||||||
if not os.path.isdir(_REPORTS_DIR):
|
target = _RACES_DIR if os.path.isdir(_RACES_DIR) else _PROJECT_ROOT
|
||||||
self._log_append(f'Reports folder does not exist yet: {_REPORTS_DIR}')
|
os.startfile(target)
|
||||||
return
|
|
||||||
os.startfile(_REPORTS_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,37 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
from .parser import _parse_header_row, _parse_value_row
|
from .parser import _parse_header_row, _parse_value_row
|
||||||
|
|
||||||
|
_SESSION_PAT = re.compile(r'^(FP[1-3]|P[1-3]|Q[1-3]|R|Race|Sprint)$', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def lap_token_present(stem: str) -> bool:
|
def lap_token_present(stem: str) -> bool:
|
||||||
return any(re.fullmatch(r'L\d+', t) for t in stem.split('_'))
|
return any(re.fullmatch(r'L\d+', t) for t in stem.split('_'))
|
||||||
|
|
||||||
|
|
||||||
|
def session_token_present(stem: str) -> bool:
|
||||||
|
return any(_SESSION_PAT.fullmatch(t) for t in stem.split('_'))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_processed(stem: str) -> bool:
|
||||||
|
return lap_token_present(stem) and session_token_present(stem)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_tokens(filename: str, session_type: str, lap: int) -> str:
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
tokens = base.split('_')
|
||||||
|
lap_tok = f'L{lap}'
|
||||||
|
for i, t in enumerate(tokens):
|
||||||
|
if re.fullmatch(r'\d+(\.\d+)?', t):
|
||||||
|
tokens = tokens[:i] + [session_type, lap_tok] + tokens[i:]
|
||||||
|
return '_'.join(tokens) + ext
|
||||||
|
if len(tokens) >= 2:
|
||||||
|
tokens = tokens[:2] + [session_type, lap_tok] + tokens[2:]
|
||||||
|
else:
|
||||||
|
tokens += [session_type, lap_tok]
|
||||||
|
return '_'.join(tokens) + ext
|
||||||
|
|
||||||
|
|
||||||
|
# kept for backward compat (tests + external callers)
|
||||||
def insert_lap_token(filename: str, lap: int) -> str:
|
def insert_lap_token(filename: str, lap: int) -> str:
|
||||||
base, ext = os.path.splitext(filename)
|
base, ext = os.path.splitext(filename)
|
||||||
tokens = base.split('_')
|
tokens = base.split('_')
|
||||||
|
|
@ -33,6 +59,15 @@ def read_lap_number(path: str) -> int:
|
||||||
return int(float(track['Lap']))
|
return int(float(track['Lap']))
|
||||||
|
|
||||||
|
|
||||||
|
def read_session_type(path: str) -> str:
|
||||||
|
with open(path, encoding='utf-8') as f:
|
||||||
|
lines = [f.readline() for _ in range(3)]
|
||||||
|
keys = _parse_header_row(lines[1])
|
||||||
|
vals = _parse_value_row(lines[2])
|
||||||
|
session = dict(zip(keys, vals))
|
||||||
|
return session.get('event', '').strip()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RenameResult:
|
class RenameResult:
|
||||||
old: str
|
old: str
|
||||||
|
|
@ -62,15 +97,19 @@ def rename_unprocessed(targets: list[str]) -> list[RenameResult]:
|
||||||
results.append(RenameResult(name, None, 'error', 'not a CSV file'))
|
results.append(RenameResult(name, None, 'error', 'not a CSV file'))
|
||||||
continue
|
continue
|
||||||
stem = os.path.splitext(name)[0]
|
stem = os.path.splitext(name)[0]
|
||||||
if lap_token_present(stem):
|
if _is_processed(stem):
|
||||||
results.append(RenameResult(name, None, 'skipped', 'already has lap number'))
|
results.append(RenameResult(name, None, 'skipped', 'already processed'))
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
lap = read_lap_number(p)
|
lap = read_lap_number(p)
|
||||||
|
session_type = read_session_type(p)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append(RenameResult(name, None, 'error', str(e)))
|
results.append(RenameResult(name, None, 'error', str(e)))
|
||||||
continue
|
continue
|
||||||
new_name = insert_lap_token(name, lap)
|
if not session_type:
|
||||||
|
results.append(RenameResult(name, None, 'error', 'session type not found in CSV'))
|
||||||
|
continue
|
||||||
|
new_name = insert_tokens(name, session_type, lap)
|
||||||
dest = os.path.join(os.path.dirname(p), new_name)
|
dest = os.path.join(os.path.dirname(p), new_name)
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
results.append(RenameResult(name, new_name, 'error', 'target exists'))
|
results.append(RenameResult(name, new_name, 'error', 'target exists'))
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,14 @@ def _header(laps: list[Lap]) -> str:
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _race_pos(lap: Lap) -> str:
|
||||||
|
pos = lap.ch.get('RacePosition', [])
|
||||||
|
if not pos:
|
||||||
|
return '—'
|
||||||
|
v = int(pos[-1])
|
||||||
|
return str(v) if v > 0 else '—'
|
||||||
|
|
||||||
|
|
||||||
def _lap_times_table(laps: list[Lap], stints: list[int], ages: list[int]) -> str:
|
def _lap_times_table(laps: list[Lap], stints: list[int], ages: list[int]) -> str:
|
||||||
# find fastest valid lap time
|
# find fastest valid lap time
|
||||||
best_time = None
|
best_time = None
|
||||||
|
|
@ -110,6 +118,7 @@ def _lap_times_table(laps: list[Lap], stints: list[int], ages: list[int]) -> str
|
||||||
|
|
||||||
rows.append([
|
rows.append([
|
||||||
lap_n,
|
lap_n,
|
||||||
|
_race_pos(lap),
|
||||||
_fmt_laptime(lt) if lt > 0 else '—',
|
_fmt_laptime(lt) if lt > 0 else '—',
|
||||||
_fmt_sector(s1) if s1 > 0 else '—',
|
_fmt_sector(s1) if s1 > 0 else '—',
|
||||||
_fmt_sector(s2) if s2 > 0 else '—',
|
_fmt_sector(s2) if s2 > 0 else '—',
|
||||||
|
|
@ -121,7 +130,7 @@ def _lap_times_table(laps: list[Lap], stints: list[int], ages: list[int]) -> str
|
||||||
])
|
])
|
||||||
|
|
||||||
return md_table(
|
return md_table(
|
||||||
['Lap', 'Time', 'S1', 'S2', 'S3', 'Tyre', 'Age', 'Fuel kg', 'Note'],
|
['Lap', 'Pos', 'Time', 'S1', 'S2', 'S3', 'Tyre', 'Age', 'Fuel kg', 'Note'],
|
||||||
rows,
|
rows,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import pytest
|
import pytest
|
||||||
from telemetry.rename import lap_token_present, insert_lap_token, read_lap_number, rename_unprocessed
|
from telemetry.rename import (
|
||||||
|
lap_token_present, insert_lap_token, session_token_present,
|
||||||
|
insert_tokens, read_lap_number, read_session_type, rename_unprocessed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- insert_lap_token ---
|
# --- insert_lap_token (backward compat) ---
|
||||||
|
|
||||||
@pytest.mark.parametrize("filename,lap,expected", [
|
@pytest.mark.parametrize("filename,lap,expected", [
|
||||||
# float token found → insert before it
|
|
||||||
("zandvoort_P_74.152_ferrari.csv", 8, "zandvoort_P_L8_74.152_ferrari.csv"),
|
("zandvoort_P_74.152_ferrari.csv", 8, "zandvoort_P_L8_74.152_ferrari.csv"),
|
||||||
# single non-float token → append
|
|
||||||
("weirdname.csv", 3, "weirdname_L3.csv"),
|
("weirdname.csv", 3, "weirdname_L3.csv"),
|
||||||
# two non-float tokens, no float → insert at index 2 (after 2nd token)
|
|
||||||
("track_session.csv", 5, "track_session_L5.csv"),
|
("track_session.csv", 5, "track_session_L5.csv"),
|
||||||
# extension preserved
|
|
||||||
("a_b_1.5.csv", 2, "a_b_L2_1.5.csv"),
|
("a_b_1.5.csv", 2, "a_b_L2_1.5.csv"),
|
||||||
])
|
])
|
||||||
def test_insert_lap_token(filename, lap, expected):
|
def test_insert_lap_token(filename, lap, expected):
|
||||||
|
|
@ -24,10 +23,10 @@ def test_insert_lap_token(filename, lap, expected):
|
||||||
|
|
||||||
@pytest.mark.parametrize("stem,expected", [
|
@pytest.mark.parametrize("stem,expected", [
|
||||||
("a_L8_b", True),
|
("a_L8_b", True),
|
||||||
("a_L8_b.csv", True), # stem may include extension (function splits on '_')
|
("a_L8_b.csv", True),
|
||||||
("a_b", False),
|
("a_b", False),
|
||||||
("a_b.csv", False),
|
("a_b.csv", False),
|
||||||
("a_L_b", False), # L without digits
|
("a_L_b", False),
|
||||||
("L8", True),
|
("L8", True),
|
||||||
("notoken", False),
|
("notoken", False),
|
||||||
])
|
])
|
||||||
|
|
@ -35,6 +34,33 @@ def test_lap_token_present(stem, expected):
|
||||||
assert lap_token_present(stem) == expected
|
assert lap_token_present(stem) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# --- session_token_present ---
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("stem,expected", [
|
||||||
|
("zandvoort_P1_L7_74.074_ferrari", True),
|
||||||
|
("zandvoort_Q2_L3_ferrari", True),
|
||||||
|
("zandvoort_R_L1_ferrari", True),
|
||||||
|
("zandvoort_FP1_L5_ferrari", True),
|
||||||
|
("zandvoort_P_74.074_ferrari", False), # bare P without digit not matched
|
||||||
|
("zandvoort_L7_74.074_ferrari", False),
|
||||||
|
("notoken", False),
|
||||||
|
])
|
||||||
|
def test_session_token_present(stem, expected):
|
||||||
|
assert session_token_present(stem) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# --- insert_tokens ---
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("filename,session_type,lap,expected", [
|
||||||
|
("zandvoort_74.074_ferrari.csv", "P1", 7, "zandvoort_P1_L7_74.074_ferrari.csv"),
|
||||||
|
("zandvoort_P_74.074_ferrari.csv", "P1", 7, "zandvoort_P_P1_L7_74.074_ferrari.csv"),
|
||||||
|
("track_session.csv", "Q2", 3, "track_session_Q2_L3.csv"),
|
||||||
|
("weirdname.csv", "R", 1, "weirdname_R_L1.csv"),
|
||||||
|
])
|
||||||
|
def test_insert_tokens(filename, session_type, lap, expected):
|
||||||
|
assert insert_tokens(filename, session_type, lap) == expected
|
||||||
|
|
||||||
|
|
||||||
# --- read_lap_number ---
|
# --- read_lap_number ---
|
||||||
|
|
||||||
def test_read_lap_number_sample(sample_path):
|
def test_read_lap_number_sample(sample_path):
|
||||||
|
|
@ -45,34 +71,37 @@ def test_read_lap_number_truncated(truncated_path):
|
||||||
assert read_lap_number(truncated_path) == 9
|
assert read_lap_number(truncated_path) == 9
|
||||||
|
|
||||||
|
|
||||||
|
# --- read_session_type ---
|
||||||
|
|
||||||
|
def test_read_session_type_sample(sample_path):
|
||||||
|
assert read_session_type(sample_path) == 'P1'
|
||||||
|
|
||||||
|
|
||||||
# --- rename_unprocessed ---
|
# --- rename_unprocessed ---
|
||||||
|
|
||||||
def test_rename_unprocessed_renames(tmp_path, sample_path):
|
def test_rename_unprocessed_renames(tmp_path, sample_path):
|
||||||
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
|
src = str(tmp_path / "zandvoort_74.074_ferrari.csv")
|
||||||
shutil.copy(sample_path, src)
|
shutil.copy(sample_path, src)
|
||||||
results = rename_unprocessed([src])
|
results = rename_unprocessed([src])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
r = results[0]
|
r = results[0]
|
||||||
assert r.status == "renamed"
|
assert r.status == "renamed"
|
||||||
assert r.new is not None and "L7" in r.new
|
assert r.new is not None and "L7" in r.new and "P1" in r.new
|
||||||
assert os.path.exists(tmp_path / r.new)
|
assert os.path.exists(tmp_path / r.new)
|
||||||
|
|
||||||
|
|
||||||
def test_rename_unprocessed_skips_already_tagged(tmp_path, sample_path):
|
def test_rename_unprocessed_skips_already_tagged(tmp_path, sample_path):
|
||||||
# rename first so it has L7 in name
|
src = str(tmp_path / "zandvoort_74.074_ferrari.csv")
|
||||||
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
|
|
||||||
shutil.copy(sample_path, src)
|
shutil.copy(sample_path, src)
|
||||||
rename_unprocessed([src])
|
rename_unprocessed([src])
|
||||||
# now run on the directory — only the renamed file is present, should be skipped
|
|
||||||
results = rename_unprocessed([str(tmp_path)])
|
results = rename_unprocessed([str(tmp_path)])
|
||||||
assert all(r.status == "skipped" for r in results)
|
assert all(r.status == "skipped" for r in results)
|
||||||
|
|
||||||
|
|
||||||
def test_rename_unprocessed_error_target_exists(tmp_path, sample_path):
|
def test_rename_unprocessed_error_target_exists(tmp_path, sample_path):
|
||||||
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
|
src = str(tmp_path / "zandvoort_74.074_ferrari.csv")
|
||||||
shutil.copy(sample_path, src)
|
shutil.copy(sample_path, src)
|
||||||
# pre-create the destination so rename sees a conflict
|
dest = str(tmp_path / "zandvoort_P1_L7_74.074_ferrari.csv")
|
||||||
dest = str(tmp_path / "zandvoort_P_L7_74.074_ferrari.csv")
|
|
||||||
shutil.copy(sample_path, dest)
|
shutil.copy(sample_path, dest)
|
||||||
results = rename_unprocessed([src])
|
results = rename_unprocessed([src])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
|
|
@ -80,7 +109,7 @@ def test_rename_unprocessed_error_target_exists(tmp_path, sample_path):
|
||||||
|
|
||||||
|
|
||||||
def test_rename_unprocessed_dir_target(tmp_path, sample_path):
|
def test_rename_unprocessed_dir_target(tmp_path, sample_path):
|
||||||
src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv")
|
src = str(tmp_path / "zandvoort_74.074_ferrari.csv")
|
||||||
shutil.copy(sample_path, src)
|
shutil.copy(sample_path, src)
|
||||||
results = rename_unprocessed([str(tmp_path)])
|
results = rename_unprocessed([str(tmp_path)])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue