run_sample.py
Language Python8.2 KB183 lines
#!/usr/bin/env python3"""run_sample.py: replay the sample's two Monday runs of the weekly pipeline.Everything the vault ships under 03-Clients/, 05-Outputs/ (runs and audit) and07-Automation/data/ came out of this replay, apart from the research added by handto ten stubs, which a replay does not regenerate. It does what the scheduled Claudesession does in the real run (see scheduled-task.md), with the MCP calls stoodin for by files from sample/inputs/:0. once: seed the baseline from the roster workbook in two passes, copy it asthe first Drive-swap base, and start an empty census1. per run date, oldest first:a. pull each day of the trailing window from Plinth and ingest it, paginguntil a page adds nothing (the convergence loop; a missing next pagestands for a response that adds nothing)b. classify, verify (exit 2 stops the run), write-stubs, refresh-xml,refresh-workbookc. reconcile-deals and route against that week's Bitrix enumerationd. outreach-pull-list, then attach stage histories and contacts only forthe deals it asks for (the get_stage_history / get_record step), thenoutreach-auditThe clock is pinned to each run date with freezegun, because the driver datesits outputs by today's date. That is the only thing this script changes.Usage (from the vault root, on a copy with the generated state removed; theREADME's "Replay the sample" has the exact commands):pip install -r 07-Automation/requirements.txtpython3 07-Automation/sample/generate_sample.pypython3 07-Automation/sample/run_sample.py"""from __future__ import annotationsimport argparseimport datetime as dtimport jsonimport reimport shutilimport subprocessimport sysfrom pathlib import PathHERE = Path(__file__).resolve().parentVAULT = HERE.parent.parentDRIVER = HERE.parent / "weekly_partner_pipeline.py"CENSUS = "03-Clients/partner-client-master-list.xml"CENSUS_ROOT = "partner_client_master_list"ALL_TAB = "All Accounts"PARTNER_TAB = "Complete Partner Clients List"BASE_PREFIX = "Complete-Partner-Clients-List_updated-"ROSTER_GLOB = "Partner-Clients-Roster_*.xlsx"BASELINE_AS_OF = dt.date(2026, 9, 7)RUN_DATES = [dt.date(2026, 9, 14), dt.date(2026, 9, 21)]PULL_WINDOW_DAYS = 8SUSPECT_ROWS = 50BOOT = ("import sys, runpy, freezegun\n""freezegun.freeze_time(sys.argv[1]).start()\n""sys.argv = sys.argv[2:]\n""runpy.run_path(sys.argv[0], run_name='__main__')\n")class Run:def __init__(self, vault: Path, inputs: Path, driver: Path):self.vault, self.inputs, self.driver = vault, inputs, driverself.out = vault / "05-Outputs" / "partner-runs"self.log: list[str] = []def drive(self, when: str, *args: str, ok=(0,)) -> str:cmd = [sys.executable, "-c", BOOT, when, str(self.driver), *args]p = subprocess.run(cmd, cwd=self.vault, capture_output=True, text=True)# Logs are kept in the vault, so print paths relative to it.text = (p.stdout + p.stderr).replace(f"{self.vault}/", "")shown = " ".join(a if " " not in a else f'"{a}"' for a in args)self.log.append(f"$ weekly_partner_pipeline.py {shown}\n{text.rstrip()}\n")print(f" {args[0]:<20} exit {p.returncode}")if p.returncode not in ok:print(text)sys.exit(f"stopped: {args[0]} exited {p.returncode}")return textdef seed(self):roster = sorted((self.inputs).glob(ROSTER_GLOB))[-1]data = self.vault / "07-Automation" / "data"data.mkdir(parents=True, exist_ok=True)for f in ("merge_registry.json", "suppression_list.json"):if not (data / f).exists():shutil.copy2(self.inputs / f, data / f)rel = roster.relative_to(self.vault) if roster.is_relative_to(self.vault) else rosterwhen = f"{BASELINE_AS_OF.isoformat()} 18:00:00"self.drive(when, "seed-baseline", "--sheet", str(rel), "--tab", ALL_TAB,"--as-of", BASELINE_AS_OF.isoformat())self.drive(when, "seed-baseline", "--sheet", str(rel), "--tab", PARTNER_TAB,"--as-of", BASELINE_AS_OF.isoformat(), "--force")self.out.mkdir(parents=True, exist_ok=True)shutil.copy2(roster, self.out / f"{BASE_PREFIX}{BASELINE_AS_OF.isoformat()}.xlsx")(self.vault / "03-Clients" / "client-stubs").mkdir(parents=True, exist_ok=True)census = self.vault / CENSUSif not census.exists():census.parent.mkdir(parents=True, exist_ok=True)census.write_text("<?xml version='1.0' encoding='UTF-8'?>\n"f'<{CENSUS_ROOT} schema_version="3.0" />\n', encoding="utf-8")def pull(self, run_date: dt.date, when: str):raw = self.out / "raw" / run_date.isoformat()raw.mkdir(parents=True, exist_ok=True)for back in range(PULL_WINDOW_DAYS - 1, -1, -1):day = run_date - dt.timedelta(days=back)page = 0while True:src = self.inputs / "plinth" / f"{day.isoformat()}_p{page}.json"if not src.exists():breakdst = raw / src.nameshutil.copy2(src, dst)text = self.drive(when, "ingest", "--orders", str(dst.relative_to(self.vault)))m = re.search(r"parsed=(\d+) added=(\d+)", text)parsed, added = (int(m.group(1)), int(m.group(2))) if m else (0, 0)# Keep paging while a page adds orders or looks truncated.if added == 0 and parsed < SUSPECT_ROWS:breakpage += 1def week(self, run_date: dt.date):d = run_date.isoformat()when = f"{d} 06:00:00"print(f"run {d}")self.pull(run_date, when)self.drive(when, "classify")self.drive(when, "verify")plan = f"05-Outputs/partner-runs/partner-classification_{d}.plan.json"self.drive(when, "write-stubs", "--plan", plan)self.drive(when, "refresh-xml", "--plan", plan)self.drive(when, "refresh-workbook", "--plan", plan)deals_in = self.inputs / "bitrix" / f"bitrix-deals_{d}.json"deals = self.out / f"bitrix-deals_{d}.json"shutil.copy2(deals_in, deals)rel = str(deals.relative_to(self.vault))self.drive(when, "reconcile-deals", "--deals", rel)self.drive(when, "route")self.drive(when, "outreach-pull-list", "--deals", rel)pl = json.loads((self.out / f"outreach-pull-list_{d}.json").read_text(encoding="utf-8"))hist = json.loads((self.inputs / "bitrix" / "stage-histories.json").read_text(encoding="utf-8"))cons = json.loads((self.inputs / "bitrix" / "contacts.json").read_text(encoding="utf-8"))need_h, need_c = set(pl["need_history"]), set(pl["need_contact"])body = json.loads(deals.read_text(encoding="utf-8"))for x in body["deals"]:did = x["deal_id"]if did in need_h:x["history"] = [e for e in hist.get(did, []) if e["at"][:10] <= d]if did in need_c and did in cons:x["contact"] = cons[did]deals.write_text(json.dumps(body, indent=1), encoding="utf-8")self.drive(when, "outreach-audit", "--deals", rel)(self.out / f"run-log_{d}.txt").write_text("\n".join(self.log), encoding="utf-8")self.log = []def main():ap = argparse.ArgumentParser(description="Replay the sample's two weekly runs.")ap.add_argument("--vault", default=str(VAULT))ap.add_argument("--inputs", default=str(HERE / "inputs"))ap.add_argument("--driver", default=str(DRIVER))a = ap.parse_args()r = Run(Path(a.vault).resolve(), Path(a.inputs).resolve(), Path(a.driver).resolve())if (r.vault / "07-Automation" / "data" / "order_history.json").exists():sys.exit("order_history.json already exists: this replay expects an unseeded vault ""(see the README, 'Replay the sample').")print("seed")r.seed()(r.out / f"run-log_{BASELINE_AS_OF.isoformat()}-seed.txt").write_text("\n".join(r.log), encoding="utf-8")r.log = []for day in RUN_DATES:r.week(day)print("done")if __name__ == "__main__":main()