import copy
import os
from pathlib import Path
from typing import Optional
import click
import yaml
from uac_cli.utils.config.schema import CONFIG_DEFAULTS, validate
_PROFILES_FILENAME = "profiles.yml"
config_path = Path.home() / ".uac" / "config.yml"
profiles_path = Path.home() / ".uac" / _PROFILES_FILENAME
[docs]def _ensure_uac_dir():
d = Path.home() / ".uac"
if not os.path.exists(d):
os.mkdir(d)
[docs]def _load_yaml(path: Path, label: str) -> Optional[dict]:
"""Read and parse a YAML file, raising a ClickException on parse errors."""
try:
with open(path, "r", encoding="UTF-8") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
except yaml.YAMLError as e:
raise click.ClickException(f"Invalid {label}: {e}")
[docs]def read_config_file() -> Optional[dict]:
"""Read ~/.uac/config.yml, returning the full document or None."""
if not os.path.exists(config_path):
return None
cfg = _load_yaml(config_path, "config.yml")
if cfg is not None:
validate(cfg)
return cfg
[docs]def write_config_file(cfg: dict, silent: bool = False) -> None:
"""Write the full config document to ~/.uac/config.yml."""
_ensure_uac_dir()
with open(config_path, "w", encoding="UTF-8") as f:
yaml.dump(cfg, f, sort_keys=False)
os.chmod(config_path, 0o600)
if not silent:
click.echo(f"Config file written. (Path: {config_path})")
[docs]def migrate_profiles_to_config() -> None:
"""One-time migration: profiles.yml → config.yml. Leaves profiles.yml intact."""
if os.path.exists(config_path):
return
if not os.path.exists(profiles_path):
return
click.echo(f"Migrating {profiles_path} → {config_path}", err=True)
old = _load_yaml(profiles_path, _PROFILES_FILENAME) or {}
profiles = {name: data for name, data in old.items() if isinstance(data, dict)}
cfg = copy.deepcopy(CONFIG_DEFAULTS)
cfg["profiles"] = profiles
# Write silently — migration already printed its own notice
write_config_file(cfg, silent=True)
[docs]def read_config() -> Optional[dict]:
"""Return the profiles dict, or None if no config exists or profiles is empty."""
cfg = read_config_file()
if cfg is not None:
return cfg.get("profiles") or None
if not os.path.exists(profiles_path):
return None
return _load_yaml(profiles_path, _PROFILES_FILENAME)
[docs]def read_profile(profile_name: str) -> Optional[dict]:
"""Return a single profile. Prefers config.yml, falls back to profiles.yml."""
cfg = read_config_file()
if cfg is not None:
return (cfg.get("profiles") or {}).get(profile_name)
if not os.path.exists(profiles_path):
return None
data = _load_yaml(profiles_path, _PROFILES_FILENAME)
return data.get(profile_name) if data else None
[docs]def write_profile(profile_name: str, profile: dict) -> None:
"""Write a profile into config.yml, creating the file with defaults if needed."""
cfg = read_config_file()
if cfg is None:
cfg = copy.deepcopy(CONFIG_DEFAULTS)
if "profiles" not in cfg:
cfg["profiles"] = {}
cfg["profiles"][profile_name] = profile
write_config_file(cfg)
[docs]def get_view_columns(command: tuple) -> Optional[list]:
"""Look up output_options.table_columns for the command tuple and return the column list, or None."""
cfg = read_config_file()
if cfg is None:
return None
table_columns = (cfg.get("output_options") or {}).get("table_columns") or {}
command_key = " ".join(command)
entry = table_columns.get(command_key)
return entry if isinstance(entry, list) and entry else None
[docs]def get_default_view() -> str:
"""Return 'json' or 'table', defaulting to 'json' when not configured."""
cfg = read_config_file()
if cfg is None:
return "json"
return (cfg.get("output_options") or {}).get("default_stdout_format", "json")
[docs]def get_table_settings() -> dict:
"""Return table settings merged with hardcoded defaults, with validation."""
cfg = read_config_file()
overrides = {}
if cfg is not None:
overrides = dict((cfg.get("output_options") or {}).get("table_settings") or {})
mcw = overrides.get("max_column_width")
if not isinstance(mcw, int) or mcw < 1:
overrides.pop("max_column_width", None)
defaults = CONFIG_DEFAULTS["output_options"]["table_settings"]
return {**defaults, **overrides}
[docs]def ask_profile(profile):
url = click.prompt("Please enter UAC URL", type=str, default=profile.get("url", ""))
cfg = {"url": url}
token = click.prompt(
"Please enter personal access token",
type=str,
default=profile.get("token", ""),
)
cfg["token"] = token
return cfg