← back to Dw Yolo Loop
artmura-site/romo-reprice.py
67 lines
#!/usr/bin/env python3
"""
romo-reprice.py — reprice live Romo Single Roll variants to the catalog's correct retail.
Re-fetches each product's LIVE current price (state-drift guard) before any change.
Default = DRY-RUN (no writes). --apply performs live productVariantUpdate, ledgered before->after.
Subset: --sku <dwsku> | --canary N | --all (from romo-reprice-candidates.json)
Env: SHOPIFY_ADMIN_TOKEN, STORE.
"""
import json, os, sys, time, urllib.request, urllib.error
TOKEN=os.environ['SHOPIFY_ADMIN_TOKEN']; STORE=os.environ['STORE']
API=f"https://{STORE}/admin/api/2024-10/graphql.json"
H={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"}
CANDS=os.path.join(os.path.dirname(__file__),"romo-reprice-candidates.json")
LEDGER=os.path.expanduser("~/Projects/dw-yolo-loop/romo-reprice-applied.jsonl")
def gql(q,v=None):
for a in range(4):
try:
r=urllib.request.Request(API,data=json.dumps({"query":q,"variables":v or {}}).encode(),headers=H,method="POST")
with urllib.request.urlopen(r,timeout=30) as resp: return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
if e.code==429: time.sleep(2*(a+1)); continue
return {"errors":str(e)}
return {"errors":"retries exhausted"}
def live_variant(dwsku):
q='{ productVariants(first:1, query:"sku:%s"){edges{node{id price product{id status title}}}} }'%dwsku
d=gql(q); e=d.get("data",{}).get("productVariants",{}).get("edges") if "data" in d else None
if not e: return None
n=e[0]["node"]; return {"vid":n["id"],"price":n["price"],"pid":n["product"]["id"],"status":n["product"]["status"],"title":n["product"]["title"]}
# args
apply = "--apply" in sys.argv
cands=json.load(open(CANDS))
if "--sku" in sys.argv: sel=[c for c in cands if c["dw"]==sys.argv[sys.argv.index("--sku")+1]]
elif "--canary" in sys.argv: sel=cands[:int(sys.argv[sys.argv.index("--canary")+1])]
elif "--all" in sys.argv: sel=cands
else: sel=cands[:10]
print(f"=== ROMO REPRICE {'APPLY (LIVE WRITE)' if apply else 'DRY-RUN'} — {len(sel)} SKUs ===")
print(f"{'DW SKU':<13}{'live now':>10}{'artifact':>10}{'→ new':>10}{'status':>8} note")
led=open(LEDGER,"a") if apply else None
applied=skipped=drift=0
for c in sel:
lv=live_variant(c["dw"])
if not lv: print(f"{c['dw']:<13}{'—':>10} NOT FOUND on store (skip)"); skipped+=1; continue
live_now=float(lv["price"]); new=float(c["correct_price"]); art=float(c["live_price"])
note=[]
if abs(live_now-art)>=0.01: note.append(f"drift(artifact ${art:.2f})"); drift+=1
if abs(live_now-new)<0.01: note.append("already correct");
below = c.get("cost") and live_now<float(c["cost"])
if below: note.append("BELOW-COST")
flag=""
if apply and abs(live_now-new)>=0.01:
m='mutation($id:ID!,$p:String!){productVariantUpdate(input:{id:$id,price:$p}){productVariant{price} userErrors{message}}}'
r=gql(m,{"id":lv["vid"],"p":f"{new:.2f}"})
ue=r.get("data",{}).get("productVariantUpdate",{}).get("userErrors") if "data" in r else r.get("errors")
if not ue:
led.write(json.dumps({"dw":c["dw"],"vid":lv["vid"],"before":live_now,"after":round(new,2)})+"\n"); led.flush(); applied+=1; flag="✅ written"
else: flag=f"❌ {str(ue)[:60]}"
print(f"{c['dw']:<13}${live_now:>9.2f}${art:>9.2f}${new:>9.2f}{lv['status']:>8} {' '.join(note)} {flag}")
time.sleep(0.5)
if led: led.close()
print(f"\n=== {'applied '+str(applied)+' writes' if apply else 'DRY-RUN — nothing written'} | drift:{drift} | not-found:{skipped} ===")
if apply: print(f"reversible ledger: {LEDGER}")