[object Object]

← back to Hollywood Price 2026

524 split executors: price_524 (257 strong-match, canary verified) + archive_267 (Shopify ARCHIVE + dw_unified mirror update)

d5f69c9bf2e7f2623a3507b8cb62199840bc5624 · 2026-07-12 09:06:10 -0700 · Steve Abrams

Files touched

Diff

commit d5f69c9bf2e7f2623a3507b8cb62199840bc5624
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 09:06:10 2026 -0700

    524 split executors: price_524 (257 strong-match, canary verified) + archive_267 (Shopify ARCHIVE + dw_unified mirror update)
---
 archive_267.py        |  83 ++++++++++++++++++++++++++
 ledger/priced_524.csv |  46 +++++++++++++++
 price_524.py          | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 286 insertions(+)

diff --git a/archive_267.py b/archive_267.py
new file mode 100644
index 0000000..8436ec3
--- /dev/null
+++ b/archive_267.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+"""Archive the Hollywood products with no live Momentum match (Steve 2026-07-12,
+'archive gone, price live' split) + reflect in dw_unified mirror.
+Reads archive_final.csv. Shopify: status->ARCHIVED. dw_unified.shopify_products:
+status->ARCHIVED (shopify_id = gid). Idempotent (ledger + skip already-archived).
+Reversible (un-archive). $0."""
+import sys, os, csv, json, subprocess, datetime, time
+sys.path.insert(0, "lib")
+from shopify import gql
+
+CSV = "archive_final.csv"
+LEDGER = os.path.join("ledger", "archived.csv")
+M_UPD = """mutation($input:ProductInput!){ productUpdate(input:$input){
+  product{ id status } userErrors{ field message } } }"""
+Q = "query($id:ID!){ product(id:$id){ status } }"
+
+
+def conn():
+    import glob, re
+    for f in glob.glob(os.path.expanduser("~/Projects/hollywood-import/**/.env"), recursive=True) + \
+             glob.glob(os.path.expanduser("~/Projects/hollywood-import/*.env")):
+        for line in open(f):
+            m = re.search(r"postgresql://dw_admin:[^\s\"']+@127\.0\.0\.1:5432/dw_unified", line)
+            if m:
+                return m.group(0)
+    return None
+
+
+def log(pid, title, status):
+    new = not (os.path.exists(LEDGER) and os.path.getsize(LEDGER) > 0)
+    with open(LEDGER, "a", newline="") as f:
+        w = csv.writer(f)
+        if new:
+            w.writerow(["ts", "product_id", "title", "status"])
+        w.writerow([datetime.datetime.now().isoformat(timespec="seconds"), pid, title, status])
+
+
+def seen():
+    s = set()
+    if os.path.exists(LEDGER):
+        for r in csv.DictReader(open(LEDGER)):
+            if r["status"].startswith("OK") and r.get("product_id"):
+                s.add(r["product_id"])
+    return s
+
+
+def main():
+    rows = list(csv.DictReader(open(CSV)))
+    done = seen()
+    ok = skip = fail = 0
+    archived_gids = []
+    for i, r in enumerate(rows):
+        pid = str(r["product_id"]); gid = r["gid"]
+        if pid in done:
+            skip += 1; continue
+        cur = gql(Q, {"id": gid})["product"]
+        if not cur:
+            log(pid, r["title"], "SKIP:not-found"); skip += 1; continue
+        if cur["status"] == "ARCHIVED":
+            log(pid, r["title"], "OK:already-archived"); archived_gids.append(gid); ok += 1; continue
+        d = gql(M_UPD, {"input": {"id": gid, "status": "ARCHIVED"}})
+        ue = d["productUpdate"]["userErrors"]
+        if ue:
+            log(pid, r["title"], "FAIL:" + json.dumps(ue)[:120]); fail += 1; continue
+        log(pid, r["title"], "OK"); archived_gids.append(gid); ok += 1
+        if (i + 1) % 25 == 0:
+            print(f"  ...{i+1}/{len(rows)} ok={ok} skip={skip} fail={fail}")
+        time.sleep(0.15)
+    print(f"SHOPIFY ARCHIVE DONE: ok={ok} skip={skip} fail={fail}")
+
+    # reflect in dw_unified mirror
+    c = conn()
+    if c and archived_gids:
+        vals = ",".join("'" + g.replace("'", "") + "'" for g in archived_gids)
+        sql = f"UPDATE shopify_products SET status='ARCHIVED' WHERE shopify_id IN ({vals});"
+        r = subprocess.run(["psql", c, "-tAc", sql], capture_output=True, text=True)
+        print("dw_unified update:", (r.stdout or r.stderr).strip()[:120])
+    else:
+        print("dw_unified: skipped (no conn or nothing archived)")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/ledger/priced_524.csv b/ledger/priced_524.csv
new file mode 100644
index 0000000..ddd5f5f
--- /dev/null
+++ b/ledger/priced_524.csv
@@ -0,0 +1,46 @@
+ts,product_id,dw_sku,uom,hw,sellable_sku,variant_id,status

