[object Object]

← back to Reid Witlin Onboarding

Add gated rename.py: unify existing 312 live Reid Witlin DWRW-210xxx -> DWDQ-210xxx

37362612bf1bc9831b7a402be26e016d5a6453af · 2026-07-30 15:24:14 -0700 · Steve

Files touched

Diff

commit 37362612bf1bc9831b7a402be26e016d5a6453af
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 15:24:14 2026 -0700

    Add gated rename.py: unify existing 312 live Reid Witlin DWRW-210xxx -> DWDQ-210xxx
---
 rename-results.json |  32 +++++++++++++
 rename.py           | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 168 insertions(+)

diff --git a/rename-results.json b/rename-results.json
new file mode 100644
index 0000000..0388a0a
--- /dev/null
+++ b/rename-results.json
@@ -0,0 +1,32 @@
+{
+  "products": 312,
+  "variants": 312,
+  "renamed": 312,
+  "errors": [],
+  "remap_sample": [
+    {
+      "old": "DWRW-210000",
+      "new": "DWDQ-210000"
+    },
+    {
+      "old": "DWRW-210001",
+      "new": "DWDQ-210001"
+    },
+    {
+      "old": "DWRW-210002",
+      "new": "DWDQ-210002"
+    },
+    {
+      "old": "DWRW-210003",
+      "new": "DWDQ-210003"
+    },
+    {
+      "old": "DWRW-210004",
+      "new": "DWDQ-210004"
+    },
+    {
+      "old": "DWRW-210005",
+      "new": "DWDQ-210005"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/rename.py b/rename.py
new file mode 100644
index 0000000..23bd13d
--- /dev/null
+++ b/rename.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""
+Reid Witlin SKU unification — GATED live rename (Steve runs this).
+
+Renames the 315 already-live Reid Witlin products from DWRW-210xxx to
+DWDQ-210xxx (prefix swap, number preserved) so the whole line lives under the
+dedicated DWDQ prefix. Operates on LIVE variants (SKU is on the InventoryItem in
+API 2024-10), covering the product SKU AND its -Sample variant. Then mirrors the
+change into dw_unified.
+
+SAFETY:
+  * DRY_RUN=1 by default — lists every rename it WOULD do, writes nothing.
+  * status:any so ACTIVE + ARCHIVED are both caught (DELETED ones are gone → skipped).
+  * Number is preserved; only the DWRW->DWDQ prefix changes. Fully reversible.
+  * Smoke test one: DRY_RUN=0 LIMIT=1 python3 rename.py
+"""
+import os, json, time, urllib.request, subprocess
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
+LIMIT = int(os.environ.get("LIMIT", "0")) or None
+
+def _tok():
+    for line in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+        if line.startswith("SHOPIFY_ADMIN_TOKEN="):
+            return line.split("=", 1)[1].strip().strip('"')
+    raise SystemExit("SHOPIFY_ADMIN_TOKEN not found")
+
+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):
+    body = json.dumps({"query": q, "variables": v or {}}).encode()
+    req = urllib.request.Request(URL, body,
+        {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
+    for a in range(8):
+        try:
+            d = json.load(urllib.request.urlopen(req, timeout=90))
+            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!,$c:String){
+  products(first:50, query:$q, after:$c){
+    pageInfo{hasNextPage endCursor}
+    edges{node{ id title status
+      variants(first:20){edges{node{ id sku }}}
+    }}
+  }
+}"""
+
+UPDATE = """
+mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
+  productVariantsBulkUpdate(productId:$pid, variants:$variants){
+    productVariants{ id sku }
+    userErrors{ field message }
+  }
+}"""
+
+def new_sku(old):
+    # DWRW-210314 -> DWDQ-210314 ; DWRW-210314-Sample -> DWDQ-210314-Sample
+    return old.replace("DWRW-210", "DWDQ-210", 1) if old and old.startswith("DWRW-210") else old
+
+def main():
+    # gather every product with a DWRW-210xxx variant. Shopify tokenizes on '-',
+    # so 'sku:DWRW-210*' matches nothing; query the broad 'sku:DWRW*' and filter
+    # to the 210xxx band in code (this also excludes Rebel Walls' 360xxx/76xxx).
+    # Default query omits ARCHIVED, so run an explicit archived pass too.
+    targets, seen = [], set()
+    for qbase in ("sku:DWRW*", "sku:DWRW* status:archived"):
+        cur = None
+        while True:
+            d = gql(FIND, {"q": qbase, "c": cur})
+            p = d["data"]["products"]
+            for e in p["edges"]:
+                n = e["node"]
+                if n["id"] in seen:
+                    continue
+                vs = [(v["node"]["id"], v["node"]["sku"]) for v in n["variants"]["edges"]
+                      if (v["node"]["sku"] or "").startswith("DWRW-210")]
+                if vs:
+                    seen.add(n["id"])
+                    targets.append({"pid": n["id"], "title": n["title"], "status": n["status"], "variants": vs})
+            if p["pageInfo"]["hasNextPage"]:
+                cur = p["pageInfo"]["endCursor"]
+            else:
+                break
+    if LIMIT:
+        targets = targets[:LIMIT]
+    n_var = sum(len(t["variants"]) for t in targets)
+    print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: {len(targets)} products / {n_var} variants "
+          f"DWRW-210xxx -> DWDQ-210xxx")
+
+    done, errs, remap = 0, [], []
+    for i, t in enumerate(targets):
+        updates = [{"id": vid, "inventoryItem": {"sku": new_sku(old)}} for vid, old in t["variants"]]
+        for vid, old in t["variants"]:
+            remap.append({"old": old, "new": new_sku(old)})
+        if DRY_RUN:
+            if i < 4:
+                print(f"  would rename [{t['status']}] {t['title'][:30]!r}: "
+                      + ", ".join(f"{o}->{new_sku(o)}" for _, o in t["variants"]))
+            done += 1
+            continue
+        d = gql(UPDATE, {"pid": t["pid"], "variants": updates})
+        r = (d.get("data") or {}).get("productVariantsBulkUpdate") or {}
+        ue = r.get("userErrors") or []
+        if ue:
+            errs.append({"title": t["title"], "errors": ue[:2]})
+        else:
+            done += 1
+        if i % 25 == 0:
+            print(f"  ...{i}/{len(targets)} done={done} errs={len(errs)}")
+        time.sleep(0.3)
+
+    # mirror the change into dw_unified (both mirror + catalog), non-fatal
+    if not DRY_RUN and done:
+        for tbl, col in [("shopify_products", "sku"), ("rwltd_catalog", "dw_sku")]:
+            subprocess.run(["psql", "host=/tmp dbname=dw_unified", "-c",
+                f"UPDATE {tbl} SET {col}=replace({col},'DWRW-210','DWDQ-210') "
+                f"WHERE {col} LIKE 'DWRW-210%';"], capture_output=True, text=True)
+
+    out = {"products": len(targets), "variants": n_var, "renamed": done,
+           "errors": errs[:20], "remap_sample": remap[:6]}
+    json.dump(out, open(os.path.join(HERE, "rename-results.json"), "w"), indent=2)
+    print(f"\nDONE. products={done} userErrors={len(errs)} "
+          f"({'DRY-RUN — nothing written' if DRY_RUN else 'live SKUs renamed + mirror synced'})")
+    if errs:
+        print("sample errors:", json.dumps(errs[:3]))
+
+if __name__ == "__main__":
+    main()

← b6f5dcd Switch new-product SKU prefix to DWDQ (dedicated Reid Witlin  ·  back to Reid Witlin Onboarding  ·  rename.py: also strip 'Reid Witlin' PL-leak tag (5 products) b05424c →