115 lines
3.7 KiB
Python
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())
|