+2026-07-12T09:03:30,1497183846512,Flamenco WC,YD,15.06,-68079-Yard,gid://shopify/ProductVariant/44525140869171,OK

+2026-07-12T09:03:33,1497184239728,Fracture,YD,52.71,XP4-68082-Yard,gid://shopify/ProductVariant/44525140901939,OK

+2026-07-12T09:03:36,1498985234544,Silica Etch,YD,123.8,XLB-47632-Yard,gid://shopify/ProductVariant/44525140934707,OK

+2026-07-12T09:04:31,1498985365616,Silica Etch,YD,123.8,XLB-47633-Yard,gid://shopify/ProductVariant/44525141196851,OK

+2026-07-12T09:04:33,1498985562224,Silica Etch,YD,123.8,XLB-47634-Yard,gid://shopify/ProductVariant/44525141229619,OK

+2026-07-12T09:04:35,1498985791600,Silica Etch,YD,123.8,XLB-47635-Yard,gid://shopify/ProductVariant/44525141262387,OK

+2026-07-12T09:04:38,1498986152048,Silica Etch,YD,123.8,XLB-47637-Yard,gid://shopify/ProductVariant/44525141295155,OK

+2026-07-12T09:04:40,1498986348656,Silica Etch,YD,123.8,XLB-47638-Yard,gid://shopify/ProductVariant/44525141327923,OK

+2026-07-12T09:04:43,1498986840176,Silica Etch,YD,123.8,XLB-47640-Yard,gid://shopify/ProductVariant/44525141360691,OK

+2026-07-12T09:04:45,1498986971248,Silica Etch,YD,123.8,XLB-47641-Yard,gid://shopify/ProductVariant/44525141393459,OK

+2026-07-12T09:04:47,1498987266160,Silica Etch,YD,123.8,XLB-47642-Yard,gid://shopify/ProductVariant/44525141426227,OK

+2026-07-12T09:04:50,1498987692144,Silica Etch,YD,123.8,XLB-47644-Yard,gid://shopify/ProductVariant/44525141458995,OK

+2026-07-12T09:04:52,1498987921520,Silica Etch,YD,123.8,XLB-47645-Yard,gid://shopify/ProductVariant/44525141491763,OK

+2026-07-12T09:04:55,1499123449968,Reflections WC,YD,45.32,XPK-48223-Yard,gid://shopify/ProductVariant/44525141524531,OK

+2026-07-12T09:04:57,1499123581040,Reflections WC,YD,45.32,XPK-48224-Yard,gid://shopify/ProductVariant/44525141557299,OK

+2026-07-12T09:04:59,1499123777648,Reflections WC,YD,45.32,XPK-48225-Yard,gid://shopify/ProductVariant/44525141590067,OK

+2026-07-12T09:05:02,1499123974256,Reflections WC,YD,45.32,XPK-48226-Yard,gid://shopify/ProductVariant/44525141622835,OK

+2026-07-12T09:05:04,1499124138096,Reflections WC,YD,45.32,XPK-48227-Yard,gid://shopify/ProductVariant/44525141655603,OK

+2026-07-12T09:05:06,1499124269168,Reflections WC,YD,45.32,XPK-48228-Yard,gid://shopify/ProductVariant/44525141688371,OK

+2026-07-12T09:05:09,1499124662384,Reflections WC,YD,45.32,XPK-48230-Yard,gid://shopify/ProductVariant/44525141721139,OK

+2026-07-12T09:05:11,1499124826224,Reflections WC,YD,45.32,XPK-48231-Yard,gid://shopify/ProductVariant/44525141753907,OK

