[object Object]

← 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

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 →