← back to Dw Collection Banner Audit

apply_live.py

96 lines

#!/usr/bin/env python3
"""
LIVE collection-banner writer for the DW Shopify store (Admin GraphQL collectionUpdate).
Usage:
  python3 apply_live.py canary        # apply the 8 highest-traffic READY collections
  python3 apply_live.py rest          # apply all remaining READY not yet applied
  python3 apply_live.py <handle> ...  # apply specific handles
Records every write to rollback.jsonl (handle, old_src, new_src, ok). Reversible.
Reads token from env SHOPIFY_ADMIN_TOKEN (never printed).
"""
import json, os, sys, time, urllib.request

HERE = os.path.dirname(os.path.abspath(__file__))
STORE = "designer-laboratory-sandbox.myshopify.com"
API = "2024-10"
TOKEN = os.environ.get("SHOPIFY_ADMIN_TOKEN", "")
assert TOKEN, "SHOPIFY_ADMIN_TOKEN not in env"

cols = {c["handle"]: c for c in json.load(open(os.path.join(HERE, "dw_collections.json")))}
proposal = {r["handle"]: r for r in json.load(open(os.path.join(HERE, "proposal.json")))}
ready = [h for h, r in proposal.items() if r["status"] == "READY"]

CANARY = ["primary-suite-1", "grey-silver", "paper", "modern", "warm-taupe",
          "metallic-wallpaper-collections", "botanicals-and-florals", "florals-botanicals"]

MUT = """mutation($input: CollectionInput!) {
  collectionUpdate(input: $input) {
    collection { id handle image { url width height } }
    userErrors { field message }
  }
}"""

def gql(query, variables):
    body = json.dumps({"query": query, "variables": variables}).encode()
    req = urllib.request.Request(
        f"https://{STORE}/admin/api/{API}/graphql.json", data=body,
        headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
    return json.load(urllib.request.urlopen(req, timeout=40))

def already_applied():
    done = set()
    p = os.path.join(HERE, "rollback.jsonl")
    if os.path.exists(p):
        for line in open(p):
            try:
                d = json.loads(line)
                if d.get("ok"): done.add(d["handle"])
            except: pass
    return done

def apply(handles):
    log = open(os.path.join(HERE, "rollback.jsonl"), "a")
    ok = fail = 0
    for i, h in enumerate(handles, 1):
        r = proposal.get(h); c = cols.get(h)
        if not r or not c or r["status"] != "READY":
            print(f"  SKIP {h} (not a READY proposal)"); continue
        gid = f"gid://shopify/Collection/{c['id']}"
        new_src = r["new_src"]; old_src = r.get("current_src")
        try:
            res = gql(MUT, {"input": {"id": gid, "image": {"src": new_src}}})
            errs = res.get("data", {}).get("collectionUpdate", {}).get("userErrors", [])
            top = res.get("errors")
            if top:
                print(f"  FAIL {h}: {top}"); fail += 1
                log.write(json.dumps({"handle": h, "ok": False, "err": str(top)}) + "\n")
            elif errs:
                print(f"  FAIL {h}: {errs}"); fail += 1
                log.write(json.dumps({"handle": h, "ok": False, "err": str(errs)}) + "\n")
            else:
                img = res["data"]["collectionUpdate"]["collection"]["image"]
                dim = f"{img['width']}x{img['height']}" if img else "?"
                print(f"  OK   {h}  -> {dim}")
                ok += 1
                log.write(json.dumps({"handle": h, "ok": True, "old_src": old_src,
                                      "new_src": new_src, "new_url": img["url"] if img else None,
                                      "new_dim": dim}) + "\n")
        except Exception as e:
            print(f"  ERR  {h}: {str(e)[:120]}"); fail += 1
            log.write(json.dumps({"handle": h, "ok": False, "err": str(e)[:200]}) + "\n")
        time.sleep(0.6)  # gentle on the 2 req/s REST-equiv; GraphQL cost-based but stay polite
    log.close()
    print(f"\napplied ok={ok} fail={fail}")

if __name__ == "__main__":
    arg = sys.argv[1] if len(sys.argv) > 1 else "canary"
    if arg == "canary":
        target = CANARY
    elif arg == "rest":
        done = already_applied()
        target = [h for h in ready if h not in done]
        print(f"remaining READY to apply: {len(target)}")
    else:
        target = sys.argv[1:]
    apply(target)