[object Object]

← back to Designer Wallcoverings

add generalized QH fabric reclass script (vendor-parameterized)

b46225ab054455ef1bdf84d1d55dfaf27980390e · 2026-07-07 12:37:08 -0700 · Steve

Files touched

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 →