← back to Novasuede Onboard
Validated tag keep/drop + body planner + live writer (dry-run default + rollback)
e5c548d939949189e727060f53113b7016b5b620 · 2026-06-24 15:08:07 -0700 · Steve
Files touched
A __pycache__/plan_writes.cpython-314.pycM data/enriched.ndjsonA write_live.py
Diff
commit e5c548d939949189e727060f53113b7016b5b620
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jun 24 15:08:07 2026 -0700
Validated tag keep/drop + body planner + live writer (dry-run default + rollback)
---
__pycache__/plan_writes.cpython-314.pyc | Bin 0 -> 10386 bytes
data/enriched.ndjson | 4 ++
write_live.py | 76 ++++++++++++++++++++++++++++++++
3 files changed, 80 insertions(+)
diff --git a/__pycache__/plan_writes.cpython-314.pyc b/__pycache__/plan_writes.cpython-314.pyc
new file mode 100644
index 0000000..b997121
Binary files /dev/null and b/__pycache__/plan_writes.cpython-314.pyc differ
diff --git a/data/enriched.ndjson b/data/enriched.ndjson
index b539d1b..5e47027 100644
--- a/data/enriched.ndjson
+++ b/data/enriched.ndjson
@@ -1,2 +1,6 @@
{"id": 7692849971251, "title": "Novasuede\u2122 - Acorn Luxury Suede", "vendor_name": "Acorn", "handle": "novasuede\u2122-acorn", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-acorn.jpg?v=1761251816", "palette": [{"hex": "#7F3B0F", "percentage": 96}, {"hex": "#5D2805", "percentage": 4}], "dominant_hex": "#7F3B0F", "depth": "deep", "undertone": "warm", "hue_family": "orange", "styles": ["Mediterranean", "Organic Modern", "Mid-Century Modern"], "primary_colorway": "Caramel", "description": "A rich, deep Caramel microfiber suede exudes warmth and sophistication, ideal for creating inviting spaces in Mediterranean or Organic Modern decor.", "_ok": true}
{"id": 7692850004019, "title": "Novasuede\u2122 - Adobe Luxury Suede", "vendor_name": "Adobe", "handle": "novasuede\u2122-adobe", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-adobe.jpg?v=1761251819", "palette": [{"hex": "#EDDBC4", "percentage": 95}, {"hex": "#E0C9AE", "percentage": 5}], "dominant_hex": "#EDDBC4", "depth": "pale", "undertone": "warm", "hue_family": "orange", "styles": ["Desert Modern", "Organic Modern", "Bohemian"], "primary_colorway": "Bone", "description": "A pale Bone-colored microfiber suede exudes warmth and simplicity, perfect for creating serene and organic modern spaces in desert-inspired settings.", "_ok": true}
+{"id": 7692850036787, "title": "Novasuede\u2122 - African Violet Luxury Suede", "vendor_name": "African Violet", "handle": "novasuede\u2122-african-violet", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-african-violet.jpg?v=1761251822", "palette": [{"hex": "#30213F", "percentage": 76}, {"hex": "#423056", "percentage": 24}], "dominant_hex": "#30213F", "depth": "dark", "undertone": "cool", "hue_family": "purple", "styles": ["Hollywood Regency", "Art Deco", "Glam"], "primary_colorway": "Aubergine", "description": "This deep, cool Aubergine microfiber suede exudes elegance and sophistication, making it a perfect choice for creating luxurious Hollywood Regency or Art Deco interiors.", "_ok": true}
+{"id": 7692850069555, "title": "Novasuede\u2122 - Apricot Luxury Suede", "vendor_name": "Apricot", "handle": "novasuede\u2122-apricot", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-apricot.jpg?v=1761251825", "palette": [{"hex": "#D56B41", "percentage": 90}, {"hex": "#C34F28", "percentage": 6}, {"hex": "#A54523", "percentage": 4}], "dominant_hex": "#D56B41", "depth": "medium", "undertone": "warm", "hue_family": "red", "styles": ["Transitional", "Bohemian", "Eclectic"], "primary_colorway": "Terracotta", "description": "A rich terracotta microfiber suede exudes warmth and depth, perfect for creating a cozy ambiance in transitional or bohemian spaces.", "_ok": true}
+{"id": 7692850135091, "title": "Novasuede\u2122 - Arrow Wood Luxury Suede", "vendor_name": "Arrow Wood", "handle": "novasuede\u2122-arrow-wood", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-arrow-wood.jpg?v=1761251827", "palette": [{"hex": "#A18967", "percentage": 26}, {"hex": "#4C3C25", "percentage": 13}, {"hex": "#756146", "percentage": 44}, {"hex": "#604D35", "percentage": 6}, {"hex": "#8C7558", "percentage": 6}, {"hex": "#342715", "percentage": 5}], "dominant_hex": "#A18967", "depth": "medium", "undertone": "warm", "hue_family": "orange", "styles": ["Desert Modern", "Organic Modern", "Bohemian"], "primary_colorway": "Oatmeal", "description": "A warm, medium-toned Oatmeal microfiber suede exudes a rustic charm, perfect for Desert Modern or Organic Modern interiors that embrace earthy tones and natural textures.", "_ok": true}
+{"id": 7692850167859, "title": "Novasuede\u2122 - Aubergine Luxury Suede", "vendor_name": "Aubergine", "handle": "novasuede\u2122-aubergine", "img": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/novasuede-aubergine.jpg?v=1761251830", "palette": [{"hex": "#4F2F41", "percentage": 62}, {"hex": "#321725", "percentage": 38}], "dominant_hex": "#4F2F41", "depth": "dark", "undertone": "cool", "hue_family": "pink", "styles": ["Glam", "Transitional", "Contemporary"], "primary_colorway": "Mauve", "description": "A rich Mauve microfiber suede exudes elegance with its deep cool undertone, perfect for creating a sophisticated ambiance in both Glam and Transitional interior spaces.", "_ok": true}
diff --git a/write_live.py b/write_live.py
new file mode 100644
index 0000000..f718864
--- /dev/null
+++ b/write_live.py
@@ -0,0 +1,76 @@
+#!/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()
← 5c3b06d Novasuede lexicon onboarding: pull + classifier + planner sc
·
back to Novasuede Onboard
·
auto-save: 2026-06-24T15:27:54 (2 files) — data/enriched.ndj 0b87ba8 →