Add the puga command and plan pull; document the UI plan workflow
- pyproject.toml + puga/cli.py: `pip install -e .` installs a `puga <tool>` command, so no venv path is needed (replaces requirements.txt) - tools/plan_push.py: `pull <uuid>` reads a plan (incl. UI edits) back into a YAML spec; recipe entries may carry an amount so switched-off recipes round trip; fixes a variable that overwrote the plan name in build_payload, with a regression test - README: usage with the puga command, and a section on working on the same plans in the PRUNplanner UI and from Claude (write, edit in UI, read back) - CLAUDE.md updated accordingly Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+27
-3
@@ -4,6 +4,7 @@ DEFAULT IS DRY RUN. Guardrails (docs/decisions.md): only plans named '[PuGa] ...
|
||||
--update refuses if the existing plan's name does not start with '[PuGa]'. Show the dry-run to the user and get a yes before --apply.
|
||||
|
||||
tools/plan_push.py list
|
||||
tools/plan_push.py pull <uuid> [-o file.yaml] # read a plan (UI edits included) back into a YAML spec
|
||||
tools/plan_push.py plans/examples/base_plus_hwp.yaml # dry run: validated payload summary
|
||||
tools/plan_push.py plans/examples/base_plus_hwp.yaml --json # full JSON payload
|
||||
tools/plan_push.py plans/examples/base_plus_hwp.yaml --apply # create (after user says yes)
|
||||
@@ -66,7 +67,10 @@ def build_payload(spec: dict, recipes: list[dict], building_tickers: set[str]) -
|
||||
tk = b["building"]
|
||||
if tk not in building_tickers:
|
||||
raise ValueError(f"unknown building {tk}")
|
||||
recs = [{"recipeid": resolve_recipe(r, tk, recipes), "amount": 1} for r in b.get("recipes", [])]
|
||||
recs = []
|
||||
for r in b.get("recipes", []):
|
||||
rname, amt = (r["recipe"], r.get("amount", 1)) if isinstance(r, dict) else (r, 1)
|
||||
recs.append({"recipeid": resolve_recipe(rname, tk, recipes), "amount": int(amt)})
|
||||
buildings.append({"name": tk, "amount": int(b["amount"]), "active_recipes": recs})
|
||||
infra = [{"building": k, "amount": int(v)} for k, v in (spec.get("infrastructure") or {}).items()]
|
||||
return {
|
||||
@@ -80,8 +84,9 @@ def build_payload(spec: dict, recipes: list[dict], building_tickers: set[str]) -
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("spec", help="plans/*.yaml, 'list', or 'delete'")
|
||||
ap.add_argument("target", nargs="?", help="uuid for 'delete'")
|
||||
ap.add_argument("spec", help="plans/*.yaml, 'list', 'pull' or 'delete'")
|
||||
ap.add_argument("target", nargs="?", help="uuid for 'delete' / 'pull'")
|
||||
ap.add_argument("-o", "--out", help="with 'pull': write the YAML spec to this file")
|
||||
ap.add_argument("--apply", action="store_true", help="actually write to PRUNplanner (creates a new plan)")
|
||||
ap.add_argument("--update", metavar="UUID", help="with --apply: update this existing [PuGa] plan instead of creating")
|
||||
ap.add_argument("--json", action="store_true")
|
||||
@@ -91,6 +96,25 @@ def main():
|
||||
for p in pp.request("GET", "/planning/plan/"):
|
||||
print(p["uuid"], p.get("plan_name"), p.get("planet_natural_id"))
|
||||
return
|
||||
if a.spec == "pull": # read a plan from the account (e.g. after edits in the PRUNplanner UI) into a YAML spec
|
||||
cur = pp.request("GET", f"/planning/plan/{a.target}/")
|
||||
d = cur["plan_data"]
|
||||
cg = cur.get("plan_cogc")
|
||||
spec = {"name": cur["plan_name"], "planet": cur["planet_natural_id"], "permits": cur["plan_permits_used"],
|
||||
"cogc": None if cg in (None, "---") else cg, "hq": cur["plan_corphq"],
|
||||
"experts": {e["type"].upper(): e["amount"] for e in d["experts"] if e["amount"]},
|
||||
"lux": {w["type"]: [w["lux1"], w["lux2"]] for w in d["workforce"] if not (w["lux1"] and w["lux2"])},
|
||||
"infrastructure": {i["building"]: i["amount"] for i in d["infrastructure"]},
|
||||
"buildings": [{"building": b["name"], "amount": b["amount"],
|
||||
"recipes": [r["recipeid"] if r["amount"] == 1 else {"recipe": r["recipeid"], "amount": r["amount"]} for r in b["active_recipes"]]}
|
||||
for b in d["buildings"]]}
|
||||
text = yaml.safe_dump(spec, sort_keys=False, default_flow_style=None, width=140)
|
||||
if a.out:
|
||||
Path(a.out).write_text(text)
|
||||
print("wrote", a.out)
|
||||
else:
|
||||
print(text)
|
||||
return
|
||||
if a.spec == "delete":
|
||||
# delete is allowed only on request, and only for plans this tool made ([PuGa] prefix).
|
||||
cur = pp.request("GET", f"/planning/plan/{a.target}/")
|
||||
|
||||
Reference in New Issue
Block a user