← 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)