[object Object]

← back to Dw Yolo Loop

Romo ADD canary: create script + 10 DRAFT products on live store (ledger)

e6d0701c2f7f05f3da5732a2997324ee9c6a7e3a · 2026-08-03 11:17:29 -0700 · steve-office

- W-code reconciliation vs live store → 817 true-new safe-to-ADD
- mirrors live Romo structure (Title option, Single Roll + $4.25 Sample)
- 10/10 verified: sellable variant, sample, image, attribution tags, draft
- idempotency guard (skip existing SKU) + reversibility ledger

Files touched

Diff

commit e6d0701c2f7f05f3da5732a2997324ee9c6a7e3a
Author: steve-office <steve@designerwallcoverings.com>
Date:   Mon Aug 3 11:17:29 2026 -0700

    Romo ADD canary: create script + 10 DRAFT products on live store (ledger)
    
    - W-code reconciliation vs live store → 817 true-new safe-to-ADD
    - mirrors live Romo structure (Title option, Single Roll + $4.25 Sample)
    - 10/10 verified: sellable variant, sample, image, attribution tags, draft
    - idempotency guard (skip existing SKU) + reversibility ledger
---
 artmura-site/romo-canary-create.py | 86 ++++++++++++++++++++++++++++++++++++++
 romo-canary-created.jsonl          | 10 +++++
 2 files changed, 96 insertions(+)

