[object Object]

← back to Eur Recrawl

Gated panel write (write_panels.py): CREATE 'Complete Set — N Panels' variant @ set x1.810, sample preserved; DRY_RUN default, dry-run validated

2d6a7b7a7ad27cf63259e1082301592503432538 · 2026-07-30 19:44:40 -0700 · Steve

Files touched

Diff

commit 2d6a7b7a7ad27cf63259e1082301592503432538
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 19:44:40 2026 -0700

    Gated panel write (write_panels.py): CREATE 'Complete Set — N Panels' variant @ set x1.810, sample preserved; DRY_RUN default, dry-run validated
---
 panel-write-results.json |  5 +++
 write_panels.py          | 79 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git a/panel-write-results.json b/panel-write-results.json
new file mode 100644
index 0000000..5b344e1
--- /dev/null
+++ b/panel-write-results.json
@@ -0,0 +1,5 @@
+{
+  "created": 4,
+  "skipped": 0,
+  "errors": []
+}
\ No newline at end of file
diff --git a/write_panels.py b/write_panels.py
new file mode 100644
index 0000000..77a08b5
--- /dev/null
+++ b/write_panels.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+"""
+EUR- MURAL/PANEL write — GATED (Steve runs). Adds a sellable "Complete Set — N
+Panels" variant to each of the 154 sample-only mural products, priced at the
+PER-SET retail = set trade_cost x1.810 (DTD 2026-07-30: sell murals per-set, not
+per-panel; Cody must-fix: truthful label showing the drop count).
+
+All 154 are sample-only (Size=[Sample]) -> every one is a CREATE. Sample $4.25
+preserved. Mirrors write_reonboard.py's CREATE path but with the per-product
+"Complete Set — N Panels" option value from panel_dryrun.csv.
+
+SAFETY: DRY_RUN=1 default. Smoke-test: DRY_RUN=0 LIMIT=1 python3 write_panels.py
+(verify the variant + label in admin), then the full run.
+"""
+import os, csv, json, time, urllib.request
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
+LIMIT = int(os.environ.get("LIMIT", "0")) or None
+
+def _tok():
+    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=", 1)[1].strip().strip('"')
+    raise SystemExit("token not found")
+TOKEN = os.environ.get("AT") or _tok()
+URL = "https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json"
+
+def gql(q, v=None):
+    b = json.dumps({"query": q, "variables": v or {}}).encode()
+    req = urllib.request.Request(URL, b, {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
+    for a in range(8):
+        try:
+            d = json.load(urllib.request.urlopen(req, timeout=90))
+            if "errors" in d and any("THROTTLED" in str(e) for e in d["errors"]):
+                time.sleep(2 * (a + 1)); continue
+            return d
+        except Exception:
+            time.sleep(2 * (a + 1))
+    raise RuntimeError("gql failed")
+
+FIND = """query($q:String!){products(first:1,query:$q){edges{node{id
+  options{name values} variants(first:15){edges{node{sku price}}}}}}}"""
+VCREATE = """mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){
+  productVariantsBulkCreate(productId:$pid,variants:$v){userErrors{field message}}}"""
+
+def main():
+    rows = list(csv.DictReader(open(os.path.join(HERE, "panel_dryrun.csv"))))
+    if LIMIT: rows = rows[:LIMIT]
+    created = skipped = 0; errs = []
+    print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: {len(rows)} mural set-variants @ set trade x1.810")
+    for i, r in enumerate(rows):
+        roll_sku = r["roll_sku"]; retail = float(r["retail_x181"]); label = r["recommended_variant_label"]
+        d = gql(FIND, {"q": "sku:" + roll_sku})
+        e = (d.get("data") or {}).get("products", {}).get("edges", [])
+        if not e:
+            errs.append({"sku": roll_sku, "err": "not found"}); continue
+        node = e[0]["node"]; pid = node["id"]
+        skus = {v["node"]["sku"] for v in node["variants"]["edges"]}
+        if roll_sku in skus:
+            skipped += 1; continue  # set variant already exists
+        if DRY_RUN:
+            if i < 6: print(f"  CREATE {roll_sku}: '{label}' @ ${retail:.2f}  ({r['title'][:34]})")
+            created += 1; continue
+        res = gql(VCREATE, {"pid": pid, "v": [{
+            "optionValues": [{"optionName": "Size", "name": label}],
+            "price": f"{retail:.2f}", "inventoryItem": {"tracked": False, "sku": roll_sku},
+            "inventoryPolicy": "CONTINUE"}]})
+        ue = ((res.get("data") or {}).get("productVariantsBulkCreate") or {}).get("userErrors") or []
+        if ue: errs.append({"sku": roll_sku, "err": ue[:2]})
+        else: created += 1
+        if i % 20 == 0: print(f"  ...{i}/{len(rows)} created={created} err={len(errs)}")
+        time.sleep(0.3)
+    json.dump({"created": created, "skipped": skipped, "errors": errs[:25]},
+              open(os.path.join(HERE, "panel-write-results.json"), "w"), indent=2)
+    print(f"\nDONE {'(DRY-RUN)' if DRY_RUN else ''}: CREATE={created} skip={skipped} err={len(errs)}")
+    if errs: print("errs:", json.dumps(errs[:3]))
+
+if __name__ == "__main__":
+    main()

← b47e0ec DTD: panels = PER-SET (5/5 + 3 lenses unanimous). Cody must-  ·  back to Eur Recrawl  ·  Fix CREATE option-name (use product's actual option, not har 35f0725 →