- config.Config 仅支持 ~/.mytoolkit/config.json 分层键 - 删除旧 env.json/module-config.json 迁移逻辑与 flat-key 别名 - ssh/bib/image 命令改用 connections.* / paths.* / secrets.api_keys.* - 单测覆盖分层读写、嵌套删除、export、resolve_key、权限
134 lines
4.0 KiB
Python
134 lines
4.0 KiB
Python
"""Configuration management for mytoolkit.
|
|
|
|
Storage: ~/.mytoolkit/config.json
|
|
Schema: hierarchical dict, e.g. ``secrets.api_keys.ark``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
_HOME = Path(os.environ.get("MYTOOLKIT_HOME", Path.home() / ".mytoolkit"))
|
|
CONFIG_PATH = _HOME / "config.json"
|
|
|
|
|
|
def _get_path(data: dict, path: str) -> Any:
|
|
"""Get a value from nested dict using dot-separated path."""
|
|
parts = path.split(".")
|
|
d: Any = data
|
|
for part in parts:
|
|
if not isinstance(d, dict) or part not in d:
|
|
return None
|
|
d = d[part]
|
|
return d
|
|
|
|
|
|
def _set_path(data: dict, path: str, value: Any) -> None:
|
|
"""Set a value in nested dict using dot-separated path."""
|
|
parts = path.split(".")
|
|
for part in parts[:-1]:
|
|
data = data.setdefault(part, {})
|
|
data[parts[-1]] = value
|
|
|
|
|
|
def _remove_path(data: dict, path: str) -> bool:
|
|
"""Remove a leaf value from nested dict using dot-separated path.
|
|
|
|
Returns ``True`` if the leaf existed and was removed.
|
|
"""
|
|
parts = path.split(".")
|
|
d: Any = data
|
|
for part in parts[:-1]:
|
|
if not isinstance(d, dict) or part not in d:
|
|
return False
|
|
d = d[part]
|
|
if isinstance(d, dict) and parts[-1] in d:
|
|
del d[parts[-1]]
|
|
return True
|
|
return False
|
|
|
|
|
|
def _walk(data: Any, prefix: str = "") -> dict[str, Any]:
|
|
"""Return a flat dict of dot-path -> value for nested data."""
|
|
flat: dict[str, Any] = {}
|
|
if isinstance(data, dict):
|
|
for k, v in data.items():
|
|
key = f"{prefix}.{k}" if prefix else k
|
|
if isinstance(v, dict):
|
|
flat.update(_walk(v, key))
|
|
else:
|
|
flat[key] = v
|
|
return flat
|
|
|
|
|
|
class Config:
|
|
"""Hierarchical config manager for ``~/.mytoolkit/config.json``."""
|
|
|
|
def __init__(self) -> None:
|
|
self._config_path = CONFIG_PATH
|
|
self._data = self._load()
|
|
|
|
def _load(self) -> dict:
|
|
if self._config_path.exists():
|
|
try:
|
|
data = json.loads(self._config_path.read_text(encoding="utf-8"))
|
|
if isinstance(data, dict):
|
|
return data
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
return {}
|
|
|
|
def save(self) -> None:
|
|
"""Persist current data to disk with restrictive permissions."""
|
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._config_path.write_text(
|
|
json.dumps(self._data, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
self._config_path.chmod(0o600)
|
|
|
|
def get(self, name: str) -> Any:
|
|
"""Get a config value by dot-separated hierarchical name."""
|
|
return _get_path(self._data, name)
|
|
|
|
def set(self, name: str, value: Any) -> None:
|
|
"""Set a config value by dot-separated hierarchical name."""
|
|
_set_path(self._data, name, value)
|
|
self.save()
|
|
|
|
def remove(self, name: str) -> bool:
|
|
"""Remove a config value by dot-separated hierarchical name."""
|
|
return _remove_path(self._data, name)
|
|
|
|
def get_all(self) -> dict[str, Any]:
|
|
"""Return all values as a flat dict using dot-separated paths."""
|
|
return _walk(self._data)
|
|
|
|
def export(self) -> dict[str, str]:
|
|
"""Return all values with ``MYCLI_`` prefix for shell eval."""
|
|
return {
|
|
f"MYCLI_{k.upper().replace('.', '_')}": str(v)
|
|
for k, v in self.get_all().items()
|
|
}
|
|
|
|
def resolve_key(self, name: str, env_var: str | None = None) -> Any:
|
|
"""Resolve a key with priority: env > config.
|
|
|
|
Args:
|
|
name: Dot-separated hierarchical config key.
|
|
env_var: Optional environment variable name that overrides config.
|
|
"""
|
|
if env_var:
|
|
env_value = os.environ.get(env_var)
|
|
if env_value:
|
|
return env_value
|
|
return self.get(name)
|
|
|
|
|
|
# Module-level singleton.
|
|
config = Config()
|