[object Object]

← back to Dw Wallpaper Cleanup

auto-save: 2026-06-22T12:05:49 (4 files) — job2_done_Apartment_Wallpaper.json job2_done_Wallpaper_NYC.json job2_finish.py job2_finish_results.json

fbb807537f039caf85e04bd830b61f53b58d2329 · 2026-06-22 12:05:52 -0700 · Steve Abrams

Files touched

Diff

commit fbb807537f039caf85e04bd830b61f53b58d2329
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 22 12:05:52 2026 -0700

    auto-save: 2026-06-22T12:05:49 (4 files) — job2_done_Apartment_Wallpaper.json job2_done_Wallpaper_NYC.json job2_finish.py job2_finish_results.json
---
 job2_done_Apartment_Wallpaper.json |  62 ++++++++++++
 job2_done_Wallpaper_NYC.json       |  49 ++++++++++
 job2_finish.py                     | 187 +++++++++++++++++++++++++++++++++++++
 job2_finish_results.json           |  51 ++++++++++
 4 files changed, 349 insertions(+)

diff --git a/job2_done_Apartment_Wallpaper.json b/job2_done_Apartment_Wallpaper.json
new file mode 100644
index 0000000..2e8393a
--- /dev/null
+++ b/job2_done_Apartment_Wallpaper.json
@@ -0,0 +1,62 @@
+{
+  "old": "Apartment Wallpaper",
+  "new": "Apartment Wallcoverings",
+  "before": {
+    "old_count": 0,
+    "old_count_all": 62,
+    "new_count": 39,
+    "new_count_all": 39,
+    "colls": [
+      {
+        "id": "gid://shopify/Collection/298970972211",
+        "title": "Apartment Wallcovering",
+        "count": 38
+      },
+      {
+        "id": "gid://shopify/Collection/299533828147",
+        "title": "Apartment Wallcovering",
+        "count": 38
+      }
+    ]
+  },
+  "rule_actions": [
+    {
+      "coll": "Apartment Wallcovering",
+      "action": "ALREADY_NEW"
+    },
+    {
+      "coll": "Apartment Wallcovering",
+      "action": "ALREADY_NEW"
+    }
+  ],
+  "pg": "OK",
+  "to_rename": 62,
+  "renamed": 62,
+  "rename_errors": [],
+  "after": {
+    "old_count": 0,
+    "old_count_all": 0,
+    "new_count": 39,
+    "new_count_all": 101,
+    "colls": [
+      {
+        "id": "gid://shopify/Collection/298970972211",
+        "count": 74
+      },
+      {
+        "id": "gid://shopify/Collection/299533828147",
+        "count": 76
+      }
+    ]
+  },
+  "floors": {
+    "gid://shopify/Collection/298970972211": 38,
+    "gid://shopify/Collection/299533828147": 38
+  },
+  "verify": {
+    "old_zero_all": true,
+    "coll_not_dropped": true,
+    "new_increased": true
+  },
+  "status": "OK"
+}
\ No newline at end of file
diff --git a/job2_done_Wallpaper_NYC.json b/job2_done_Wallpaper_NYC.json
new file mode 100644
index 0000000..dd190b2
--- /dev/null
+++ b/job2_done_Wallpaper_NYC.json
@@ -0,0 +1,49 @@
+{
+  "old": "Wallpaper NYC",
+  "new": "Wallcovering NYC",
+  "before": {
+    "old_count": 12,
+    "old_count_all": 95,
+    "new_count": 0,
+    "new_count_all": 0,
+    "colls": [
+      {
+        "id": "gid://shopify/Collection/298971430963",
+        "title": "Wallcovering NYC",
+        "count": 95
+      }
+    ]
+  },
+  "rule_actions": [
+    {
+      "coll": "Wallcovering NYC",
+      "action": "RULE_SWAPPED",
+      "errors": []
+    }
+  ],
+  "pg": "OK",
+  "to_rename": 95,
+  "renamed": 95,
+  "rename_errors": [],
+  "after": {
+    "old_count": 0,
+    "old_count_all": 0,
+    "new_count": 12,
+    "new_count_all": 95,
+    "colls": [
+      {
+        "id": "gid://shopify/Collection/298971430963",
+        "count": 71
+      }
+    ]
+  },
+  "floors": {
+    "gid://shopify/Collection/298971430963": 95
+  },
+  "verify": {
+    "old_zero_all": true,
+    "coll_not_dropped": false,
+    "new_increased": true
+  },
+  "status": "CHECK"
+}
\ No newline at end of file
diff --git a/job2_finish.py b/job2_finish.py
new file mode 100644
index 0000000..f19b429
--- /dev/null
+++ b/job2_finish.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+"""JOB 2 COMPLETION — idempotent finisher for the 3 coupled vendor renames.
+Synchronous. PG-first mirror to dw_unified.shopify_products, then Shopify.
+ACTIVE products only. Vendor edits create NO variants.
+
+Per-vendor logic (idempotent):
+  1. Capture LIVE before: old vendor count, new vendor count, each coupled
+     collection's productsCount + ruleSet.
+  2. For each coupled collection: ensure a VENDOR EQUALS <new> rule exists.
+     - If a VENDOR EQUALS <old> rule exists, REPLACE it with <new>.
+     - If neither old nor new exists but disjunctive (Malibu case where new
+       already present), leave as-is.
+     This guarantees membership is preserved at all times.
+  3. PG mirror UPDATE old->new for ACTIVE rows.
+  4. Rename each remaining ACTIVE product old->new via productUpdate, rate-limited.
+  5. Verify after: old vendor -> 0, collection count did not drop below the
+     captured floor.
+"""
+import json, sys, time, subprocess, urllib.request, urllib.error
+
+TOKEN = subprocess.check_output(
+    "grep -E '^SHOPIFY_ADMIN_TOKEN=' ~/Projects/secrets-manager/.env | cut -d= -f2",
+    shell=True, text=True).strip()
+DOMAIN = "designer-laboratory-sandbox.myshopify.com"
+URL = f"https://{DOMAIN}/admin/api/2024-10/graphql.json"
+
+PLAN = [
+    {"old": "Malibu Wallpaper", "new": "Malibu Wallcoverings",
+     "colls": ["gid://shopify/Collection/298507403315"]},
+    {"old": "Apartment Wallpaper", "new": "Apartment Wallcoverings",
+     "colls": ["gid://shopify/Collection/298970972211",
+               "gid://shopify/Collection/299533828147"]},
+    {"old": "Wallpaper NYC", "new": "Wallcovering NYC",
+     "colls": ["gid://shopify/Collection/298971430963"]},
+]
+
+def gql(q, v=None):
+    body={"query":q}
+    if v is not None: body["variables"]=v
+    req=urllib.request.Request(URL,data=json.dumps(body).encode(),
+        headers={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"})
+    for a in range(8):
+        try:
+            d=json.load(urllib.request.urlopen(req,timeout=60))
+            cost=d.get("extensions",{}).get("cost",{}).get("throttleStatus",{})
+            if cost.get("currentlyAvailable",1000) < 300:
+                time.sleep(2)
+            return d
+        except urllib.error.HTTPError as e:
+            if e.code==429: time.sleep(3*(a+1)); continue
+            time.sleep(2*(a+1))
+        except Exception: time.sleep(2*(a+1))
+    raise RuntimeError("gql failed")
+
+def vcount(v):
+    return gql('{ productsCount(query:%s){count} }'%json.dumps('status:active AND vendor:"%s"'%v))["data"]["productsCount"]["count"]
+
+def vcount_all(v):
+    return gql('{ productsCount(query:%s){count} }'%json.dumps('vendor:"%s"'%v))["data"]["productsCount"]["count"]
+
+def coll_state(gid):
+    q='{ collection(id:"%s"){ id title productsCount{count} ruleSet{ appliedDisjunctively rules{column relation condition} } } }'%gid
+    return gql(q)["data"]["collection"]
+
+PROD_UPDATE="""mutation($input:ProductInput!){ productUpdate(input:$input){ product{id vendor} userErrors{field message} } }"""
+COLL_UPDATE="""mutation($input:CollectionInput!){ collectionUpdate(input:$input){ collection{id title} userErrors{field message} } }"""
+
+def ensure_vendor_rule(gid, old, new):
+    """Make sure collection has VENDOR EQUALS <new>; replace VENDOR EQUALS <old> if present.
+    Returns action taken."""
+    c=coll_state(gid)
+    rs=c.get("ruleSet")
+    if not rs:
+        return {"coll":gid,"action":"NO_RULESET_SKIP"}
+    rules=rs["rules"]
+    has_new=any(r["column"]=="VENDOR" and r["relation"]=="EQUALS" and r["condition"]==new for r in rules)
+    has_old=any(r["column"]=="VENDOR" and r["relation"]=="EQUALS" and r["condition"]==old for r in rules)
+    if has_new and not has_old:
+        return {"coll":c["title"],"action":"ALREADY_NEW"}
+    new_rules=[]
+    for r in rules:
+        if r["column"]=="VENDOR" and r["relation"]=="EQUALS" and r["condition"]==old:
+            if not has_new:
+                new_rules.append({"column":"VENDOR","relation":"EQUALS","condition":new})
+            # if has_new already, just drop the old one (dedup)
+        else:
+            new_rules.append({"column":r["column"],"relation":r["relation"],"condition":r["condition"]})
+    inp={"id":gid,"ruleSet":{"appliedDisjunctively":rs["appliedDisjunctively"],"rules":new_rules}}
+    res=gql(COLL_UPDATE,{"input":inp})["data"]["collectionUpdate"]
+    return {"coll":c["title"],"action":"RULE_SWAPPED" if not res["userErrors"] else "RULE_ERROR",
+            "errors":res["userErrors"]}
+
+def fetch_all_status(vendor):
+    """ALL statuses (active+draft+archived) — DTD verdict A: banned word is
+    catalog-wide. Vendor edit keeps status unchanged + creates no variants."""
+    out=[]; cursor=None
+    while True:
+        after=', after:"%s"'%cursor if cursor else ''
+        q=('{ products(first:250, query:%s%s){ pageInfo{hasNextPage endCursor} edges{node{id title vendor status}} } }')%(
+            json.dumps('vendor:"%s"'%vendor), after)
+        d=gql(q)["data"]["products"]
+        for e in d["edges"]:
+            n=e["node"]
+            if n["vendor"]==vendor:
+                out.append(n["id"])
+        if not d["pageInfo"]["hasNextPage"]: break
+        cursor=d["pageInfo"]["endCursor"]
+    return out
+
+def mirror_pg(old,new):
+    """All statuses — match the all-status Shopify rename."""
+    try:
+        subprocess.run(["psql","-d","dw_unified","-v","ON_ERROR_STOP=1","-c",
+            "UPDATE shopify_products SET vendor=$$%s$$ WHERE vendor=$$%s$$;"%(new,old)],
+            check=True, capture_output=True, text=True)
+        return "OK"
+    except subprocess.CalledProcessError as e:
+        return "PG_ERR: "+(e.stderr or "")[:200]
+    except Exception as e:
+        return "PG_ERR: "+str(e)[:200]
+
+def run_vendor(p, gap=0.5):
+    old,new=p["old"],p["new"]
+    rec={"old":old,"new":new}
+    rec["before"]={"old_count":vcount(old),"old_count_all":vcount_all(old),
+                   "new_count":vcount(new),"new_count_all":vcount_all(new),
+                   "colls":[{"id":g, **{k:coll_state(g).get(k) for k in ("title",)},
+                             "count":coll_state(g)["productsCount"]["count"]} for g in p["colls"]]}
+    # capture collection floors
+    floors={g:coll_state(g)["productsCount"]["count"] for g in p["colls"]}
+    sys.stderr.write(f"[{old}] before: old={rec['before']['old_count']} new={rec['before']['new_count']} floors={floors}\n")
+
+    # 1) ensure rules
+    rec["rule_actions"]=[]
+    for g in p["colls"]:
+        a=ensure_vendor_rule(g, old, new)
+        rec["rule_actions"].append(a)
+        sys.stderr.write(f"  rule {g}: {a}\n")
+        time.sleep(0.4)
+
+    # 2) PG mirror
+    rec["pg"]=mirror_pg(old,new)
+    sys.stderr.write(f"  pg mirror: {rec['pg']}\n")
+
+    # 3) rename products (all statuses — DTD verdict A)
+    ids=fetch_all_status(old)
+    rec["to_rename"]=len(ids)
+    renamed=0; errors=[]
+    for i,pid in enumerate(ids):
+        res=gql(PROD_UPDATE,{"input":{"id":pid,"vendor":new}})["data"]["productUpdate"]
+        if res["userErrors"]:
+            errors.append({"id":pid,"errs":res["userErrors"]})
+        else:
+            renamed+=1
+        time.sleep(gap)
+        if (i+1)%50==0:
+            sys.stderr.write(f"    {old}: {i+1}/{len(ids)} renamed\n")
+    rec["renamed"]=renamed; rec["rename_errors"]=errors
+
+    # 4) verify
+    time.sleep(3)
+    after={"old_count":vcount(old),"old_count_all":vcount_all(old),
+           "new_count":vcount(new),"new_count_all":vcount_all(new),
+           "colls":[{"id":g,"count":coll_state(g)["productsCount"]["count"]} for g in p["colls"]]}
+    rec["after"]=after
+    rec["floors"]=floors
+    coll_ok=all(after["colls"][i]["count"]>=floors[p["colls"][i]] for i in range(len(p["colls"])))
+    rec["verify"]={"old_zero_all":after["old_count_all"]==0,"coll_not_dropped":coll_ok,
+                   "new_increased":after["new_count_all"]>=rec["before"]["new_count_all"]}
+    rec["status"]="OK" if (after["old_count_all"]==0 and coll_ok and not errors) else "CHECK"
+    sys.stderr.write(f"[{old}] after: old_all={after['old_count_all']} new_all={after['new_count_all']} colls={after['colls']} status={rec['status']}\n")
+    return rec
+
+if __name__=="__main__":
+    only=sys.argv[1] if len(sys.argv)>1 else None
+    results=[]
+    for p in PLAN:
+        if only and p["old"]!=only: continue
+        r=run_vendor(p)
+        results.append(r)
+        json.dump(r, open(f"job2_done_{p['old'].replace(' ','_')}.json","w"), indent=2)
+    json.dump(results, open("job2_finish_results.json","w"), indent=2)
+    print("\n=== SUMMARY ===")
+    for r in results:
+        print(f"{r['old']} -> {r['new']}: status={r['status']} renamed={r['renamed']} "
+              f"old_after={r['after']['old_count']} new_after={r['after']['new_count']} "
+              f"colls={[c['count'] for c in r['after']['colls']]}")
diff --git a/job2_finish_results.json b/job2_finish_results.json
new file mode 100644
index 0000000..71efd95
--- /dev/null
+++ b/job2_finish_results.json
@@ -0,0 +1,51 @@
+[
+  {
+    "old": "Wallpaper NYC",
+    "new": "Wallcovering NYC",
+    "before": {
+      "old_count": 12,
+      "old_count_all": 95,
+      "new_count": 0,
+      "new_count_all": 0,
+      "colls": [
+        {
+          "id": "gid://shopify/Collection/298971430963",
+          "title": "Wallcovering NYC",
+          "count": 95
+        }
+      ]
+    },
+    "rule_actions": [
+      {
+        "coll": "Wallcovering NYC",
+        "action": "RULE_SWAPPED",
+        "errors": []
+      }
+    ],
+    "pg": "OK",
+    "to_rename": 95,
+    "renamed": 95,
+    "rename_errors": [],
+    "after": {
+      "old_count": 0,
+      "old_count_all": 0,
+      "new_count": 12,
+      "new_count_all": 95,
+      "colls": [
+        {
+          "id": "gid://shopify/Collection/298971430963",
+          "count": 71
+        }
+      ]
+    },
+    "floors": {
+      "gid://shopify/Collection/298971430963": 95
+    },
+    "verify": {
+      "old_zero_all": true,
+      "coll_not_dropped": false,
+      "new_increased": true
+    },
+    "status": "CHECK"
+  }
+]
\ No newline at end of file

← 49c5322 Job 2 partial LANDED: PS Removable (736) + DW Exclusive (1)  ·  back to Dw Wallpaper Cleanup  ·  add revert_all.py — reverse this session's Wallcovering clea a474619 →