← back to Eur Recrawl

write_panels.py

80 lines

#!/usr/bin/env python3
"""
EUR- MURAL/PANEL write — GATED (Steve runs). Adds a sellable "Complete Set — N
Panels" variant to each of the 154 sample-only mural products, priced at the
PER-SET retail = set trade_cost x1.810 (DTD 2026-07-30: sell murals per-set, not
per-panel; Cody must-fix: truthful label showing the drop count).

All 154 are sample-only (Size=[Sample]) -> every one is a CREATE. Sample $4.25
preserved. Mirrors write_reonboard.py's CREATE path but with the per-product
"Complete Set — N Panels" option value from panel_dryrun.csv.

SAFETY: DRY_RUN=1 default. Smoke-test: DRY_RUN=0 LIMIT=1 python3 write_panels.py
(verify the variant + label in admin), then the full run.
"""
import os, csv, json, time, urllib.request

HERE = os.path.dirname(os.path.abspath(__file__))
DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
LIMIT = int(os.environ.get("LIMIT", "0")) or None

def _tok():
    for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
        if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=", 1)[1].strip().strip('"')
    raise SystemExit("token not found")
TOKEN = os.environ.get("AT") or _tok()
URL = "https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json"

def gql(q, v=None):
    b = json.dumps({"query": q, "variables": v or {}}).encode()
    req = urllib.request.Request(URL, b, {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
    for a in range(8):
        try:
            d = json.load(urllib.request.urlopen(req, timeout=90))
            if "errors" in d and any("THROTTLED" in str(e) for e in d["errors"]):
                time.sleep(2 * (a + 1)); continue
            return d
        except Exception:
            time.sleep(2 * (a + 1))
    raise RuntimeError("gql failed")

FIND = """query($q:String!){products(first:1,query:$q){edges{node{id
  options{name values} variants(first:15){edges{node{sku price}}}}}}}"""
VCREATE = """mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){
  productVariantsBulkCreate(productId:$pid,variants:$v){userErrors{field message}}}"""

def main():
    rows = list(csv.DictReader(open(os.path.join(HERE, "panel_dryrun.csv"))))
    if LIMIT: rows = rows[:LIMIT]
    created = skipped = 0; errs = []
    print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: {len(rows)} mural set-variants @ set trade x1.810")
    for i, r in enumerate(rows):
        roll_sku = r["roll_sku"]; retail = float(r["retail_x181"]); label = r["recommended_variant_label"]
        d = gql(FIND, {"q": "sku:" + roll_sku})
        e = (d.get("data") or {}).get("products", {}).get("edges", [])
        if not e:
            errs.append({"sku": roll_sku, "err": "not found"}); continue
        node = e[0]["node"]; pid = node["id"]
        skus = {v["node"]["sku"] for v in node["variants"]["edges"]}
        if roll_sku in skus:
            skipped += 1; continue  # set variant already exists
        if DRY_RUN:
            if i < 6: print(f"  CREATE {roll_sku}: '{label}' @ ${retail:.2f}  ({r['title'][:34]})")
            created += 1; continue
        res = gql(VCREATE, {"pid": pid, "v": [{
            "optionValues": [{"optionName": "Size", "name": label}],
            "price": f"{retail:.2f}", "inventoryItem": {"tracked": False, "sku": roll_sku},
            "inventoryPolicy": "CONTINUE"}]})
        ue = ((res.get("data") or {}).get("productVariantsBulkCreate") or {}).get("userErrors") or []
        if ue: errs.append({"sku": roll_sku, "err": ue[:2]})
        else: created += 1
        if i % 20 == 0: print(f"  ...{i}/{len(rows)} created={created} err={len(errs)}")
        time.sleep(0.3)
    json.dump({"created": created, "skipped": skipped, "errors": errs[:25]},
              open(os.path.join(HERE, "panel-write-results.json"), "w"), indent=2)
    print(f"\nDONE {'(DRY-RUN)' if DRY_RUN else ''}: CREATE={created} skip={skipped} err={len(errs)}")
    if errs: print("errs:", json.dumps(errs[:3]))

if __name__ == "__main__":
    main()