""" Configuration Management for Scientific Gallery Generator This module provides dataclasses for managing configuration, including defaults for gallery generation settings. """ from dataclasses import dataclass, field from pathlib import Path from typing import List, Dict, Any, Union import yaml @dataclass class GalleryDefaults: """Default values for gallery generation.""" png_dpi: int = 400 plot_root: str = "gallery" cache_enabled: bool = True inherit_from_parent: bool = True @dataclass class GallerySource: """Represents a single data source for the gallery.""" name: str path: Union[str, Path] def __post_init__(self): """Convert path to Path object if needed.""" if isinstance(self.path, str): self.path = Path(self.path) @dataclass class GalleryConfig: """ Main configuration for gallery generation. Can be created programmatically or loaded from YAML. """ web_folder: Union[str, Path] sources: List[Union[GallerySource, Dict[str, Any]]] = field( default_factory=list ) png_dpi: int = GalleryDefaults.png_dpi plot_root: str = GalleryDefaults.plot_root cache_enabled: bool = GalleryDefaults.cache_enabled inherit_from_parent: bool = GalleryDefaults.inherit_from_parent backup_folder: str = "" def __post_init__(self): """Normalize and validate configuration.""" # Convert web_folder to Path if isinstance(self.web_folder, str): self.web_folder = Path(self.web_folder) # Convert sources to GallerySource objects if they're dicts normalized_sources = [] for source in self.sources: if isinstance(source, dict): source = GallerySource(**source) elif not isinstance(source, GallerySource): raise TypeError( f"Source must be dict or GallerySource, " f"got {type(source)}" ) normalized_sources.append(source) self.sources = normalized_sources @classmethod def from_yaml(cls, yaml_file: Union[str, Path]) -> "GalleryConfig": """ Load configuration from a YAML file. Args: yaml_file: Path to the YAML configuration file Returns: GalleryConfig instance with loaded settings Raises: FileNotFoundError: If the YAML file doesn't exist yaml.YAMLError: If the YAML file is malformed """ yaml_path = Path(yaml_file) if not yaml_path.exists(): raise FileNotFoundError(f"Config file not found: {yaml_file}") with open(yaml_path, "r") as f: data = yaml.safe_load(f) if data is None: data = {} # Extract relevant sections web_folder = data.get("paths", {}).get("web_folder") if not web_folder: raise ValueError( "web_folder must be specified in config under paths" ) gallery_cfg = data.get("gallery", {}) sources_data = data.get("sources", []) # Build sources list sources = [ {"name": s["name"], "path": s["path"]} for s in sources_data ] return cls( web_folder=web_folder, sources=sources, png_dpi=gallery_cfg.get("png_dpi", GalleryDefaults.png_dpi), plot_root=gallery_cfg.get( "plot_root", GalleryDefaults.plot_root ), cache_enabled=data.get("metadata", {}).get( "cache_enabled", GalleryDefaults.cache_enabled ), inherit_from_parent=data.get("metadata", {}).get( "inherit_from_parent", GalleryDefaults.inherit_from_parent ), backup_folder=gallery_cfg.get("backup_folder", ""), ) def to_yaml(self, yaml_file: Union[str, Path]) -> None: """ Save the current configuration to a YAML file. Args: yaml_file: Path where to save the YAML configuration Raises: IOError: If unable to write to the specified file """ yaml_path = Path(yaml_file) data = { "paths": { "work_dir": str(Path.cwd()), "web_folder": str(self.web_folder), }, "gallery": { "plot_root": self.plot_root, "png_dpi": self.png_dpi, "backup_folder": self.backup_folder, }, "ui": { "max_recent_plots": 20, "search_debounce_ms": 300, }, "metadata": { "cache_enabled": self.cache_enabled, "inherit_from_parent": self.inherit_from_parent, "supported_formats": [".yaml", ".yml", ".json"], }, "sources": [ {"name": s.name, "path": str(s.path)} for s in self.sources ], } with open(yaml_path, "w") as f: yaml.dump(data, f, default_flow_style=False)