"""Configuration file utilities for Utils CLI. This module provides utilities for managing the user configuration file. The core integration (actually using these configs) is TODO for the core team. """ import os from pathlib import Path from typing import Optional import yaml def get_config_path(config_path: Optional[str] = None) -> Path: """Get path to user config file. Args: config_path: Optional custom config path Returns: Path to config file """ if config_path: return Path(config_path) # Try XDG_CONFIG_HOME first (Linux standard) xdg_config = os.environ.get("XDG_CONFIG_HOME") if xdg_config: return Path(xdg_config) / "utils" / "config.yaml" # Fall back to ~/.utils/config.yaml return Path.home() / ".utils" / "config.yaml" def load_user_config(config_path: Optional[str] = None) -> Optional[dict]: """Load user configuration from file. Args: config_path: Optional custom config path Returns: Config dict if file exists, None otherwise """ path = get_config_path(config_path) if not path.exists(): return None try: with open(path, "r") as f: config = yaml.safe_load(f) return config if config else {} except yaml.YAMLError as e: raise ValueError(f"Invalid YAML in config file: {e}") except Exception as e: raise IOError(f"Error reading config file: {e}") def save_user_config(config: dict, config_path: Optional[str] = None) -> Path: """Save user configuration to file. Args: config: Configuration dictionary config_path: Optional custom config path Returns: Path where config was saved """ path = get_config_path(config_path) # Create parent directory if it doesn't exist path.parent.mkdir(parents=True, exist_ok=True) # Write config with open(path, "w") as f: f.write("# Utils SDR CLI Configuration\n") f.write("# Auto-generated by 'utils init'\n") f.write("# Edit with 'utils init' or modify this file directly\n\n") yaml.dump(config, f, default_flow_style=False, sort_keys=False) # Set secure permissions (user read/write only) try: os.chmod(path, 0o600) except Exception: pass # Best effort on Windows return path def validate_config(config: dict) -> list[str]: """Validate configuration and return list of warnings. Args: config: Configuration dictionary Returns: List of warning messages (empty if no issues) """ warnings = [] # Check for empty author if not config.get("author"): warnings.append("Author field is empty - consider setting your name") # Check for non-standard license (but allow Proprietary as valid) if "sigmf" in config and "license" in config["sigmf"]: license_id = config["sigmf"]["license"] # Common licenses (Proprietary is valid, not open source) common_licenses = [ "Proprietary", "CC0-1.0", "CC-BY-4.0", "CC-BY-SA-4.0", "MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause", ] if license_id not in common_licenses: warnings.append( f"License '{license_id}' is not a common identifier. " f"Consider: Proprietary, CC-BY-4.0, MIT, or other SPDX identifier" ) return warnings def format_config_display(config: dict) -> str: """Format configuration for display. Args: config: Configuration dictionary Returns: Formatted string """ lines = [] # Main metadata if config.get("author"): lines.append(f"Author: {config['author']}") if config.get("organization"): lines.append(f"Organization: {config['organization']}") if config.get("project"): lines.append(f"Project: {config['project']}") if config.get("location"): lines.append(f"Location: {config['location']}") if config.get("testbed"): lines.append(f"Testbed: {config['testbed']}") # SigMF metadata if "sigmf" in config: sigmf = config["sigmf"] if sigmf.get("license"): lines.append(f"License: {sigmf['license']}") if sigmf.get("hw"): lines.append(f"Hardware: {sigmf['hw']}") if sigmf.get("dataset"): lines.append(f"Dataset: {sigmf['dataset']}") return "\n".join(lines) if lines else "(empty configuration)" # TODO for core team: Integration functions # These will be implemented when wiring config into core utils logic def merge_config(user_config: dict, cli_args: dict) -> dict: """Merge configs with precedence: cli_args > user_config > defaults. TODO: Implement this when integrating with capture/convert/transmit commands. Args: user_config: User configuration from file cli_args: Arguments from CLI Returns: Merged configuration """ # Placeholder implementation merged = user_config.copy() merged.update({k: v for k, v in cli_args.items() if v is not None}) return merged def apply_config_to_metadata(metadata: dict, config: dict) -> dict: """Apply configuration defaults to recording metadata. TODO: Implement this in capture.py, convert.py when core team wires it in. Args: metadata: Existing metadata dict config: User configuration Returns: Updated metadata dict """ # Placeholder implementation updated = metadata.copy() # Add config values if not already present for key in ["author", "organization", "project", "location", "testbed"]: if key in config and key not in updated: updated[key] = config[key] return updated