← back to Novasuede Onboard

write_live.py

77 lines

#!/usr/bin/env python3
"""
Write enriched tags + body_html to the LIVE Novasuede products.
SAFE BY DEFAULT: dry-run unless --commit is passed.
Backs up every product's pre-write {id,tags,body_html} to data/rollback.ndjson
BEFORE writing, so the change is fully reversible.

Usage:
  write_live.py            # dry run: prints what would change, writes nothing
  write_live.py --commit   # performs the live PUTs (Steve-authorized)
  write_live.py --rollback # restores tags+body from data/rollback.ndjson
"""
import sys, json, time, urllib.request, pathlib

ROOT = pathlib.Path(__file__).resolve().parent; DATA = ROOT/"data"
env = pathlib.Path.home()/"Projects/secrets-manager/.env"
TOKEN=None
for ln in env.read_text().splitlines():
    if ln.startswith("SHOPIFY_ADMIN_TOKEN="):
        TOKEN=ln.split("=",1)[1].strip().strip('"'); break
SHOP="designer-laboratory-sandbox.myshopify.com"
API=f"https://{SHOP}/admin/api/2024-01"
COMMIT="--commit" in sys.argv
ROLLBACK="--rollback" in sys.argv

def req(method, path, body=None):
    data=json.dumps(body).encode() if body is not None else None
    r=urllib.request.Request(API+path, data=data, method=method,
        headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
    for _ in range(6):
        try:
            with urllib.request.urlopen(r, timeout=30) as resp:
                return json.load(resp)
        except urllib.error.HTTPError as e:
            if e.code==429: time.sleep(2.5); continue
            raise
    raise RuntimeError("retries exhausted")

def get_current(pid):
    return req("GET", f"/products/{pid}.json?fields=id,tags,body_html")["product"]

def do_rollback():
    rb=[json.loads(l) for l in (DATA/"rollback.ndjson").read_text().splitlines()]
    print(f"Rolling back {len(rb)} products...")
    for i,p in enumerate(rb,1):
        req("PUT", f"/products/{p['id']}.json",
            {"product":{"id":p["id"],"tags":p["tags"],"body_html":p["body_html"]}})
        print(f"[{i}/{len(rb)}] restored {p['id']}"); time.sleep(0.55)
    print("rollback complete")

def main():
    if ROLLBACK: return do_rollback()
    plan=[json.loads(l) for l in (DATA/"plan.ndjson").read_text().splitlines()]
    print(f"{'COMMIT' if COMMIT else 'DRY RUN'} — {len(plan)} products\n")
    rb_path=DATA/"rollback.ndjson"
    rb = rb_path.open("a") if COMMIT else None
    done=0
    for i,p in enumerate(plan,1):
        if COMMIT:
            cur=get_current(p["id"])                 # backup live state first
            rb.write(json.dumps({"id":cur["id"],"tags":cur.get("tags",""),
                                 "body_html":cur.get("body_html","")})+"\n"); rb.flush()
            req("PUT", f"/products/{p['id']}.json",
                {"product":{"id":p["id"],"tags":p["new_tags"],"body_html":p["new_body"]}})
            done+=1
            print(f"[{i}/{len(plan)}] ✓ {p['title']}  (+{len(p['added'])}/-{len(p['removed'])} tags)")
            time.sleep(0.6)
        else:
            print(f"[{i}/{len(plan)}] {p['title']}")
            print(f"    + {p['added']}")
            print(f"    - {p['removed']}")
    if rb: rb.close()
    print(f"\n{'WROTE '+str(done) if COMMIT else 'dry-run only — no writes'} | rollback log: {rb_path if COMMIT else '(none)'}")

if __name__=="__main__":
    main()