← back to Eur Recrawl

restore_samples.py

74 lines

#!/usr/bin/env python3
"""
REMEDIATION — restore the Sample variant on the 5 stragglers whose sample was
dropped when a Roll variant was created on their "Title:[Default Title]" option
(Shopify replaced the sole default variant instead of adding alongside it).

Adds back "Title: Sample" @ the original memo price, tracked=false, so each
product again has both a Sample and a Roll. GATED (DRY_RUN default).
"""
import os, json, time, urllib.request

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

# sku -> original sample price (from pre-fix diagnosis)
SAMPLES = {
    "EUR-70023": "3.50", "EUR-70022": "3.50", "EUR-90168": "3.50",
    "EUR-70237": "4.25", "EUR-80735": "4.25",
}

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")
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(6):
        try:
            d = json.load(urllib.request.urlopen(req, timeout=60))
            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:10){edges{node{sku}}}}}}}"""
VCREATE = """mutation($pid:ID!,$v:[ProductVariantsBulkInput!]!){
  productVariantsBulkCreate(productId:$pid,variants:$v){userErrors{field message}}}"""

def main():
    ok = 0; errs = []
    print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: restoring {len(SAMPLES)} sample variants")
    for sku, price in SAMPLES.items():
        d = gql(FIND, {"q": "sku:" + sku})
        e = (d.get("data") or {}).get("products", {}).get("edges", [])
        if not e:
            errs.append({"sku": sku, "err": "not found"}); continue
        node = e[0]["node"]; pid = node["id"]
        have = {v["node"]["sku"] for v in node["variants"]["edges"]}
        opt_name = node["options"][0]["name"] if node["options"] else "Title"
        if sku + "-Sample" in have:
            print(f"  {sku}: sample already present — skip"); ok += 1; continue
        if DRY_RUN:
            print(f"  would restore {sku}-Sample @ ${price} on option '{opt_name}=Sample'"); ok += 1; continue
        res = gql(VCREATE, {"pid": pid, "v": [{
            "optionValues": [{"optionName": opt_name, "name": "Sample"}],
            "price": price, "inventoryItem": {"tracked": False, "sku": sku + "-Sample"},
            "inventoryPolicy": "CONTINUE"}]})
        ue = ((res.get("data") or {}).get("productVariantsBulkCreate") or {}).get("userErrors") or []
        if ue: errs.append({"sku": sku, "err": ue[:2]})
        else: ok += 1; print(f"  restored {sku}-Sample @ ${price}")
        time.sleep(0.3)
    print(f"\nDONE {'(DRY-RUN)' if DRY_RUN else ''}: ok={ok} err={len(errs)}")
    if errs: print("errs:", json.dumps(errs))

if __name__ == "__main__":
    main()