ria-toolkit-oss/ria_toolkit_oss_cli/ria_toolkit_oss/config.py

207 lines
5.7 KiB
Python

"""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