← back to Designer Wallcoverings
add generalized QH fabric reclass script (vendor-parameterized)
b46225ab054455ef1bdf84d1d55dfaf27980390e · 2026-07-07 12:37:08 -0700 · Steve
Files touched
A scripts/qh-fabric-fix.py
Diff
commit b46225ab054455ef1bdf84d1d55dfaf27980390e
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 7 12:37:08 2026 -0700
add generalized QH fabric reclass script (vendor-parameterized)
---
scripts/qh-fabric-fix.py | 102 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 102 insertions(+)
diff --git a/scripts/qh-fabric-fix.py b/scripts/qh-fabric-fix.py
new file mode 100644
index 00000000..2c6fdd31
--- /dev/null
+++ b/scripts/qh-fabric-fix.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""
+Quadrille-House fabric brands: Wallcovering -> Fabric reclassification (LIVE Shopify).
+
+Generalizes alan-campbell-fabric-fix.py to any vendor list. Same idempotent transform:
+ product_type -> Fabric; strip "Wallcovering(s)" from title before "| <Brand>";
+ drop "Wallcovering" tag / add "Fabric"; rename "Trending Wallcovering Collection 2026"
+ -> "Trending Fabric Collection 2026" (any tag containing "Wallcovering" -> "Fabric");
+ case-preserving wallcovering(s) -> fabric(s) in descriptionHtml.
+
+Usage: VENDORS="Quadrille,Home Couture,Plains" python3 qh-fabric-fix.py [--apply]
+ (default vendors = "Alan Campbell"; default mode = dry run)
+"""
+import os, sys, re, json, time, urllib.request, urllib.error
+
+STORE = os.environ["SHOPIFY_STORE_DOMAIN"]
+TOK = os.environ["SHOPIFY_ADMIN_TOKEN"]
+VER = "2024-10"
+APPLY = "--apply" in sys.argv
+VENDORS = [v.strip() for v in os.environ.get("VENDORS", "Alan Campbell").split(",") if v.strip()]
+
+def gql(query, variables=None):
+ body = json.dumps({"query": query, "variables": variables or {}}).encode()
+ for attempt in range(6):
+ req = urllib.request.Request(
+ f"https://{STORE}/admin/api/{VER}/graphql.json", data=body,
+ headers={"X-Shopify-Access-Token": TOK, "Content-Type": "application/json"})
+ try:
+ d = json.load(urllib.request.urlopen(req))
+ except urllib.error.HTTPError as e:
+ if e.code == 429: time.sleep(2*(attempt+1)); continue
+ raise
+ if d.get("errors") and any("THROTTLED" in json.dumps(x) for x in d["errors"]):
+ time.sleep(2*(attempt+1)); continue
+ return d
+ raise RuntimeError("throttled repeatedly")
+
+def scrub_prose(s):
+ if not s: return s
+ s = s.replace("Wallcoverings","Fabrics").replace("wallcoverings","fabrics")
+ return s.replace("Wallcovering","Fabric").replace("wallcovering","fabric")
+
+def fix_title(t):
+ if "|" in t:
+ head, rest = t.split("|", 1)
+ head = re.sub(r"\s*Wallcoverings?\s*$", " ", head).rstrip() + " "
+ return head + "|" + rest
+ return re.sub(r"\s*Wallcoverings?\s*$", "", t)
+
+def fix_tags(tags):
+ out = []
+ for t in tags:
+ if t == "Wallcovering": continue
+ elif "Wallcovering" in t: out.append(t.replace("Wallcovering","Fabric"))
+ else: out.append(t)
+ if "Fabric" not in out: out.append("Fabric")
+ return out
+
+Q = """query($cursor:String,$q:String!){ products(first:100, query:$q, after:$cursor){
+ pageInfo{hasNextPage endCursor}
+ edges{ node{ id title productType tags descriptionHtml } } } }"""
+M = """mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id } userErrors{ field message } } }"""
+
+grand_ok = grand_err = grand_changes = 0
+for vendor in VENDORS:
+ prods, cur = [], None
+ while True:
+ d = gql(Q, {"cursor": cur, "q": f"vendor:'{vendor}'"})
+ pg = d["data"]["products"]
+ prods += [e["node"] for e in pg["edges"]]
+ if not pg["pageInfo"]["hasNextPage"]: break
+ cur = pg["pageInfo"]["endCursor"]
+ changes = []
+ for p in prods:
+ diff = {}
+ if p["productType"] != "Fabric": diff["productType"] = "Fabric"
+ nt = fix_title(p["title"]); diff.update({} if p["title"]==nt else {"title":nt})
+ ng = fix_tags(p["tags"])
+ if sorted(p["tags"]) != sorted(ng): diff["tags"] = ng
+ nd = scrub_prose(p["descriptionHtml"])
+ if (p["descriptionHtml"] or "") != (nd or ""): diff["descriptionHtml"] = nd
+ if diff: changes.append((p, diff))
+ print(f"[{vendor}] {len(prods)} products, {len(changes)} need update")
+ grand_changes += len(changes)
+ if not APPLY:
+ continue
+ ok = err = 0
+ for i, (p, diff) in enumerate(changes, 1):
+ inp = {"id": p["id"], **diff}
+ r = gql(M, {"input": inp})
+ ue = r.get("data",{}).get("productUpdate",{}).get("userErrors") if r.get("data") else r.get("errors")
+ if ue: err += 1; print(f" ERR {p['title']}: {ue}")
+ else: ok += 1
+ if i % 25 == 0: print(f" [{vendor}] ...{i}/{len(changes)} (ok={ok} err={err})")
+ time.sleep(0.25)
+ print(f"[{vendor}] DONE ok={ok} err={err}")
+ grand_ok += ok; grand_err += err
+
+if not APPLY:
+ print(f"\nDRY RUN. total needing update: {grand_changes}. Re-run with --apply.")
+else:
+ print(f"\nALL DONE. total ok={grand_ok} err={grand_err}")
← 51a09038 cadence: QH fabric brands (Quadrille/Home Couture/Charles Bu
·
back to Designer Wallcoverings
·
deploy Thibaut srLen garble fix (890 pages) + provenance 7fa64131 →