+2026-07-12T09:05:14,1499125284976,Reflections WC,YD,45.32,XPK-48233-Yard,gid://shopify/ProductVariant/44525141786675,OK

+2026-07-12T09:05:16,1499125481584,Reflections WC,YD,45.32,XPK-48234-Yard,gid://shopify/ProductVariant/44525141819443,OK

+2026-07-12T09:05:18,1499125678192,Reflections WC,YD,45.32,XPK-48235-Yard,gid://shopify/ProductVariant/44525141852211,OK

+2026-07-12T09:05:20,1499125809264,Reflections WC,YD,45.32,XPK-48236-Yard,gid://shopify/ProductVariant/44525141884979,OK

+2026-07-12T09:05:23,1499126169712,Reflections WC,YD,45.32,XPK-48237-Yard,gid://shopify/ProductVariant/44525141917747,OK

+2026-07-12T09:05:25,1499126333552,Reflections WC,YD,45.32,XPK-48238-Yard,gid://shopify/ProductVariant/44525141950515,OK

+2026-07-12T09:05:28,1499126726768,Reflections WC,YD,45.32,XPK-48240-Yard,gid://shopify/ProductVariant/44525141983283,OK

+2026-07-12T09:05:30,1499145633904,Silence,YD,49.88,XPV-48321-Yard,gid://shopify/ProductVariant/44525142016051,OK

+2026-07-12T09:05:33,1499145896048,Silence,YD,49.88,XPV-48322-Yard,gid://shopify/ProductVariant/44525142048819,OK

+2026-07-12T09:05:35,1499146027120,Silence,YD,49.88,XPV-48323-Yard,gid://shopify/ProductVariant/44525142081587,OK

+2026-07-12T09:05:37,1499146420336,Silence,YD,49.88,XPV-48324-Yard,gid://shopify/ProductVariant/44525142114355,OK

+2026-07-12T09:05:40,1499146780784,Silence,YD,49.88,XPV-48325-Yard,gid://shopify/ProductVariant/44525142147123,OK

+2026-07-12T09:05:42,1499147010160,Silence,YD,49.88,XPV-48326-Yard,gid://shopify/ProductVariant/44525142212659,OK

+2026-07-12T09:05:44,1499147403376,Silence,YD,49.88,XPV-48327-Yard,gid://shopify/ProductVariant/44525142245427,OK

+2026-07-12T09:05:46,1499147698288,Silence,YD,49.88,XPV-48328-Yard,gid://shopify/ProductVariant/44525142278195,OK

+2026-07-12T09:05:49,1499148157040,Silence,YD,49.88,XPV-48329-Yard,gid://shopify/ProductVariant/44525142310963,OK

+2026-07-12T09:05:51,1499148419184,Silence,YD,49.88,XPV-48330-Yard,gid://shopify/ProductVariant/44525142343731,OK

+2026-07-12T09:05:53,1499148648560,Silence,YD,49.88,XPV-48331-Yard,gid://shopify/ProductVariant/44525142376499,OK

+2026-07-12T09:05:56,1499148976240,Silence,YD,49.88,XPV-48332-Yard,gid://shopify/ProductVariant/44525142409267,OK

+2026-07-12T09:05:58,1499149238384,Silence,YD,49.88,XPV-48333-Yard,gid://shopify/ProductVariant/44525142442035,OK

+2026-07-12T09:06:00,1499149566064,Silence,YD,49.88,XPV-48334-Yard,gid://shopify/ProductVariant/44525142474803,OK

+2026-07-12T09:06:03,1499149860976,Silence,YD,49.88,XPV-48335-Yard,gid://shopify/ProductVariant/44525142507571,OK

+2026-07-12T09:06:05,1499150057584,Silence,YD,49.88,XPV-48336-Yard,gid://shopify/ProductVariant/44525142540339,OK

+2026-07-12T09:06:07,1499150614640,Silence,YD,49.88,XPV-48338-Yard,gid://shopify/ProductVariant/44525142573107,OK

