← back to Mr Image Repair
archive_discontinued.py
99 lines
#!/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()