207 lines
5.7 KiB
Python
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
|