diff --git a/price_524.py b/price_524.py
new file mode 100644
index 0000000..e0c9784
--- /dev/null
+++ b/price_524.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+"""Price Hollywood/Momentum sample-only products (Steve-approved 2026-07-12).
+Per plan.csv row: rename option Title->Size, keep $4.25 Sample, ADD one sellable
+variant "Sold Per {Box|Yard|Panel|Square Yard}" priced at feed `hw` (Hollywood
+retail = list x 1.448), inventoryPolicy CONTINUE, tracked, qty min/step=1; strip
+Needs-Price tag. Idempotent (ledger + live pre-check). $0 (Admin API free).
+
+Usage:  python3 price.py --canary N      # price first N, verbose, then STOP
+        python3 price.py --batch          # price the rest (skips ledger-done)
+"""
+import sys, os, csv, json, time, datetime
+sys.path.insert(0, "lib")
+from shopify import gql
+
+CSV = "plan_price_final.csv"
+LEDGER = os.path.join("ledger", "priced_524.csv")
+
+UNIT = {  # feed uom -> (option value label, sku suffix)
+    "YD": ("Sold Per Yard", "Yard"),
+    "PANEL": ("Sold Per Panel", "Panel"),
+    "BOX": ("Sold Per Box", "Box"),
+    "SY": ("Sold Per Square Yard", "SqYard"),
+}
+
+Q_PROD = """query($id:ID!){ product(id:$id){ id legacyResourceId title status handle tags
+  options{ id name position values }
+  variants(first:30){ edges{ node{ id title sku price selectedOptions{name value} } } } } }"""
+M_OPT_RENAME = """mutation($pid:ID!,$oid:ID!,$name:String!){
+  productOptionUpdate(productId:$pid, option:{id:$oid, name:$name}){ userErrors{ field message } } }"""
+M_VAR_CREATE = """mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
+  productVariantsBulkCreate(productId:$pid, variants:$variants){
+    userErrors{ field message }
+    productVariants{ id title sku price inventoryPolicy selectedOptions{name value} } } }"""
+M_MF_SET = """mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ userErrors{ field message } } }"""
+M_TAGSRM = """mutation($id:ID!,$tags:[String!]!){ tagsRemove(id:$id, tags:$tags){ userErrors{ message } } }"""
+
+
+def has_sellable(node):
+    for e in node["variants"]["edges"]:
+        n = e["node"]
+        sk = (n["sku"] or "")
+        if n["title"] != "Sample" and not sk.lower().endswith("-sample"):
+            return True
+    return False
+
+
+def log(row):
+    new = not (os.path.exists(LEDGER) and os.path.getsize(LEDGER) > 0)
+    with open(LEDGER, "a", newline="") as f:
+        w = csv.writer(f)
+        if new:
+            w.writerow(["ts", "product_id", "dw_sku", "uom", "hw", "sellable_sku", "variant_id", "status"])
+        w.writerow(row)
+
+
+def seen():
+    s = set()
+    if os.path.exists(LEDGER):
+        for r in csv.DictReader(open(LEDGER)):
+            if r.get("status", "").startswith("OK") and r.get("product_id"):
+                s.add(r["product_id"])
+    return s
+
+
+def process(r, verbose=False):
+    ts = datetime.datetime.now().isoformat(timespec="seconds")
+    pid = str(r["product_id"]); dw = r.get("mom_pattern", ""); uom = r["unit"]
+    try:
+        hw = round(float(r["hw"]), 2)
+    except Exception:
+        log([ts, pid, dw, uom, r.get("hw"), "", "", "SKIP:bad-hw"]); return "SKIP:bad-hw"
+    lbl = UNIT.get(uom)
+    if not lbl:
+        log([ts, pid, dw, uom, hw, "", "", f"SKIP:bad-uom:{uom}"]); return f"SKIP:bad-uom:{uom}"
+    label, suffix = lbl
+    node = gql(Q_PROD, {"id": r["gid"]})["product"]
+    if not node:
+        log([ts, pid, dw, uom, hw, "", "", "SKIP:not-found"]); return "SKIP:not-found"
+    gid = node["id"]
+    # Already priced? The Needs-Price tag is often STALE (product already has a
+    # correct Sold-Per-Yard variant). If the existing price matches hw, just strip
+    # the stale tag; if it DIFFERS, flag it (never silently overwrite a real price).
+    existing = None
+    for e in node["variants"]["edges"]:
+        n2 = e["node"]; sk = (n2["sku"] or "")
+        if n2["title"] != "Sample" and not sk.lower().endswith("-sample"):
+            existing = n2; break
+    if existing:
+        try:
+            same = abs(float(existing["price"]) - hw) < 0.5
+        except Exception:
+            same = False
+        if same:
+            gql(M_TAGSRM, {"id": gid, "tags": ["Needs-Price"]})
+            log([ts, pid, dw, uom, hw, existing["sku"], existing["id"], "OK:tag-stripped"]); return "OK:tag-stripped"
+        log([ts, pid, dw, uom, hw, existing["sku"], existing["id"], f"FLAG:price-mismatch:{existing['price']}vs{hw}"]); return "FLAG:price-mismatch"
+    sellable_sku = f"{r['sku_base']}-{suffix}"
+
+    # 1) rename option Title -> Size
+    opt = node["options"][0]
+    if opt["name"] != "Size":
+        d = gql(M_OPT_RENAME, {"pid": gid, "oid": opt["id"], "name": "Size"})
+        ue = d["productOptionUpdate"]["userErrors"]
+        if ue:
+            log([ts, pid, dw, uom, hw, sellable_sku, "", "FAIL:opt-rename:" + json.dumps(ue)[:120]]); return "FAIL:opt-rename"
+
+    # 2) add sellable variant at hw
+    d = gql(M_VAR_CREATE, {"pid": gid, "variants": [{
+        "price": f"{hw:.2f}",
+        "optionValues": [{"optionName": "Size", "name": label}],
+        "inventoryPolicy": "CONTINUE",
+        "inventoryItem": {"sku": sellable_sku, "tracked": True},
+    }]})
+    res = d["productVariantsBulkCreate"]
+    if res["userErrors"]:
+        log([ts, pid, dw, uom, hw, sellable_sku, "", "FAIL:var-create:" + json.dumps(res["userErrors"])[:160]]); return "FAIL:var-create"
+    vid = res["productVariants"][0]["id"]
+
+    # 3) qty min/step = 1
+    gql(M_MF_SET, {"mf": [
+        {"ownerId": gid, "namespace": "global", "key": "v_prods_quantity_order_min", "type": "single_line_text_field", "value": "1"},
+        {"ownerId": gid, "namespace": "global", "key": "v_prods_quantity_order_units", "type": "single_line_text_field", "value": "1"},
+    ]})
+    # 4) strip Needs-Price
+    gql(M_TAGSRM, {"id": gid, "tags": ["Needs-Price"]})
+
+    log([ts, pid, dw, uom, hw, sellable_sku, vid, "OK"])
+    if verbose:
+        print(json.dumps({"pid": pid, "title": node["title"], "dw_sku": dw,
+                          "sellable_sku": sellable_sku, "price": f"{hw:.2f}", "unit": label,
+                          "variant_id": vid}, indent=2, ensure_ascii=False))
+    return "OK"
+
+
+if __name__ == "__main__":
+    rows = list(csv.DictReader(open(CSV)))
+    done = seen()
+    if "--canary" in sys.argv:
+        n = int(sys.argv[sys.argv.index("--canary") + 1])
+        todo = [r for r in rows if str(r["product_id"]) not in done][:n]
+        print(f"=== CANARY: pricing {len(todo)} ===")
+        for r in todo:
+            print(f"\n--- {r['title'][:50]} ({r['unit']} @ ${r['hw']}) ---")
+            print("RESULT:", process(r, verbose=True))
+        sys.exit(0)
+    todo = [r for r in rows if str(r["product_id"]) not in done]
+    print(f"=== BATCH: {len(todo)} to price ({len(done)} already done) ===")
+    ok = sk = fl = 0
+    for i, r in enumerate(todo):
+        st = process(r)
+        if st == "OK": ok += 1
+        elif st.startswith("FAIL"): fl += 1
+        else: sk += 1
+        if (i + 1) % 25 == 0:
+            print(f"  ...{i+1}/{len(todo)} ok={ok} skip={sk} fail={fl}")
+        time.sleep(0.15)
+    print(f"BATCH DONE: ok={ok} skip={sk} fail={fl}")

← 676b8cc 795 pricing batch complete + 524 split plans (257 price-stro  ·  back to Hollywood Price 2026  ·  auto-save: 2026-07-12T09:17:21 (2 files) — ledger/priced_524 1bdc425 →