← back to Dw Yolo Loop
artmura-site/romo-batch-create.py
76 lines
#!/usr/bin/env python3
"""
romo-batch-create.py — create the full remaining Romo true-new set on the LIVE store as DRAFT.
Same structure/guards as the canary; resumable (skips SKUs that already exist), ledgered,
throttled, progress every 25. Activation stays a SEPARATE gate.
Env: SHOPIFY_ADMIN_TOKEN, STORE, INPUT (jsonl), LEDGER (jsonl).
"""
import json, os, time, urllib.request, urllib.error
TOKEN=os.environ['SHOPIFY_ADMIN_TOKEN']; STORE=os.environ['STORE']
INPUT=os.environ['INPUT']; LEDGER=os.environ['LEDGER']
API=f"https://{STORE}/admin/api/2024-10"
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)
for attempt in range(4):
try:
with urllib.request.urlopen(r,timeout=30) as resp:
return resp.status, json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
if e.code==429: time.sleep(2*(attempt+1)); continue
return e.code, json.loads(e.read().decode() or '{}')
except Exception as ex:
time.sleep(1.5); continue
return 0, {"errors":"retries exhausted"}
def sku_exists(sku):
_,d=req("POST",f"{API}/graphql.json",{"query":"{ productVariants(first:1, query:\"sku:%s\"){edges{node{id}}} }"%sku})
return bool(d.get("data",{}).get("productVariants",{}).get("edges"))
def nl(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):
dw=row['dw']; title=f"{row['pattern']} {row['color']}".strip()
tags=list(dict.fromkeys(["Romo","Wallcovering","display_variant","Priced Per Single Roll",
*nl(row.get('styles')),*nl(row.get('pats')),*nl(row.get('tags'))]))
ai=row.get('all_images')
imgs=[u.strip() for u in ai.split('|') if u.strip().startswith('http')] if isinstance(ai,str) and '|' in ai else nl(ai)
if not imgs and row.get('img'): imgs=[row['img']]
images=[{"src":u} for u in imgs][:6]
return {"product":{"title":title,"vendor":"Romo","product_type":row.get('type') or "Wallcovering",
"status":"draft","body_html":(f"<p>{row['desc']}</p>" if row.get('desc') else ""),"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(INPUT) if l.strip()]
done={json.loads(l)['dw'] for l in open(LEDGER)} if os.path.exists(LEDGER) else set()
led=open(LEDGER,"a")
created=skipped=failed=0
print(f"=== ROMO BATCH CREATE — {len(rows)} products, DRAFT (already ledgered: {len(done)}) ===",flush=True)
for i,row in enumerate(rows,1):
dw=row['dw']
if dw in done: skipped+=1; continue
if sku_exists(dw):
led.write(json.dumps({"dw":dw,"skipped":"exists"})+"\n"); led.flush(); skipped+=1
else:
st,resp=req("POST",f"{API}/products.json",build(row))
if st in (200,201) and resp.get("product"):
p=resp["product"]; led.write(json.dumps({"dw":dw,"product_id":p["id"],"handle":p["handle"],"status":p["status"]})+"\n"); led.flush(); created+=1
else:
led.write(json.dumps({"dw":dw,"FAIL":str(resp.get('errors') or resp)[:150]})+"\n"); led.flush(); failed+=1
if i%25==0: print(f" [{i}/{len(rows)}] created={created} skipped={skipped} failed={failed}",flush=True)
time.sleep(0.55)
led.close()
print(f"\n=== DONE: created={created} skipped={skipped} failed={failed} of {len(rows)} → {LEDGER} ===",flush=True)