← back to Eur Recrawl
REMEDIATION: restore_samples.py — the 5 default-Title stragglers lost their Sample when the Roll variant replaced the sole default variant; restore Sample @ original price; also fixed write_reonboard option-name logic
86d80b8e1ddbcc7cd59fd7b61312b2c485aed52f · 2026-07-30 20:17:23 -0700 · Steve
Files touched
A restore_samples.pyM write-results.json
Diff
commit 86d80b8e1ddbcc7cd59fd7b61312b2c485aed52f
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 30 20:17:23 2026 -0700
REMEDIATION: restore_samples.py — the 5 default-Title stragglers lost their Sample when the Roll variant replaced the sole default variant; restore Sample @ original price; also fixed write_reonboard option-name logic
---
restore_samples.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++
write-results.json | 76 +++---------------------------------------------------
2 files changed, 76 insertions(+), 73 deletions(-)
diff --git a/restore_samples.py b/restore_samples.py
new file mode 100644
index 0000000..e578b05
--- /dev/null
+++ b/restore_samples.py
@@ -0,0 +1,73 @@
+#!/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()
diff --git a/write-results.json b/write-results.json
index c0c628e..735d899 100644
--- a/write-results.json
+++ b/write-results.json
@@ -1,81 +1,11 @@
{
- "updated": 324,
- "created": 1042,
- "skipped": 38,
+ "updated": 0,
+ "created": 5,
+ "skipped": 1404,
"errors": [
- {
- "sku": "EUR-70023",
- "err": [
- {
- "field": [
- "variants",
- "0",
- "optionValues",
- "0"
- ],
- "message": "Option does not exist"
- }
- ]
- },
- {
- "sku": "EUR-70022",
- "err": [
- {
- "field": [
- "variants",
- "0",
- "optionValues",
- "0"
- ],
- "message": "Option does not exist"
- }
- ]
- },
- {
- "sku": "EUR-90168",
- "err": [
- {
- "field": [
- "variants",
- "0",
- "optionValues",
- "0"
- ],
- "message": "Option does not exist"
- }
- ]
- },
- {
- "sku": "EUR-70237",
- "err": [
- {
- "field": [
- "variants",
- "0",
- "optionValues",
- "0"
- ],
- "message": "Option does not exist"
- }
- ]
- },
{
"sku": "EUR-90364",
"err": "product not found"
- },
- {
- "sku": "EUR-80735",
- "err": [
- {
- "field": [
- "variants",
- "0",
- "optionValues",
- "0"
- ],
- "message": "Option does not exist"
- }
- ]
}
]
}
\ No newline at end of file
← 35f0725 Fix CREATE option-name (use product's actual option, not har
·
back to Eur Recrawl
·
chore: session-close — syntax-clean + version bump v0.1.0 -> 83fd205 →