← 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} ===")