Files

115 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Apply this plugin's settings snapshot to a harness config file.
python apply.py --config ~/.dsh/config.json # merge defaults in
python apply.py --config ~/.dsh/config.json --dry-run
python apply.py --config ~/.dsh/config.json --only model.* provider.*
Existing keys are kept unless --overwrite is given, so applying this to a
machine someone has already tuned will not stomp their choices.
A backup of the original file is written next to it before any change.
"""
from __future__ import annotations
import argparse
import fnmatch
import json
import shutil
import sys
from pathlib import Path
DEFAULTS = Path(__file__).resolve().parent / "defaults.json"
def flatten(prefix: str, value, out: dict) -> None:
"""`{"model": {"temperature": 0.7}}` -> `{"model.temperature": 0.7}`."""
if isinstance(value, dict):
for k, v in value.items():
flatten(f"{prefix}.{k}" if prefix else k, v, out)
else:
out[prefix] = value
def load_defaults() -> dict:
raw = json.loads(DEFAULTS.read_text(encoding="utf-8"))
out: dict = {}
flatten("", raw, out)
return {k: v for k, v in out.items() if not k.startswith("_")}
def set_path(cfg: dict, dotted: str, value) -> None:
parts = dotted.split(".")
node = cfg
for p in parts[:-1]:
nxt = node.get(p)
if not isinstance(nxt, dict):
nxt = {}
node[p] = nxt
node = nxt
node[parts[-1]] = value
def get_path(cfg: dict, dotted: str):
node = cfg
for p in dotted.split("."):
if not isinstance(node, dict) or p not in node:
return None, False
node = node[p]
return node, True
def apply(config_path: Path, *, overwrite: bool, dry_run: bool,
only: list[str] | None) -> dict:
defaults = load_defaults()
if only:
defaults = {k: v for k, v in defaults.items()
if any(fnmatch.fnmatch(k, pat) for pat in only)}
if config_path.is_file():
cfg = json.loads(config_path.read_text(encoding="utf-8") or "{}")
else:
cfg = {}
added, kept = [], []
for key, value in sorted(defaults.items()):
_cur, present = get_path(cfg, key)
if present and not overwrite:
kept.append(key)
continue
set_path(cfg, key, value)
added.append(key)
if added and not dry_run:
config_path.parent.mkdir(parents=True, exist_ok=True)
if config_path.is_file():
shutil.copy2(config_path, config_path.with_suffix(config_path.suffix + ".bak"))
config_path.write_text(
json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return {"added": added, "kept": kept, "config": str(config_path)}
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="apply team default settings")
ap.add_argument("--config", type=Path, required=True)
ap.add_argument("--overwrite", action="store_true",
help="replace values that already exist")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--only", nargs="*", metavar="GLOB",
help="limit to keys matching these patterns, e.g. 'model.*'")
args = ap.parse_args(argv)
result = apply(args.config, overwrite=args.overwrite, dry_run=args.dry_run,
only=args.only)
if args.dry_run:
print("dry run — nothing written")
print(f"config : {result['config']}")
print(f"added : {len(result['added'])}" + (f" {result['added']}" if result["added"] else ""))
print(f"kept : {len(result['kept'])}" + (f" {result['kept']}" if result["kept"] else ""))
return 0
if __name__ == "__main__":
sys.exit(main())