← back to Dw Yolo Loop

artmura-site/romo-canary-create.py

87 lines

#!/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} ===")