"""Helpers for writing runtime configs and preprocessing bundles.""" from __future__ import annotations import json import os from pathlib import Path from python_app.models.run_config_model import RunConfigModel, parse_combos_from_text from python_app.orchestration.preprocess_assets import ( PREPROCESS_ASSET_KEYS, PREPROCESS_ASSET_SPECS, preprocess_asset_model, runtime_preprocess_asset_keys, ) from python_app.storage.npz_store import NpzStore class ConfigWriter: """Write runtime artifacts consumed by C++ processes.""" def __init__(self, runtime_dir: Path) -> None: """Create writer rooted at runtime directory.""" self._runtime_dir = runtime_dir self._runtime_dir.mkdir(parents=True, exist_ok=True) def prepare_preprocess_bundles( self, store: NpzStore, radar_key: str, config: RunConfigModel, ) -> None: """Export selected preprocess sets into runtime bundles and update config paths.""" for key in PREPROCESS_ASSET_KEYS: preprocess_asset_model(config, key).bundle_path = "" for key in runtime_preprocess_asset_keys(config): spec = PREPROCESS_ASSET_SPECS[key] asset = preprocess_asset_model(config, key) bundle_path = self._runtime_dir / spec.runtime_filename store.export_set_bundle(spec.set_kind, radar_key, asset.set_name, bundle_path) asset.bundle_path = str(bundle_path) def write(self, config: RunConfigModel, output_path: Path) -> Path: """Atomically write run configuration JSON file. Mirrors ProcessingLiveConfigWriter: dump to a sibling .tmp, flush+fsync to durably commit the bytes, then os.replace() onto the destination. The replace is atomic, so a C++ consumer can never observe a half-written config (which would abort it with an opaque JSON parse error), even across a crash or power loss mid-write on the SD-card-backed Pi. """ output_path.parent.mkdir(parents=True, exist_ok=True) # allow_nan=False: a stray NaN/Infinity must fail loudly here in Python # rather than serialize to a non-standard token that aborts every C++ # consumer at startup with an opaque JSON parse error. serialized = json.dumps(config.to_dict(), indent=2, allow_nan=False) tmp_path = output_path.with_suffix(output_path.suffix + ".tmp") with open(tmp_path, "w", encoding="utf-8") as handle: handle.write(serialized) handle.flush() os.fsync(handle.fileno()) os.replace(tmp_path, output_path) return output_path __all__ = ["ConfigWriter", "parse_combos_from_text"]