← back to Designer Wallcoverings
Stage full Winfield Thybony line (2,613 SKUs) into kravet_catalog for cadence — yard-priced foils recovered at MAP; WDW2310 incl
aaf83b4229903cb6813c8bbbf619a9b83db1f798 · 2026-06-25 16:55:23 -0700 · Steve
Files touched
A shopify/scripts/cadence/stage-winfield-thybony.py
Diff
commit aaf83b4229903cb6813c8bbbf619a9b83db1f798
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jun 25 16:55:23 2026 -0700
Stage full Winfield Thybony line (2,613 SKUs) into kravet_catalog for cadence — yard-priced foils recovered at MAP; WDW2310 incl
---
shopify/scripts/cadence/stage-winfield-thybony.py | 147 ++++++++++++++++++++++
1 file changed, 147 insertions(+)
diff --git a/shopify/scripts/cadence/stage-winfield-thybony.py b/shopify/scripts/cadence/stage-winfield-thybony.py
new file mode 100644
index 00000000..255e7250
--- /dev/null
+++ b/shopify/scripts/cadence/stage-winfield-thybony.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+"""
+stage-winfield-thybony.py — stage the full Winfield Thybony line into kravet_catalog
+so the metered DW cadence (cadence-import.js, Kravet entry) can publish it at MAP.
+
+WHY: Winfield Thybony foils/grasscloths are sold per YARD. The Kravet master loader's
+old ROLL-only filter dropped all ~2,402 yard SKUs (incl. WDW2310 / 321 WDW foils), so
+they never got a price record and never published — 2,858 SKUs prepped in the
+2026-04-20 Kravet export but 0 sellable on Shopify. (Diagnosed 2026-06-25.)
+
+SOURCE (Steve 2026-06-25 "use April sheet now"):
+ ~/Projects/kravet-sheet-sync-2026-04-20/kravet_new_2026-04-20.csv (DW SKU + WHLS + MAP)
+ ~/Projects/kravet-sheet-sync-2026-04-20/kravet_image_catalog.jsonl (mfr_sku -> Brandfolder CDN URL)
+
+SAFE: DRY-RUN by default — reads only, prints a stage/dedup/gate-readiness report.
+ --commit UPSERTs into kravet_catalog with shopify_product_id NULL ("Staged for New");
+ it NEVER creates Shopify products (that is the metered cadence's job, cadence-only rule)
+ and NEVER touches rows that already carry a shopify_product_id (never-duplicate rule).
+
+Usage:
+ python3 stage-winfield-thybony.py # DRY-RUN report
+ python3 stage-winfield-thybony.py --commit # upsert kravet_catalog staging rows
+"""
+import argparse, csv, json, os, subprocess, sys
+
+CSV = os.path.expanduser('~/Projects/kravet-sheet-sync-2026-04-20/kravet_new_2026-04-20.csv')
+IMGS = os.path.expanduser('~/Projects/kravet-sheet-sync-2026-04-20/kravet_image_catalog.jsonl')
+BRAND = 'WINFIELD THYBONY'
+
+def num(v):
+ try: return float(str(v).replace('$','').replace(',','').strip())
+ except (ValueError, TypeError): return None
+
+def psql(sql):
+ r = subprocess.run(['psql','-d','dw_unified','-tAF','\t','-c',sql], capture_output=True, text=True)
+ if r.returncode != 0: sys.exit(f"psql error: {r.stderr[:300]}")
+ return r.stdout
+
+def load_image_index():
+ idx = {}
+ if not os.path.exists(IMGS): return idx
+ for line in open(IMGS, encoding='utf-8', errors='replace'):
+ line = line.strip()
+ if not line: continue
+ try: o = json.loads(line)
+ except json.JSONDecodeError: continue
+ a = o.get('algolia') or {}
+ url = a.get('full') or a.get('image') or a.get('thumbnail')
+ if o.get('sku') and url: idx[o['sku']] = url
+ return idx
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--commit', action='store_true')
+ args = ap.parse_args()
+
+ img_idx = load_image_index()
+
+ # Pull the Winfield rows from the April export.
+ rows = []
+ with open(CSV, newline='', encoding='utf-8', errors='replace') as fh:
+ for r in csv.DictReader(fh):
+ if BRAND not in (r.get('brand') or '').upper(): continue
+ dw, mfr = (r.get('new_dw_sku') or '').strip(), (r.get('mfr_sku') or '').strip()
+ if not dw or not mfr: continue
+ mp, whls = num(r.get('map')), num(r.get('wholesale'))
+ rows.append({
+ 'dw_sku': dw, 'mfr_sku': mfr,
+ 'pattern': (r.get('pattern_name') or '').strip(),
+ 'color': (r.get('color_name') or '').strip(),
+ 'unit': (r.get('unit_of_measure') or '').strip().upper(),
+ 'width': (r.get('width') or '').strip(),
+ 'content': (r.get('content') or '').strip(),
+ 'cost': whls, 'map': mp,
+ 'status': (r.get('display_status') or '').strip(),
+ 'img': img_idx.get(mfr),
+ })
+
+ # Dedup signals from the live DB.
+ have_pid = set(psql(
+ "SELECT dw_sku FROM kravet_catalog WHERE coalesce(shopify_product_id::text,'')<>'';").split())
+ on_shop = set(x for x in psql(
+ "SELECT sku FROM shopify_products UNION SELECT replace(sku,'-Sample','') FROM shopify_products "
+ "WHERE sku LIKE '%-Sample';").split())
+ in_cat = set(psql("SELECT dw_sku FROM kravet_catalog;").split())
+
+ total = len(rows)
+ priced = [r for r in rows if (r['map'] or 0) > 0 and (r['cost'] or 0) > 0]
+ has_img = [r for r in priced if r['img']]
+ has_wid = [r for r in has_img if r['width']]
+ gate_ok = has_wid # price + image + width = activation-eligible
+ already = [r for r in rows if r['dw_sku'] in have_pid or r['dw_sku'] in on_shop]
+ to_stage = [r for r in rows if r['dw_sku'] not in have_pid and r['dw_sku'] not in on_shop]
+ unit_split = {}
+ for r in to_stage: unit_split[r['unit'] or '(blank)'] = unit_split.get(r['unit'] or '(blank)',0)+1
+
+ print(f"\n=== Winfield Thybony staging report (source: {os.path.basename(CSV)}) ===")
+ print(f" rows in sheet (brand={BRAND}): {total}")
+ print(f" priced (WHLS>0 AND MAP>0): {len(priced)}")
+ print(f" + resolvable image URL: {len(has_img)}")
+ print(f" + width (full activation gate eligible): {len(gate_ok)}")
+ print(f" already on Shopify / linked (skip): {len(already)}")
+ print(f" --> NET to stage into kravet_catalog: {len(to_stage)}")
+ print(f" unit split of staged rows: {unit_split}")
+ # spot-check the SKU that started this
+ wd = next((r for r in rows if r['mfr_sku'].startswith('WDW2310')), None)
+ if wd: print(f" WDW2310 -> {wd['dw_sku']} {wd['pattern']}/{wd['color']} unit={wd['unit']} cost={wd['cost']} MAP={wd['map']} img={'Y' if wd['img'] else 'N'}")
+
+ if not args.commit:
+ print("\nDRY-RUN — no writes. Re-run with --commit to upsert these staging rows.")
+ return
+
+ # COMMIT: upsert staging rows (shopify_product_id stays NULL = Staged for New).
+ import tempfile
+ tmp = tempfile.mkdtemp(prefix='wt-stage-')
+ tsv = os.path.join(tmp, 'wt.tsv')
+ with open(tsv, 'w') as fh:
+ for r in to_stage:
+ fh.write('\t'.join('' if x in (None,'') else str(x) for x in [
+ r['mfr_sku'], r['dw_sku'], r['pattern'], r['color'], BRAND,
+ r['cost'], r['map'], r['map'], r['unit'], r['width'], r['content'],
+ r['img'] or '', 'WINFIELD THYBONY', r['status'] or 'Active']) + '\n')
+ sql = f"""
+ CREATE TEMP TABLE _wt(mfr_sku text,dw_sku text,pattern_name text,color_name text,brand text,
+ cost_price numeric,price_retail numeric,price_retail_new numeric,unit_of_measure text,
+ width text,content text,image_url text,collection text,sheet_display_status text);
+ \\copy _wt FROM '{tsv}'
+ INSERT INTO kravet_catalog (mfr_sku,dw_sku,pattern_name,color_name,brand,cost_price,
+ price_retail,price_retail_new,unit_of_measure,width,composition,image_url,collection,
+ sheet_display_status,product_type)
+ SELECT mfr_sku,dw_sku,pattern_name,color_name,brand,cost_price,price_retail,price_retail_new,
+ unit_of_measure,width,content,image_url,collection,sheet_display_status,'Wallcovering'
+ FROM _wt
+ ON CONFLICT (mfr_sku) DO UPDATE SET
+ cost_price=EXCLUDED.cost_price, price_retail=EXCLUDED.price_retail,
+ price_retail_new=EXCLUDED.price_retail_new, unit_of_measure=EXCLUDED.unit_of_measure,
+ image_url=COALESCE(NULLIF(kravet_catalog.image_url,''),EXCLUDED.image_url)
+ WHERE coalesce(kravet_catalog.shopify_product_id::text,'')='';
+ SELECT count(*) FROM kravet_catalog WHERE brand='WINFIELD THYBONY' AND coalesce(shopify_product_id::text,'')='';
+ """
+ r = subprocess.run(['psql','-d','dw_unified','-v','ON_ERROR_STOP=1'], input=sql,
+ capture_output=True, text=True)
+ if r.returncode != 0: sys.exit(f"COMMIT failed: {r.stderr[:400]}")
+ print(f"\n✅ staged Winfield Thybony rows (pending cadence): {r.stdout.strip().splitlines()[-1]}")
+
+if __name__ == '__main__':
+ main()
← 59dba9ea docs(filter-topbar): G3 LIVE publish done — surgical push to
·
back to Designer Wallcoverings
·
auto-save: 2026-06-25T17:05:58 (13 files) — pending-approval dd45cbf0 →