← back to Kravet Fabric Fix 2026 04 23
fix_shopify_orphans.py
104 lines
#!/usr/bin/env python3
"""Fix ALL Shopify DWKK products with Wallcovering tag where the Kravet sheet
Use column says otherwise (MULTIPURPOSE/UPHOLSTERY/DRAPERY).
Strategy:
1. Page through Shopify (tag:Wallcovering AND sku:DWKK-*)
2. For each product, find the Kravet item code in its tags (e.g. "2019114.55.0")
3. Look up Use in master sheet; if fabric use, patch productType + tags
4. Skip if no sheet match, or if Use is WALLCOVERING
5. Skip -Sample variants (sample products have nothing to fix)
6. Parallelize with 8 workers
"""
import csv, json, re, subprocess, sys, time, urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
import importlib.util
spec = importlib.util.spec_from_file_location('fx',
'/Users/macstudio3/Projects/kravet-fabric-fix-2026-04-23/fix_dwkk_misclassified.py')
fx = importlib.util.module_from_spec(spec); spec.loader.exec_module(fx)
SHEET_CSV = '/Users/macstudio3/Projects/kravet-sheet-sync-2026-04-20/kravet_sheet_2026-04-20.csv'
OUT_PATH = '/Users/macstudio3/Projects/kravet-fabric-fix-2026-04-23/orphan_fix_results.jsonl'
# Build sheet.use map
sheet = {}
with open(SHEET_CSV) as f:
for row in csv.DictReader(f):
norm = re.sub(r'\.0$', '', re.sub(r'-', '.', row['Item'].upper().strip()))
sheet[norm] = row['Use'].upper().strip()
print(f'Sheet rows: {len(sheet)}')
PAGE_Q = '''query($cursor: String) {
products(first: 100, after: $cursor, query: "tag:Wallcovering AND sku:DWKK-*") {
pageInfo { hasNextPage endCursor }
nodes {
id title productType tags status
variants(first: 2) { nodes { sku } }
}
}
}'''
def fix_product(p):
skus = [v['sku'] for v in p['variants']['nodes']]
main = next((s for s in skus if s.startswith('DWKK-') and not s.endswith('-Sample')), '')
if not main:
return {'dw_sku': '', 'status': 'sample_only'}
# Find item code in tags
sheet_use = None
for tag in p['tags']:
if tag == 'Wallcovering' or tag == 'wallcovering':
continue
norm = re.sub(r'\.0$', '', re.sub(r'-', '.', tag.upper().strip()))
if norm in sheet:
sheet_use = sheet[norm]
break
dw_sku = main
if not sheet_use:
return {'dw_sku': dw_sku, 'status': 'no_sheet_match', 'tags': p['tags'][:5]}
if sheet_use == 'WALLCOVERING':
return {'dw_sku': dw_sku, 'status': 'is_wallcovering'}
if sheet_use not in fx.TITLE_USE:
return {'dw_sku': dw_sku, 'status': 'unknown_use', 'use': sheet_use}
new_type, new_tags, new_title, changes = fx.compute_target(p, sheet_use)
if not changes:
return {'dw_sku': dw_sku, 'status': 'noop'}
try:
upd = fx.gql(fx.PRODUCT_UPDATE, {'input': {
'id': p['id'], 'productType': new_type, 'tags': new_tags, 'title': new_title
}})
errs = upd['productUpdate']['userErrors']
if errs:
return {'dw_sku': dw_sku, 'status': 'user_errors', 'errors': errs}
return {'dw_sku': dw_sku, 'status': 'ok', 'use': sheet_use}
except Exception as e:
return {'dw_sku': dw_sku, 'status': 'update_error', 'error': str(e)}
# Stream pages, fix in parallel
cursor = None
total = 0
counters = {}
out = open(OUT_PATH, 'w')
with ThreadPoolExecutor(max_workers=8) as pool:
while True:
data = fx.gql(PAGE_Q, {'cursor': cursor})
page = data['products']
batch = page['nodes']
futures = [pool.submit(fix_product, p) for p in batch]
for fut in as_completed(futures):
r = fut.result()
counters[r['status']] = counters.get(r['status'], 0) + 1
out.write(json.dumps(r) + '\n')
out.flush()
total += len(batch)
print(f'page: total={total} counters={counters}', flush=True)
if not page['pageInfo']['hasNextPage']:
break
cursor = page['pageInfo']['endCursor']
time.sleep(0.25)
out.close()
print('FINAL:', counters)