Files
mytoolkit/mytoolkit/config.py
T
Zhengshou Lai 69efc6b3e2 refactor(mytoolkit): 移除历史兼容,统一分层配置键,补全 Config 单测
- config.Config 仅支持 ~/.mytoolkit/config.json 分层键
- 删除旧 env.json/module-config.json 迁移逻辑与 flat-key 别名
- ssh/bib/image 命令改用 connections.* / paths.* / secrets.api_keys.*
- 单测覆盖分层读写、嵌套删除、export、resolve_key、权限
2026-07-19 11:56:26 +08:00

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()