diff --git a/artmura-site/romo-canary-create.py b/artmura-site/romo-canary-create.py
new file mode 100644
index 0000000..d8ad01f
--- /dev/null
+++ b/artmura-site/romo-canary-create.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""
+romo-canary-create.py — create the 10 Romo canary products on the LIVE store as DRAFT.
+Mirrors the live Romo structure: product_type Wallcovering, single 'Title' option
+[Single Roll, Sample], sellable variant (our_price) + Sample ($4.25).
+- Idempotency guard: skips a SKU that already exists (GraphQL sku: search).
+- status=draft (NOT customer-facing) — activation is a separate gate.
+- Writes a ledger of created product ids for reversibility.
+Reads /tmp/romo_canary10.jsonl + env SHOPIFY_ADMIN_TOKEN, STORE.
+"""
+import json, os, time, urllib.request, urllib.error
+
+TOKEN = os.environ['SHOPIFY_ADMIN_TOKEN']; STORE = os.environ['STORE']
+API = f"https://{STORE}/admin/api/2024-10"
+LEDGER = os.path.expanduser("~/Projects/dw-yolo-loop/romo-canary-created.jsonl")
+H = {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"}
+
+def req(method, url, body=None):
+    data = json.dumps(body).encode() if body is not None else None
+    r = urllib.request.Request(url, data=data, headers=H, method=method)
+    try:
+        with urllib.request.urlopen(r, timeout=30) as resp:
+            return resp.status, json.loads(resp.read().decode())
+    except urllib.error.HTTPError as e:
+        return e.code, json.loads(e.read().decode() or '{}')
+
+def sku_exists(sku):
+    q = {"query": "{ productVariants(first:1, query:\"sku:%s\"){edges{node{id sku}}} }" % sku}
+    _, d = req("POST", f"{API}/graphql.json", q)
+    return bool(d.get("data", {}).get("productVariants", {}).get("edges"))
+
+def norm_list(x):
+    if isinstance(x, list): return x
+    if x and x not in ('', 'null'):
+        try: return json.loads(x)
+        except: return []
+    return []
+
+def build(row):
+    title = f"{row['pattern']} {row['color']}".strip()
+    styles = norm_list(row.get('styles')); motifs = norm_list(row.get('pats')); aitags = norm_list(row.get('tags'))
+    tags = list(dict.fromkeys(["Romo","Wallcovering","display_variant","Priced Per Single Roll",
+                               *styles, *motifs, *aitags]))
+    imgs = norm_list(row.get('all_images'))
+    if not imgs and row.get('img'): imgs = [row['img']]
+    # all_images may be a pipe-joined string
+    if isinstance(row.get('all_images'), str) and '|' in row['all_images']:
+        imgs = [u.strip() for u in row['all_images'].split('|') if u.strip().startswith('http')]
+    images = [{"src": u} for u in (imgs or ([row['img']] if row.get('img') else []))][:6]
+    body = f"<p>{row['desc']}</p>" if row.get('desc') else ""
+    dw = row['dw']
+    return {"product": {
+        "title": title, "vendor": "Romo", "product_type": row.get('type') or "Wallcovering",
+        "status": "draft", "body_html": body, "tags": ", ".join(tags),
+        "options": [{"name": "Title", "values": ["Single Roll", "Sample"]}],
+        "variants": [
+            {"option1": "Single Roll", "price": str(row['price']), "sku": dw,
+             "inventory_policy": "continue", "requires_shipping": True, "taxable": True},
+            {"option1": "Sample", "price": "4.25", "sku": f"{dw}-Sample",
+             "inventory_policy": "deny", "requires_shipping": True, "taxable": True},
+        ],
+        "images": images,
+    }}
+
+rows = [json.loads(l) for l in open('/tmp/romo_canary10.jsonl') if l.strip()]
+print(f"=== ROMO CANARY CREATE — {len(rows)} products, status=DRAFT ===\n")
+led = open(LEDGER, "a")
+created = []
+for row in rows:
+    dw = row['dw']
+    if sku_exists(dw):
+        print(f"  SKIP {dw} — SKU already exists on store"); continue
+    payload = build(row)
+    st, resp = req("POST", f"{API}/products.json", payload)
+    if st in (200, 201) and resp.get("product"):
+        p = resp["product"]; pid = p["id"]
+        vs = {v["option1"]: v["price"] for v in p["variants"]}
+        rec = {"dw": dw, "product_id": pid, "handle": p["handle"], "status": p["status"]}
+        led.write(json.dumps(rec) + "\n"); led.flush(); created.append(rec)
+        print(f"  OK   {dw} → id {pid} | {p['title'][:30]:<30} | Roll ${vs.get('Single Roll')} Sample ${vs.get('Sample')} | imgs {len(p['images'])} | {p['status']}")
+    else:
+        errs = resp.get("errors") or resp
+        print(f"  FAIL {dw} → HTTP {st} | {str(errs)[:120]}")
+    time.sleep(0.6)  # REST 2/sec safety
+led.close()
+print(f"\n=== created {len(created)}/{len(rows)} as DRAFT. Ledger: {LEDGER} ===")
diff --git a/romo-canary-created.jsonl b/romo-canary-created.jsonl
new file mode 100644
index 0000000..190b580
--- /dev/null
+++ b/romo-canary-created.jsonl
@@ -0,0 +1,10 @@
+{"dw": "DWRM-241249", "product_id": 7911497924659, "handle": "figura-mehndi", "status": "draft"}
+{"dw": "DWRM-240509", "product_id": 7911498121267, "handle": "picota-swedish-grey", "status": "draft"}
+{"dw": "DWRM-240741", "product_id": 7911498154035, "handle": "zenya-tawny", "status": "draft"}
+{"dw": "DWRM-241252", "product_id": 7911498252339, "handle": "onuma-moonshine", "status": "draft"}
+{"dw": "DWRM-240706", "product_id": 7911498285107, "handle": "eleni-lovat", "status": "draft"}
+{"dw": "DWRM-241256", "product_id": 7911498383411, "handle": "kuju-caper", "status": "draft"}
+{"dw": "DWRM-240605", "product_id": 7911498448947, "handle": "esai-seagrass-embossed-tundra", "status": "draft"}
+{"dw": "DWRM-240614", "product_id": 7911498481715, "handle": "ciro-abaca-embossed-oat", "status": "draft"}
+{"dw": "DWRM-240437", "product_id": 7911498547251, "handle": "kauri-stratus", "status": "draft"}
+{"dw": "DWRM-241257", "product_id": 7911498580019, "handle": "kuju-halite", "status": "draft"}

← 671aec7 Romo landing: 578-product snapshot + lexicon attribution (co  ·  back to Dw Yolo Loop  ·  Romo REPRICE dry-run: 138 live-reconciled candidates (1 belo 69ae2e6 →