[object Object]

← back to Designer Wallcoverings

Alan Campbell: reclassify wallcovering->fabric (type/title/tags/desc)

55858e6432a8dbc7d59facb0fe2e4cae4477f7dc · 2026-07-07 10:06:11 -0700 · Steve

Files touched

Diff

commit 55858e6432a8dbc7d59facb0fe2e4cae4477f7dc
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 10:06:11 2026 -0700

    Alan Campbell: reclassify wallcovering->fabric (type/title/tags/desc)
---
 scripts/alan-campbell-fabric-fix.py | 136 ++++++++++++++++++++++++++++++++++++
 1 file changed, 136 insertions(+)

diff --git a/scripts/alan-campbell-fabric-fix.py b/scripts/alan-campbell-fabric-fix.py
new file mode 100644
index 00000000..9d0479c3
--- /dev/null
+++ b/scripts/alan-campbell-fabric-fix.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""
+Alan Campbell: Wallcovering -> Fabric reclassification (LIVE Shopify).
+
+Steve's rule: all Alan Campbell products are FABRICS, not wallcoverings.
+A prior migration half-finished this (flipped 121 type+title but left tags dirty;
+never touched the other 246). This does the complete, idempotent fix:
+
+  1. product_type      Wallcovering -> Fabric
+  2. title             "<Pattern> Wallcovering(s) | Alan Campbell" -> "<Pattern> | Alan Campbell"
+  3. tags              drop "Wallcovering", add "Fabric",
+                       "Trending Wallcovering Collection 2026" -> "Trending Fabric Collection 2026"
+                       (any tag containing "Wallcovering" -> "Fabric")
+  4. descriptionHtml   case-preserving  wallcovering(s) -> fabric(s)
+
+Idempotent: recompute from current state each run; only PATCH what actually changes.
+Usage:  python3 alan-campbell-fabric-fix.py [--apply]   (default = dry run)
+"""
+import os, sys, re, json, time, urllib.request
+
+STORE = os.environ["SHOPIFY_STORE_DOMAIN"]
+TOK   = os.environ["SHOPIFY_ADMIN_TOKEN"]
+VER   = "2024-10"
+APPLY = "--apply" in sys.argv
+
+def gql(query, variables=None):
+    body = json.dumps({"query": query, "variables": variables or {}}).encode()
+    req = urllib.request.Request(
+        f"https://{STORE}/admin/api/{VER}/graphql.json", data=body,
+        headers={"X-Shopify-Access-Token": TOK, "Content-Type": "application/json"})
+    for attempt in range(6):
+        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")
+    s = s.replace("Wallcovering",  "Fabric").replace("wallcovering",  "fabric")
+    return s
+
+def fix_title(t):
+    # strip a trailing "Wallcovering(s)" from the segment before the first "|"
+    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                      # dropped; Fabric added below
+        elif "Wallcovering" in t:
+            out.append(t.replace("Wallcovering", "Fabric"))
+        else:
+            out.append(t)
+    if "Fabric" not in out:
+        out.append("Fabric")
+    return out
+
+# ---- fetch all Alan Campbell products ----
+Q = """query($cursor:String){ products(first:100, query:"vendor:'Alan Campbell'", after:$cursor){
+  pageInfo{hasNextPage endCursor}
+  edges{ node{ id title productType tags descriptionHtml } } } }"""
+prods, cur = [], None
+while True:
+    d = gql(Q, {"cursor": cur})
+    pg = d["data"]["products"]
+    prods += [e["node"] for e in pg["edges"]]
+    if not pg["pageInfo"]["hasNextPage"]: break
+    cur = pg["pageInfo"]["endCursor"]
+print(f"fetched {len(prods)} Alan Campbell products")
+
+# ---- compute diffs ----
+changes = []
+for p in prods:
+    new_type  = "Fabric"
+    new_title = fix_title(p["title"])
+    new_tags  = fix_tags(p["tags"])
+    new_desc  = scrub_prose(p["descriptionHtml"])
+    diff = {}
+    if p["productType"] != new_type: diff["productType"] = new_type
+    if p["title"] != new_title:      diff["title"] = new_title
+    if sorted(p["tags"]) != sorted(new_tags): diff["tags"] = new_tags
+    if (p["descriptionHtml"] or "") != (new_desc or ""): diff["descriptionHtml"] = new_desc
+    if diff:
+        changes.append((p, diff))
+
+print(f"products needing update: {len(changes)} / {len(prods)}")
+from collections import Counter
+fields = Counter(k for _, d in changes for k in d)
+print("field change counts:", dict(fields))
+print("\n--- sample diffs ---")
+for p, d in changes[:4]:
+    print(f"  {p['title']!r}")
+    if "title" in d: print(f"      title -> {d['title']!r}")
+    if "productType" in d: print(f"      type  {p['productType']} -> {d['productType']}")
+    if "tags" in d:
+        rm = set(p["tags"]) - set(d["tags"]); add = set(d["tags"]) - set(p["tags"])
+        print(f"      tags  -{sorted(rm)} +{sorted(add)}")
+    if "descriptionHtml" in d: print(f"      desc  scrubbed wallcovering->fabric")
+    print()
+
+if not APPLY:
+    print("DRY RUN. Re-run with --apply to write to the LIVE store.")
+    sys.exit(0)
+
+# ---- apply ----
+M = """mutation($input:ProductInput!){ productUpdate(input:$input){
+  product{ id } userErrors{ field message } } }"""
+ok = err = 0
+for i, (p, d) in enumerate(changes, 1):
+    inp = {"id": p["id"]}
+    if "productType" in d: inp["productType"] = d["productType"]
+    if "title" in d: inp["title"] = d["title"]
+    if "tags" in d: inp["tags"] = d["tags"]
+    if "descriptionHtml" in d: inp["descriptionHtml"] = d["descriptionHtml"]
+    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"  [{i}] ERR {p['title']}: {ue}")
+    else:
+        ok += 1
+    if i % 25 == 0:
+        print(f"  ...{i}/{len(changes)} (ok={ok} err={err})")
+    time.sleep(0.25)
+print(f"\nDONE. updated ok={ok} err={err} of {len(changes)}")

← e4711d86 Parameterize room push (CATALOG/VIEW) — serves Carl Robinson  ·  back to Designer Wallcoverings  ·  enrich: replace LLM naming with deterministic ~115-color int 75188a05 →