← 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()