Source code for uac_cli.utils.config

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