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