← back to Mr Image Repair
TK-11638 follow-up: add gated archive_discontinued.py (3 disco River Bed colorways) + dry-run clean
6239024742dc7bce654bf17788fb826359a8a782 · 2026-09-14 13:30:26 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A archive_discontinued.py
Diff
commit 6239024742dc7bce654bf17788fb826359a8a782
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Sep 14 13:30:26 2026 -0700
TK-11638 follow-up: add gated archive_discontinued.py (3 disco River Bed colorways) + dry-run clean
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
archive_discontinued.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 98 insertions(+)
diff --git a/archive_discontinued.py b/archive_discontinued.py
new file mode 100644
index 0000000..2bd9ba1
--- /dev/null
+++ b/archive_discontinued.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+"""
+TK-11638 follow-up — GATED archive of 3 DISCONTINUED Maya Romanoff River Bed colorways.
+HARD-GATED: DRY-RUN by default. Requires --apply AND env CONFIRM_MR_ARCHIVE=1 to
+touch Shopify (customer-facing live store designer-laboratory-sandbox).
+
+These 3 are: (a) discontinued — the colorway is gone from the live MR River Bed line
+(replaced by Glacier/Gold Rush/Silver Rush), (b) unbuyable — sample-only, no sellable
+roll variant, and (c) still logo-hero with no obtainable real image. Per the DW rule
+"discontinued = ARCHIVE candidate" they are archived (status ACTIVE -> ARCHIVED).
+
+Archive is REVERSIBLE: the product record is retained and can be un-archived
+(status -> active). Journal records prior status per product for a clean revert.
+
+Reads: data/mr_unresolved.tsv (rows where bucket == disco-colorway)
+Writes rollback journal: data/mr_archive_journal.jsonl (prior status per product).
+"""
+import csv, os, sys, json, time, urllib.request, urllib.error
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SRC = os.path.join(HERE, "data", "mr_unresolved.tsv")
+JRN = os.path.join(HERE, "data", "mr_archive_journal.jsonl")
+SHOP = "designer-laboratory-sandbox.myshopify.com"
+API = "2024-10"
+APPLY = "--apply" in sys.argv
+
+def token():
+ for line in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+ for k in ("SHOPIFY_ADMIN_TOKEN", "SHOPIFY_FULL_ACCESS_TOKEN"):
+ if line.startswith(k + "="):
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
+ raise SystemExit("no SHOPIFY token")
+TOK = token()
+
+def api(method, path, body=None, _tries=4):
+ url = f"https://{SHOP}/admin/api/{API}/{path}"
+ data = json.dumps(body).encode() if body is not None else None
+ for attempt in range(1, _tries + 1):
+ req = urllib.request.Request(url, data=data, method=method,
+ headers={"X-Shopify-Access-Token": TOK, "Content-Type": "application/json"})
+ try:
+ with urllib.request.urlopen(req, timeout=60) as r:
+ return json.loads(r.read())
+ except urllib.error.HTTPError as e:
+ if e.code in (429, 500, 502, 503, 504) and attempt < _tries:
+ time.sleep(float(e.headers.get("Retry-After", attempt)) or attempt); continue
+ raise
+
+def product_for_handle(handle):
+ d = api("GET", f"products.json?handle={handle}&fields=id,title,status,handle,variants")
+ ps = d.get("products", [])
+ return ps[0] if ps else None
+
+def main():
+ if APPLY and os.environ.get("CONFIRM_MR_ARCHIVE") != "1":
+ print("REFUSING: --apply requires env CONFIRM_MR_ARCHIVE=1 (Steve-gated). Aborting.")
+ sys.exit(2)
+ rows = [r for r in csv.DictReader(open(SRC), delimiter="\t") if r.get("bucket") == "disco-colorway"]
+ print(f"mode={'APPLY' if APPLY else 'DRY-RUN'} rows={len(rows)} store={SHOP}")
+ jf = open(JRN, "a") if APPLY else None
+ ok = err = skipped = 0
+ for i, r in enumerate(rows, 1):
+ dw, handle, title = r["dw_sku"], r["handle"], r["our_title"]
+ try:
+ prod = product_for_handle(handle)
+ if not prod:
+ err += 1; print(f"[{i}] ERR {dw} handle={handle} -> not found"); continue
+ pid, st = prod["id"], prod["status"]
+ vs = prod.get("variants", [])
+ sellable = [v for v in vs if (v.get("title") or "").lower() != "sample"]
+ # SAFETY: only archive if it is genuinely unbuyable (no non-sample sellable variant).
+ if sellable:
+ skipped += 1
+ print(f"[{i}] SKIP {dw} pid={pid} has a sellable variant ({len(sellable)}) -> NOT archiving")
+ continue
+ if st == "archived":
+ skipped += 1; print(f"[{i}] SKIP {dw} pid={pid} already archived"); continue
+ if not APPLY:
+ print(f"[{i}] DRY {dw} pid={pid} status={st} -> ARCHIVED ({title}) [sample-only, discontinued]")
+ ok += 1; continue
+ jf.write(json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%S"), "dw_sku": dw,
+ "product_id": pid, "old_status": st, "rollback": f"PUT products/{pid}.json status=active"}) + "\n")
+ jf.flush()
+ api("PUT", f"products/{pid}.json", {"product": {"id": pid, "status": "archived"}})
+ time.sleep(0.4)
+ chk = api("GET", f"products/{pid}.json?fields=id,status").get("product", {}).get("status")
+ if chk == "archived":
+ ok += 1; print(f"[{i}] OK {dw} pid={pid} now ARCHIVED")
+ else:
+ err += 1; print(f"[{i}] WARN {dw} pid={pid} status={chk} (not archived) — review")
+ except Exception as e:
+ err += 1; print(f"[{i}] ERR {dw} handle={handle}: {e}")
+ time.sleep(0.5)
+ if jf: jf.close()
+ print(f"done ok={ok} skipped={skipped} err={err} " + ("(DRY-RUN — no writes)" if not APPLY else "(LIVE)"))
+
+if __name__ == "__main__":
+ main()
← f2b7092 TK-11638: EXECUTED 7-image MR logo-hero write (Steve-approve
·
back to Mr Image Repair
·
TK-11736: EXECUTED archive of 3 discontinued MR River Bed co 2750c58 →