Source code for simcraft.utils.config

"""
Configuration loading utilities.

Supports YAML and JSON configuration files for simulation parameters.
"""

from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, Optional, Type, TypeVar
from dataclasses import dataclass, field, fields, is_dataclass

T = TypeVar("T")


[docs] class ConfigLoader: """ Load configuration from various file formats. Examples -------- >>> config = ConfigLoader.load("config.yaml") >>> print(config["simulation"]["duration"]) """
[docs] @staticmethod def load(filepath: str) -> Dict[str, Any]: """ Load configuration from file. Supports .yaml, .yml, and .json files. Parameters ---------- filepath : str Path to configuration file Returns ------- Dict[str, Any] Configuration dictionary """ path = Path(filepath) if not path.exists(): raise FileNotFoundError(f"Configuration file not found: {filepath}") suffix = path.suffix.lower() if suffix in (".yaml", ".yml"): return ConfigLoader._load_yaml(path) elif suffix == ".json": return ConfigLoader._load_json(path) else: raise ValueError(f"Unsupported config format: {suffix}")
@staticmethod def _load_yaml(path: Path) -> Dict[str, Any]: """Load YAML file.""" try: import yaml except ImportError: raise ImportError("PyYAML is required for YAML config files") with open(path, "r") as f: return yaml.safe_load(f) @staticmethod def _load_json(path: Path) -> Dict[str, Any]: """Load JSON file.""" with open(path, "r") as f: return json.load(f)
[docs] @staticmethod def save(config: Dict[str, Any], filepath: str) -> None: """ Save configuration to file. Parameters ---------- config : Dict[str, Any] Configuration dictionary filepath : str Output path """ path = Path(filepath) suffix = path.suffix.lower() if suffix in (".yaml", ".yml"): try: import yaml except ImportError: raise ImportError("PyYAML is required for YAML config files") with open(path, "w") as f: yaml.dump(config, f, default_flow_style=False) elif suffix == ".json": with open(path, "w") as f: json.dump(config, f, indent=2) else: raise ValueError(f"Unsupported config format: {suffix}")
[docs] @dataclass class YAMLConfig: """ Base class for typed configuration. Subclass this to create strongly-typed configuration classes. Examples -------- >>> @dataclass ... class SimConfig(YAMLConfig): ... duration: float = 100.0 ... warmup: float = 10.0 ... seed: int = 42 ... >>> config = SimConfig.from_file("config.yaml") >>> print(config.duration) """
[docs] @classmethod def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: """ Create instance from dictionary. Parameters ---------- data : Dict[str, Any] Configuration data Returns ------- T Configuration instance """ if not is_dataclass(cls): raise TypeError(f"{cls.__name__} must be a dataclass") # Filter to only known fields known_fields = {f.name for f in fields(cls)} filtered_data = {k: v for k, v in data.items() if k in known_fields} return cls(**filtered_data)
[docs] @classmethod def from_file(cls: Type[T], filepath: str) -> T: """ Create instance from file. Parameters ---------- filepath : str Configuration file path Returns ------- T Configuration instance """ data = ConfigLoader.load(filepath) return cls.from_dict(data)
[docs] def to_dict(self) -> Dict[str, Any]: """ Convert to dictionary. Returns ------- Dict[str, Any] Configuration dictionary """ if not is_dataclass(self): raise TypeError("Must be a dataclass") result = {} for f in fields(self): value = getattr(self, f.name) if is_dataclass(value): result[f.name] = value.to_dict() else: result[f.name] = value return result
[docs] def save(self, filepath: str) -> None: """ Save to file. Parameters ---------- filepath : str Output path """ ConfigLoader.save(self.to_dict(), filepath)
[docs] @dataclass class SimulationConfig(YAMLConfig): """ Standard simulation configuration. Attributes ---------- name : str Simulation name duration : float Total simulation time warmup : float Warmup period seed : int Random seed replications : int Number of replications time_unit : str Time unit (hours, minutes, etc.) """ name: str = "Simulation" duration: float = 100.0 warmup: float = 0.0 seed: Optional[int] = None replications: int = 1 time_unit: str = "hours" log_level: str = "WARNING" collect_trace: bool = False