From c7e1b88999e9700385d16e75ee6647e4c0c424eb Mon Sep 17 00:00:00 2001 From: Andrew Pimenov Date: Sun, 14 Jun 2026 18:27:33 +0300 Subject: [PATCH] session type in rename, race position column, races/ folder structure --- telemetry/__main__.py | 4 +-- telemetry/gui.py | 14 ++++----- telemetry/rename.py | 45 ++++++++++++++++++++++++++-- telemetry/report_race.py | 11 ++++++- tests/test_rename.py | 63 +++++++++++++++++++++++++++++----------- 5 files changed, 105 insertions(+), 32 deletions(-) diff --git a/telemetry/__main__.py b/telemetry/__main__.py index 57c4554..dd6b13d 100644 --- a/telemetry/__main__.py +++ b/telemetry/__main__.py @@ -103,9 +103,7 @@ def main() -> None: sys.exit(2) laps = [load_lap(os.path.join(race_dir, f)) for f in csv_files] dirname = os.path.basename(race_dir.rstrip('/\\')) - out = os.path.join( - os.path.dirname(os.path.dirname(race_dir)), 'reports', dirname + '_race.md' - ) + out = os.path.join(race_dir, 'reports', dirname + '_race.md') _print_result(*report_race.generate(laps, out, lang)) diff --git a/telemetry/gui.py b/telemetry/gui.py index c4b25a6..ca30e4d 100644 --- a/telemetry/gui.py +++ b/telemetry/gui.py @@ -11,12 +11,12 @@ from .rename import rename_unprocessed # project root = parent of the tool\ dir _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: 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: @@ -68,7 +68,7 @@ class _App: self._btn_rename = ttk.Button(btn_frame, text='Rename unprocessed (add lap #)', command=self._on_rename) 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) log_frame = ttk.LabelFrame(self._root, text='Log') @@ -192,7 +192,7 @@ class _App: self._log_queue.put(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: return self._set_busy(True) @@ -216,10 +216,8 @@ class _App: self._log_queue.put(None) def _open_reports(self) -> None: - if not os.path.isdir(_REPORTS_DIR): - self._log_append(f'Reports folder does not exist yet: {_REPORTS_DIR}') - return - os.startfile(_REPORTS_DIR) + target = _RACES_DIR if os.path.isdir(_RACES_DIR) else _PROJECT_ROOT + os.startfile(target) def main() -> None: diff --git a/telemetry/rename.py b/telemetry/rename.py index 99f28c6..0f7c28d 100644 --- a/telemetry/rename.py +++ b/telemetry/rename.py @@ -4,11 +4,37 @@ from dataclasses import dataclass 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: 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: base, ext = os.path.splitext(filename) tokens = base.split('_') @@ -33,6 +59,15 @@ def read_lap_number(path: str) -> int: 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) class RenameResult: old: str @@ -62,15 +97,19 @@ def rename_unprocessed(targets: list[str]) -> list[RenameResult]: results.append(RenameResult(name, None, 'error', 'not a CSV file')) continue stem = os.path.splitext(name)[0] - if lap_token_present(stem): - results.append(RenameResult(name, None, 'skipped', 'already has lap number')) + if _is_processed(stem): + results.append(RenameResult(name, None, 'skipped', 'already processed')) continue try: lap = read_lap_number(p) + session_type = read_session_type(p) except Exception as e: results.append(RenameResult(name, None, 'error', str(e))) 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) if os.path.exists(dest): results.append(RenameResult(name, new_name, 'error', 'target exists')) diff --git a/telemetry/report_race.py b/telemetry/report_race.py index a5b24a1..12adfad 100644 --- a/telemetry/report_race.py +++ b/telemetry/report_race.py @@ -78,6 +78,14 @@ def _header(laps: list[Lap]) -> str: 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: # find fastest valid lap time best_time = None @@ -110,6 +118,7 @@ def _lap_times_table(laps: list[Lap], stints: list[int], ages: list[int]) -> str rows.append([ lap_n, + _race_pos(lap), _fmt_laptime(lt) if lt > 0 else '—', _fmt_sector(s1) if s1 > 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( - ['Lap', 'Time', 'S1', 'S2', 'S3', 'Tyre', 'Age', 'Fuel kg', 'Note'], + ['Lap', 'Pos', 'Time', 'S1', 'S2', 'S3', 'Tyre', 'Age', 'Fuel kg', 'Note'], rows, ) diff --git a/tests/test_rename.py b/tests/test_rename.py index fa5a680..97c750f 100644 --- a/tests/test_rename.py +++ b/tests/test_rename.py @@ -1,19 +1,18 @@ import os import shutil 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", [ - # float token found → insert before it ("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"), - # two non-float tokens, no float → insert at index 2 (after 2nd token) ("track_session.csv", 5, "track_session_L5.csv"), - # extension preserved ("a_b_1.5.csv", 2, "a_b_L2_1.5.csv"), ]) 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", [ ("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.csv", False), - ("a_L_b", False), # L without digits + ("a_L_b", False), ("L8", True), ("notoken", False), ]) @@ -35,6 +34,33 @@ def test_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 --- 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 +# --- read_session_type --- + +def test_read_session_type_sample(sample_path): + assert read_session_type(sample_path) == 'P1' + + # --- rename_unprocessed --- 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) results = rename_unprocessed([src]) assert len(results) == 1 r = results[0] 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) def test_rename_unprocessed_skips_already_tagged(tmp_path, sample_path): - # rename first so it has L7 in name - src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv") + src = str(tmp_path / "zandvoort_74.074_ferrari.csv") shutil.copy(sample_path, src) rename_unprocessed([src]) - # now run on the directory — only the renamed file is present, should be skipped results = rename_unprocessed([str(tmp_path)]) assert all(r.status == "skipped" for r in results) 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) - # pre-create the destination so rename sees a conflict - dest = str(tmp_path / "zandvoort_P_L7_74.074_ferrari.csv") + dest = str(tmp_path / "zandvoort_P1_L7_74.074_ferrari.csv") shutil.copy(sample_path, dest) results = rename_unprocessed([src]) 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): - src = str(tmp_path / "zandvoort_P_74.074_ferrari.csv") + src = str(tmp_path / "zandvoort_74.074_ferrari.csv") shutil.copy(sample_path, src) results = rename_unprocessed([str(tmp_path)]) assert len(results) == 1