← back to Kravet Fabric Fix 2026 04 23
fix_dwkk_misclassified.py
163 lines
#!/usr/bin/env python3
"""
Fix DWKK products whose Shopify productType says wallcovering but the Kravet
master sheet classifies the item as MULTIPURPOSE / UPHOLSTERY / DRAPERY.
Per-row action (idempotent):
1. Pull live product from Shopify (productType, tags, title, status)
2. Target productType = 'Fabric'
3. Target tags = existing - {Wallcovering} + {Fabric, <TitleUse>}
4. Target title = strip stray "Wallpaper" / "Wallcovering" tokens
5. Skip the write if nothing differs
6. Mirror product_type='Fabric' back to kravet_catalog
Run:
python3 fix_dwkk_misclassified.py --limit 5 # pilot
python3 fix_dwkk_misclassified.py --limit 0 # full batch
python3 fix_dwkk_misclassified.py --dry # no writes
"""
import argparse, csv, json, re, subprocess, sys, time, urllib.request
SHOP = 'designer-laboratory-sandbox.myshopify.com'
TOKEN = os.environ.get('SHOPIFY_ADMIN_TOKEN','')
GQL = f'https://{SHOP}/admin/api/2024-10/graphql.json'
SSH = ['ssh', 'root@45.61.58.125']
CSV_PATH = '/Users/macstudio3/Projects/kravet-fabric-fix-2026-04-23/dwkk_misclassified_2026-04-23.csv'
RESULTS = '/Users/macstudio3/Projects/kravet-fabric-fix-2026-04-23/fix_results.jsonl'
TITLE_USE = {'MULTIPURPOSE': 'Multipurpose', 'UPHOLSTERY': 'Upholstery', 'DRAPERY': 'Drapery'}
STRIP_TITLE = re.compile(r'\s*\|\s*(wallpaper|wallcovering)s?\b', re.I)
STRIP_TITLE_TAIL = re.compile(r'\b(wallpaper|wallcovering)s?\s*$', re.I)
def gql(query, variables=None):
body = json.dumps({'query': query, 'variables': variables or {}}).encode()
req = urllib.request.Request(GQL, data=body, method='POST', headers={
'Content-Type': 'application/json',
'X-Shopify-Access-Token': TOKEN,
})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
if 'errors' in data:
raise RuntimeError(data['errors'])
return data['data']
def pg_exec(sql):
subprocess.run(SSH + [
f"PGPASSWORD=DW2024! psql -h 127.0.0.1 -U dw_admin -d dw_unified -c \"{sql}\""
], check=True, capture_output=True)
def compute_target(product, sheet_use):
cur_type = product['productType']
cur_tags = list(product['tags'])
cur_title = product['title']
new_type = 'Fabric'
tag_set = set(cur_tags)
tag_set.discard('Wallcovering')
tag_set.discard('wallcovering')
tag_set.add('Fabric')
tag_set.add(TITLE_USE[sheet_use])
new_tags = sorted(tag_set)
new_title = STRIP_TITLE.sub('', cur_title)
new_title = STRIP_TITLE_TAIL.sub('', new_title).strip().rstrip('|').strip()
changes = {}
if cur_type != new_type:
changes['productType'] = (cur_type, new_type)
if sorted(cur_tags) != new_tags:
changes['tags'] = (cur_tags, new_tags)
if cur_title != new_title:
changes['title'] = (cur_title, new_title)
return new_type, new_tags, new_title, changes
PRODUCT_Q = '''query($id: ID!) {
product(id: $id) { id title productType status tags vendor handle }
}'''
PRODUCT_UPDATE = '''mutation($input: ProductInput!) {
productUpdate(input: $input) {
product { id productType tags title }
userErrors { field message }
}
}'''
def fix_one(row, dry=False):
pid = row['shopify_product_id']
dw_sku = row['dw_sku']
sheet_use = row['sheet_use']
try:
data = gql(PRODUCT_Q, {'id': pid})
except Exception as e:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'fetch_error', 'error': str(e)}
p = data['product']
if p is None:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'not_found'}
new_type, new_tags, new_title, changes = compute_target(p, sheet_use)
if not changes:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'noop', 'title': p['title']}
if dry:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'would_change', 'changes': changes}
variables = {'input': {
'id': pid,
'productType': new_type,
'tags': new_tags,
'title': new_title,
}}
try:
upd = gql(PRODUCT_UPDATE, variables)
except Exception as e:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'update_error', 'error': str(e), 'changes': changes}
errs = upd['productUpdate']['userErrors']
if errs:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'user_errors', 'errors': errs, 'changes': changes}
sql = f"UPDATE kravet_catalog SET product_type='Fabric', updated_at=now() WHERE dw_sku='{dw_sku}'"
try:
pg_exec(sql)
except Exception as e:
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'shopify_ok_pg_fail', 'error': str(e), 'changes': changes}
return {'dw_sku': dw_sku, 'pid': pid, 'status': 'ok', 'changes': changes}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--limit', type=int, default=5)
ap.add_argument('--dry', action='store_true')
ap.add_argument('--offset', type=int, default=0)
args = ap.parse_args()
with open(CSV_PATH) as f:
rows = list(csv.DictReader(f))
if args.offset:
rows = rows[args.offset:]
if args.limit:
rows = rows[:args.limit]
print(f'Processing {len(rows)} products{" (DRY)" if args.dry else ""}')
counters = {}
with open(RESULTS, 'a') as out:
for i, row in enumerate(rows, 1):
result = fix_one(row, dry=args.dry)
counters[result['status']] = counters.get(result['status'], 0) + 1
out.write(json.dumps(result) + '\n')
out.flush()
if i % 25 == 0 or i <= 10:
print(f' [{i}/{len(rows)}] {row["dw_sku"]}: {result["status"]}')
time.sleep(0.15)
print('Done:', counters)
if __name__ == '__main__':
main()