""" Preset pack manager for loading, validating, and managing preset packs. """ import logging import os from typing import Dict, List, Optional import tomli from sideline.preset_packs.pack_format import PresetPack, PresetPackMetadata from sideline.preset_packs.encoder import PresetPackEncoder from sideline.plugins.compatibility import CompatibilityManager logger = logging.getLogger(__name__) class PresetPackManager: """Manages preset pack loading and validation.""" def __init__(self, pack_dir: Optional[str] = None): """Initialize preset pack manager. Args: pack_dir: Directory to search for preset packs """ self.pack_dir = pack_dir or os.path.expanduser("~/.config/sideline/packs") self._packs: Dict[str, PresetPack] = {} def load_pack(self, pack_path: str) -> Optional[PresetPack]: """Load a preset pack from a file. Args: pack_path: Path to the preset pack file (.tpack or .toml) Returns: Loaded PresetPack or None if failed """ try: with open(pack_path, "rb") as f: # Try loading as TOML first if pack_path.endswith(".toml"): data = tomli.load(f) pack = PresetPack.from_dict(data) elif pack_path.endswith(".tpack"): # Load ASCII art encoded pack content = f.read().decode("utf-8") pack = self._load_ascii_pack(content) else: logger.error(f"Unknown file format: {pack_path}") return None # Validate compatibility if not CompatibilityManager.validate_compatibility( pack.metadata.sideline_version ): error = CompatibilityManager.get_compatibility_error( pack.metadata.sideline_version ) logger.warning(f"Pack {pack.metadata.name} incompatible: {error}") return None # Store pack self._packs[pack.metadata.name] = pack logger.info( f"Loaded preset pack: {pack.metadata.name} v{pack.metadata.version}" ) return pack except Exception as e: logger.error(f"Failed to load preset pack {pack_path}: {e}") return None def _load_ascii_pack(self, content: str) -> PresetPack: """Load pack from ASCII art encoded content.""" # Extract TOML from ASCII art toml_data = PresetPackEncoder.decode_toml(content) # Parse TOML import tomli data = tomli.loads(toml_data) return PresetPack.from_dict(data) def load_directory(self, directory: Optional[str] = None) -> List[PresetPack]: """Load all preset packs from a directory. Args: directory: Directory to search (defaults to pack_dir) Returns: List of loaded PresetPack objects """ directory = directory or self.pack_dir if not os.path.exists(directory): logger.warning(f"Preset pack directory does not exist: {directory}") return [] loaded = [] for filename in os.listdir(directory): if filename.endswith((".toml", ".tpack")): path = os.path.join(directory, filename) pack = self.load_pack(path) if pack: loaded.append(pack) return loaded def save_pack( self, pack: PresetPack, output_path: str, format: str = "toml" ) -> bool: """Save a preset pack to a file. Args: pack: PresetPack to save output_path: Path to save the pack format: Output format ("toml" or "tpack") Returns: True if successful, False otherwise """ try: if format == "toml": import tomli_w with open(output_path, "w") as f: tomli_w.dump(pack.to_dict(), f) elif format == "tpack": # Encode as ASCII art toml_data = self._pack_to_toml(pack) ascii_art = PresetPackEncoder.encode_toml(toml_data, pack.metadata.name) with open(output_path, "w") as f: f.write(ascii_art) else: logger.error(f"Unknown format: {format}") return False logger.info(f"Saved preset pack: {output_path}") return True except Exception as e: logger.error(f"Failed to save preset pack: {e}") return False def _pack_to_toml(self, pack: PresetPack) -> str: """Convert PresetPack to TOML string.""" import tomli_w return tomli_w.dumps(pack.to_dict()) def get_pack(self, name: str) -> Optional[PresetPack]: """Get a loaded preset pack by name.""" return self._packs.get(name) def list_packs(self) -> List[str]: """List all loaded preset pack names.""" return list(self._packs.keys()) def register_pack_plugins(self, pack: PresetPack): """Register all plugins from a preset pack. Args: pack: PresetPack containing plugins """ from sideline.pipeline import StageRegistry for plugin_entry in pack.plugins: try: # Decode plugin code code = PresetPackEncoder.decode_plugin_code(plugin_entry.encoded_code) # Execute plugin code to get the class local_ns = {} exec(code, local_ns) # Find the plugin class (first class defined) plugin_class = None for obj in local_ns.values(): if isinstance(obj, type) and hasattr(obj, "metadata"): plugin_class = obj break if plugin_class: # Register the plugin StageRegistry.register(plugin_entry.category, plugin_class) logger.info(f"Registered plugin: {plugin_entry.name}") else: logger.warning(f"No plugin class found in {plugin_entry.name}") except Exception as e: logger.error(f"Failed to register plugin {plugin_entry.name}: {e}")