← 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)