Buckets:
| """ | |
| Session Laptimes Merger | |
| ======================= | |
| Combines per-driver laptimes.json files into a single session_laptimes.json | |
| at the session level. | |
| Input: | |
| {event_name}/{session_name}/{driver}/laptimes.json (one per driver) | |
| Output: | |
| {event_name}/{session_name}/session_laptimes.json | |
| All driver arrays are concatenated in alphabetical driver order. | |
| The existing `drv` field in each file identifies which rows belong to which driver. | |
| If session_laptimes.json already exists it is overwritten. | |
| """ | |
| import logging | |
| import os | |
| import sys | |
| from typing import Any, Dict, List, Optional | |
| import orjson | |
| # --------------------------------------------------------------------------- | |
| # Configuration — mirror the style of LapTimes.py | |
| # --------------------------------------------------------------------------- | |
| DEFAULT_YEAR = 2018 | |
| # Root directory where event folders live. | |
| # "." means the directory you invoke the script from. | |
| # Override this if your data lives elsewhere, e.g. BASE_DIR = "/data/f1/2024" | |
| BASE_DIR = "." | |
| TARGET_EVENT_NAMES_LIST = [ | |
| # 'Bahrain Grand Prix', | |
| # 'Chinese Grand Prix', | |
| # 'Azerbaijan Grand Prix', | |
| # 'Spanish Grand Prix', | |
| # 'Monaco Grand Prix', | |
| # 'Canadian Grand Prix', | |
| # 'French Grand Prix', | |
| # 'Austrian Grand Prix', | |
| # 'British Grand Prix', | |
| # 'German Grand Prix', 'Hungarian Grand Prix', | |
| # 'Belgian Grand Prix', | |
| 'Italian Grand Prix', | |
| # 'Singapore Grand Prix', | |
| # 'Mexican Grand Prix', | |
| # "Australian Grand Prix", | |
| # 'Brazilian Grand Prix', 'Abu Dhabi Grand Prix', | |
| # 'Russian Grand Prix', 'Japanese Grand Prix', 'United States Grand Prix', | |
| ] | |
| TARGET_EVENT_NAMES = [e.strip() for e in TARGET_EVENT_NAMES_LIST if e.strip()] | |
| if not TARGET_EVENT_NAMES: | |
| raise ValueError("Set at least one active event in TARGET_EVENT_NAMES_LIST.") | |
| AVAILABLE_SESSIONS = [ | |
| "Practice 1", | |
| "Practice 2", | |
| "Practice 3", | |
| "Qualifying", | |
| # "Sprint Qualifying", | |
| # "Sprint Shootout", | |
| # "Sprint", | |
| "Race", | |
| ] | |
| TARGET_SESSIONS = [ | |
| # "Practice 1", | |
| # "Practice 2", | |
| # "Practice 3", | |
| # "Qualifying", | |
| # "Sprint Qualifying", | |
| # "Sprint Shootout", | |
| # "Sprint", | |
| "Race", | |
| ] | |
| invalid_target_sessions = sorted(set(TARGET_SESSIONS) - set(AVAILABLE_SESSIONS)) | |
| if invalid_target_sessions: | |
| raise ValueError( | |
| "Invalid TARGET_SESSIONS value(s): " + ", ".join(invalid_target_sessions) | |
| ) | |
| ORJSON_OPTS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS | |
| # --------------------------------------------------------------------------- | |
| # Logging | |
| # --------------------------------------------------------------------------- | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| handlers=[ | |
| logging.FileHandler("merge_laptimes.log"), | |
| logging.StreamHandler(), | |
| ], | |
| ) | |
| logger = logging.getLogger("merge_laptimes") | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| def _read_json(path: str) -> Dict[str, Any]: | |
| with open(path, "rb") as f: | |
| return orjson.loads(f.read()) | |
| def _write_json(path: str, obj: Any) -> None: | |
| with open(path, "wb") as f: | |
| f.write(orjson.dumps(obj, option=ORJSON_OPTS)) | |
| def _driver_lap_count(data: Dict[str, List]) -> int: | |
| """Return the length of the first non-empty array in a driver's data dict.""" | |
| for v in data.values(): | |
| if isinstance(v, list) and len(v) > 0: | |
| return len(v) | |
| return 0 | |
| def _discover_drivers(session_dir: str) -> List[str]: | |
| """ | |
| Return alphabetically sorted list of driver abbreviations that have a | |
| laptimes.json inside session_dir. | |
| """ | |
| drivers = [] | |
| try: | |
| entries = os.listdir(session_dir) | |
| except FileNotFoundError: | |
| return drivers | |
| for entry in sorted(entries): | |
| driver_dir = os.path.join(session_dir, entry) | |
| laptimes_path = os.path.join(driver_dir, "laptimes.json") | |
| if os.path.isdir(driver_dir) and os.path.isfile(laptimes_path): | |
| drivers.append(entry) | |
| return drivers # already sorted because we sorted entries above | |
| # --------------------------------------------------------------------------- | |
| # Core merge logic | |
| # --------------------------------------------------------------------------- | |
| def merge_session(event_name: str, session_name: str) -> bool: | |
| """ | |
| Merge all driver laptimes.json files for one session into | |
| session_laptimes.json. | |
| Returns True on success, False on failure (after logging the error). | |
| Raises SystemExit on unrecoverable errors (missing drivers, bad files). | |
| """ | |
| label = f"{event_name} - {session_name}" | |
| session_dir = os.path.join(BASE_DIR, event_name, session_name) | |
| if not os.path.isdir(session_dir): | |
| logger.warning("Session directory not found, skipping: %s", session_dir) | |
| return True | |
| drivers = _discover_drivers(session_dir) | |
| if not drivers: | |
| logger.warning( | |
| "No driver directories with laptimes.json found in %s — skipping", session_dir | |
| ) | |
| return True | |
| logger.info( | |
| "%s: found %d driver(s): %s", label, len(drivers), ", ".join(drivers) | |
| ) | |
| # ------------------------------------------------------------------ | |
| # Load every driver's file — fail the whole session on any problem. | |
| # ------------------------------------------------------------------ | |
| driver_data: Dict[str, Dict[str, List]] = {} | |
| for driver in drivers: | |
| path = os.path.join(session_dir, driver, "laptimes.json") | |
| try: | |
| data = _read_json(path) | |
| except FileNotFoundError: | |
| logger.error("Missing laptimes.json for driver %s in %s", driver, label) | |
| return False | |
| except Exception as exc: | |
| logger.error( | |
| "Failed to read laptimes.json for driver %s in %s: %s", | |
| driver, | |
| label, | |
| exc, | |
| ) | |
| return False | |
| if not isinstance(data, dict): | |
| logger.error( | |
| "laptimes.json for driver %s in %s is not a JSON object", driver, label | |
| ) | |
| return False | |
| lap_count = _driver_lap_count(data) | |
| if lap_count == 0: | |
| logger.warning( | |
| "laptimes.json for driver %s in %s contains no lap rows — skipping driver", | |
| driver, | |
| label, | |
| ) | |
| continue | |
| driver_data[driver] = data | |
| logger.info(" Loaded %s: %d lap(s)", driver, lap_count) | |
| # ------------------------------------------------------------------ | |
| # Build the union of all keys across every driver. | |
| # ------------------------------------------------------------------ | |
| all_keys: List[str] = [] | |
| seen: set = set() | |
| # Preserve a stable key order: use the first driver's key order as the | |
| # base, then append any extra keys seen in subsequent drivers. | |
| for driver in drivers: | |
| if driver not in driver_data: | |
| continue | |
| for key in driver_data[driver]: | |
| if key not in seen: | |
| all_keys.append(key) | |
| seen.add(key) | |
| # ------------------------------------------------------------------ | |
| # Concatenate arrays in alphabetical driver order. | |
| # Keys missing for a driver are filled with "None" * that driver's lap count. | |
| # ------------------------------------------------------------------ | |
| merged: Dict[str, List] = {key: [] for key in all_keys} | |
| for driver in drivers: | |
| if driver not in driver_data: | |
| continue | |
| data = driver_data[driver] | |
| lap_count = _driver_lap_count(data) | |
| for key in all_keys: | |
| if key in data: | |
| values = data[key] | |
| if not isinstance(values, list): | |
| logger.error( | |
| "Key '%s' for driver %s in %s is not an array", | |
| key, | |
| driver, | |
| label, | |
| ) | |
| return False | |
| merged[key].extend(values) | |
| else: | |
| # Key present in another driver's file but not this one. | |
| logger.warning( | |
| "Key '%s' missing for driver %s in %s — filling with %d None(s)", | |
| key, | |
| driver, | |
| label, | |
| lap_count, | |
| ) | |
| merged[key].extend(["None"] * lap_count) | |
| # ------------------------------------------------------------------ | |
| # Sanity-check: every key must have the same total length. | |
| # ------------------------------------------------------------------ | |
| lengths = {key: len(arr) for key, arr in merged.items()} | |
| unique_lengths = set(lengths.values()) | |
| if len(unique_lengths) > 1: | |
| bad = {k: v for k, v in lengths.items() if v != max(unique_lengths)} | |
| logger.error( | |
| "Array length mismatch after merge in %s. Mismatched keys: %s", | |
| label, | |
| bad, | |
| ) | |
| return False | |
| # ------------------------------------------------------------------ | |
| # Write output. | |
| # ------------------------------------------------------------------ | |
| out_path = os.path.join(session_dir, "session_laptimes.json") | |
| try: | |
| _write_json(out_path, merged) | |
| except Exception as exc: | |
| logger.error("Failed to write %s: %s", out_path, exc) | |
| return False | |
| total_laps = max(lengths.values()) if lengths else 0 | |
| logger.info( | |
| "%s: wrote %s (%d drivers, %d total laps)", | |
| label, | |
| out_path, | |
| len(drivers), | |
| total_laps, | |
| ) | |
| return True | |
| # --------------------------------------------------------------------------- | |
| # Entry point | |
| # --------------------------------------------------------------------------- | |
| def main() -> None: | |
| overall_success = True | |
| for event_name in TARGET_EVENT_NAMES: | |
| for session_name in TARGET_SESSIONS: | |
| label = f"{event_name} - {session_name}" | |
| logger.info("=== Merging %s ===", label) | |
| success = merge_session(event_name, session_name) | |
| if not success: | |
| logger.error("Merge FAILED for %s", label) | |
| overall_success = False | |
| if not overall_success: | |
| logger.error("One or more sessions failed to merge.") | |
| sys.exit(1) | |
| logger.info("All sessions merged successfully.") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 11 kB
- Xet hash:
- be973b2efc0f7565117708b2872acc4857016a55bc3fa2b5938fdb39359840d5
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.