# target_simulator/utils/config_manager.py """ Manages loading and saving of application settings and scenarios to a single JSON file. """ import json import os import sys from typing import Dict, Any, Optional, List from enum import Enum class EnumEncoder(json.JSONEncoder): """A custom JSON encoder to handle Enum objects by encoding them as their values.""" def default(self, o: Any) -> Any: if isinstance(o, Enum): return o.value return super().default(o) class ConfigManager: def save_connection_settings(self, config: Dict[str, Any]): """ Save both target and lru connection configs in the settings file. """ if "general" not in self._settings: self._settings["general"] = {} self._settings["general"]["connection"] = config self._save_settings() def get_connection_settings(self) -> Dict[str, Any]: """ Returns the connection settings from the 'general' section. """ general = self._settings.get("general", {}) return general.get("connection", {}) """Handles reading and writing application settings and scenarios from a JSON file.""" def __init__(self, filename: str = "settings.json", scenarios_filename: str = "scenarios.json"): """ Initializes the ConfigManager. Args: filename: The name of the settings file. It will be stored in the project root directory. """ if getattr(sys, "frozen", False): application_path = os.path.dirname(sys.executable) else: application_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "..") ) self.filepath = os.path.join(application_path, filename) self.scenarios_filepath = os.path.join(application_path, scenarios_filename) self._settings = self._load_or_initialize_settings() # Load scenarios from separate file if present, otherwise keep any scenarios # found inside settings.json (fallback). self._scenarios = self._load_or_initialize_scenarios() # Apply debug overrides (if present) into the global DEBUG_CONFIG so # runtime helpers (e.g., csv_logger) pick up user-configured values. try: from target_simulator.config import DEBUG_CONFIG debug_block = self._settings.get("debug", {}) if isinstance(self._settings, dict) else {} if isinstance(debug_block, dict): for k, v in debug_block.items(): DEBUG_CONFIG[k] = v except Exception: # If anything goes wrong here, we don't want to fail initialization. pass def _load_or_initialize_settings(self) -> Dict[str, Any]: """Loads settings from the JSON file or initializes with a default structure.""" # If someone calls this at runtime and scenarios were previously loaded # from another location, clear them so the caller can decide how to # reload scenarios (this supports tests that swap filepath at runtime). if hasattr(self, "_scenarios"): self._scenarios = {} if not os.path.exists(self.filepath): return {"general": {}, "scenarios": {}} try: with open(self.filepath, "r", encoding="utf-8") as f: settings = json.load(f) if not isinstance(settings, dict) or "general" not in settings: # If file contained only general settings (old style), wrap it return {"general": settings, "scenarios": {}} # Keep scenarios key if present; actual source of truth for scenarios # will be the separate scenarios file when available. return settings except (json.JSONDecodeError, IOError): return {"general": {}, "scenarios": {}} def _save_settings(self): """Saves the current settings to the JSON file.""" try: with open(self.filepath, "w", encoding="utf-8") as f: # Write all settings except 'scenarios' (which is persisted # separately). This preserves custom top-level keys while # ensuring scenarios are stored in their dedicated file. to_write = dict(self._settings) if "scenarios" in to_write: # don't duplicate scenarios in settings.json del to_write["scenarios"] json.dump(to_write, f, indent=4, cls=EnumEncoder) except IOError as e: print(f"Error saving settings to {self.filepath}: {e}") def _load_or_initialize_scenarios(self) -> Dict[str, Any]: """Loads scenarios from the separate scenarios file if present. Falls back to scenarios inside settings.json when scenarios.json is absent. """ # If scenarios file exists, load from it. if os.path.exists(self.scenarios_filepath): try: with open(self.scenarios_filepath, "r", encoding="utf-8") as f: scenarios = json.load(f) if isinstance(scenarios, dict): return scenarios except (json.JSONDecodeError, IOError): return {} # Fallback: try to read scenarios stored inside settings.json try: with open(self.filepath, "r", encoding="utf-8") as f: settings = json.load(f) if isinstance(settings, dict) and "scenarios" in settings: return settings.get("scenarios", {}) except Exception: pass return {} def _save_scenarios(self): """Saves scenarios to the separate scenarios file.""" try: with open(self.scenarios_filepath, "w", encoding="utf-8") as f: json.dump(self._scenarios, f, indent=4, cls=EnumEncoder) except IOError as e: print(f"Error saving scenarios to {self.scenarios_filepath}: {e}") def get_general_settings(self) -> Dict[str, Any]: """Returns the general settings.""" return self._settings.get("general", {}) def save_general_settings(self, data: Dict[str, Any]): """Saves the general settings.""" self._settings["general"] = data self._save_settings() def get_scenario_names(self) -> List[str]: """Returns a list of all scenario names.""" return list(self._scenarios.keys()) def get_scenario(self, name: str) -> Optional[Dict[str, Any]]: """ Retrieves a specific scenario by name. Args: name: The name of the scenario to retrieve. Returns: A dictionary with the scenario data, or None if not found. """ return self._scenarios.get(name) def save_scenario(self, name: str, data: Dict[str, Any]): """ Saves or updates a scenario. Args: name: The name of the scenario to save. data: The dictionary of scenario data to save. """ self._scenarios[name] = data self._save_scenarios() def delete_scenario(self, name: str): """ Deletes a scenario by name. Args: name: The name of the scenario to delete. """ if name in self._scenarios: del self._scenarios[name] self._save_scenarios()