← 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
A archive_267.pyA ledger/priced_524.csvA price_524.py
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 →