← back to Designer Wallcoverings
add collection dedup script (backup+delete+301 redirect+retitle)
3e87194c99cca2dae85a9af9ecb7c3b4c17261c0 · 2026-07-07 13:15:44 -0700 · Steve
Files touched
A scripts/dedup-collections.py
Diff
commit 3e87194c99cca2dae85a9af9ecb7c3b4c17261c0
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jul 7 13:15:44 2026 -0700
add collection dedup script (backup+delete+301 redirect+retitle)
---
scripts/dedup-collections.py | 87 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/scripts/dedup-collections.py b/scripts/dedup-collections.py
new file mode 100644
index 00000000..2ee8d047
--- /dev/null
+++ b/scripts/dedup-collections.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""
+Dedup DW collections (Steve-approved "Full" 2026-07-07).
+Keep the most-populated collection per duplicate title; delete the redundant ones;
+301-redirect each removed handle -> the keeper so no URL 404s. Plus 5 non-destructive
+retitles for the type-split / title-bug collections.
+
+SAFETY: backs up every deleted collection's full definition (title/handle/rules/etc)
+to backups/deleted-collections-<ts>.json BEFORE deleting, so any delete is recreatable.
+Order per item: backup -> collectionDelete -> urlRedirectCreate (path free only after delete).
+
+Usage: python3 dedup-collections.py [--apply] (default = dry run)
+"""
+import os, sys, 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
+def gql(q,v=None):
+ for a in range(6):
+ req=urllib.request.Request(f"https://{STORE}/admin/api/{VER}/graphql.json",
+ data=json.dumps({"query":q,"variables":v or {}}).encode(),
+ 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*(a+1)); continue
+ raise
+ if d.get("errors") and any("THROTTLED" in json.dumps(x) for x in d["errors"]): time.sleep(2*(a+1)); continue
+ return d
+ raise RuntimeError("throttled")
+
+plan=json.load(open("/tmp/dedup-plan.json"))
+# collect deletions: (del_id, del_handle, keep_handle)
+dels=[]
+for title,keep_id,keep_handle,dlist in plan["plan"]:
+ for del_id,del_handle in dlist:
+ dels.append((title,del_id,del_handle,keep_handle))
+print(f"{len(dels)} deletions planned; {len(plan['retitles'])} retitles.")
+if not APPLY:
+ for t,di,dh,kh in dels: print(f" DELETE {dh} -> 301 {dh} to {kh} [{t}]")
+ print("DRY RUN."); sys.exit(0)
+
+# 1) BACKUP full defs of all deletions
+FULL="""query($id:ID!){ collection(id:$id){ id title handle descriptionHtml sortOrder templateSuffix
+ ruleSet{ appliedDisjunctively rules{ column relation condition } } } }"""
+backup=[]
+for t,di,dh,kh in dels:
+ d=gql(FULL,{"id":di}); backup.append(d.get("data",{}).get("collection"))
+import subprocess
+os.makedirs("backups",exist_ok=True)
+bpath=f"backups/deleted-collections-20260707.json"
+json.dump({"deleted":backup,"plan":plan}, open(bpath,"w"), indent=2)
+print(f"backed up {len(backup)} defs -> {bpath}")
+
+# 2) delete + redirect
+DEL="mutation($id:ID!){ collectionDelete(input:{id:$id}){ deletedCollectionId userErrors{message} } }"
+RED="""mutation($r:UrlRedirectInput!){ urlRedirectCreate(urlRedirect:$r){ urlRedirect{id} userErrors{message} } }"""
+dok=derr=rok=rerr=0
+for t,di,dh,kh in dels:
+ r=gql(DEL,{"id":di})
+ ue=r.get("data",{}).get("collectionDelete",{}).get("userErrors") if r.get("data") else r.get("errors")
+ if ue: derr+=1; print(f" DEL ERR {dh}: {ue}"); continue
+ dok+=1
+ rr=gql(RED,{"r":{"path":f"/collections/{dh}","target":f"/collections/{kh}"}})
+ rue=rr.get("data",{}).get("urlRedirectCreate",{}).get("userErrors") if rr.get("data") else rr.get("errors")
+ if rue: rerr+=1; print(f" REDIR ERR {dh}->{kh}: {rue}")
+ else: rok+=1
+ time.sleep(0.3)
+print(f"deletes ok={dok} err={derr} | redirects ok={rok} err={rerr}")
+
+# 3) retitles
+UPD="mutation($input:CollectionInput!){ collectionUpdate(input:{id:$id,title:$title}){ userErrors{message} } }"
+UPD="""mutation($id:ID!,$title:String!){ collectionUpdate(input:{id:$id,title:$title}){ collection{id title} userErrors{message} } }"""
+# resolve retitle ids by handle
+BYH="""query($q:String!){ collections(first:1, query:$q){ edges{ node{ id handle title } } } }"""
+tok=terr=0
+for desc,handle,newt in plan["retitles"]:
+ q=gql(BYH,{"q":f"handle:{handle}"})
+ edges=q.get("data",{}).get("collections",{}).get("edges",[])
+ node=next((e["node"] for e in edges if e["node"]["handle"]==handle), None)
+ if not node: terr+=1; print(f" RETITLE skip (not found): {handle}"); continue
+ r=gql(UPD,{"id":node["id"],"title":newt})
+ ue=r.get("data",{}).get("collectionUpdate",{}).get("userErrors") if r.get("data") else r.get("errors")
+ if ue: terr+=1; print(f" RETITLE ERR {handle}: {ue}")
+ else: tok+=1
+ time.sleep(0.3)
+print(f"retitles ok={tok} err={terr}")
+print(f"\nDONE. deleted={dok} redirected={rok} retitled={tok} (errs: del={derr} red={rerr} ret={terr})")
← 904dc48f add color/motif Wallcovering-collection rename script (DTD v
·
back to Designer Wallcoverings
·
dedup v2: no-redirect mode, keeper adopts shortest-clean han 5579a5c4 →