← back to Dw Photo Capture
ops/tres-tintas-dwts-2026-09-24/shop-tb.py
68 lines
# Steve 2026-09-24 "fix those too": Tres Tintas variant SKUs DWTB-N... (Timorous Beasties' registry prefix)
# -> DWTS-N... (Tres Tintas' own prefix). Same numbers; verified no collision with existing DWTS numbers,
# and no Timorous Beasties product uses any of these DWTB numbers.
# python3 shop-tb.py canary -> renames the first 4 variants and prints them
# python3 shop-tb.py all -> the rest (idempotent: already-renamed variants no longer match DWTB-)
# Undo: every change is appended to shop-tb-restore.jsonl BEFORE it is made (variant id, old sku, new sku).
import json, sys, re, urllib.request, urllib.error, os, time
S = os.path.dirname(os.path.abspath(__file__))
MODE = sys.argv[1] if len(sys.argv) > 1 else 'canary'
TOK = [l.split('=', 1)[1].strip() for l in open(os.path.expanduser('~/Projects/secrets-manager/.env'))
if l.startswith('SHOPIFY_ADMIN_TOKEN=')][0]
BASE = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10'
def call(m, p, body=None):
for a in range(6):
try:
r = urllib.request.Request(BASE + p, method=m, data=json.dumps(body).encode() if body else None,
headers={'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json'})
with urllib.request.urlopen(r, timeout=30) as x:
return json.load(x)
except urllib.error.HTTPError as e:
if e.code == 429 or e.code >= 500:
time.sleep(2 * (a + 1))
continue
raise
raise RuntimeError('retries exhausted ' + p)
prods, since = [], 0
while True:
pg = call('GET', f'/products.json?vendor=Tres%20Tintas&limit=250&since_id={since}&fields=id,title,vendor,variants')['products']
prods += pg
if len(pg) < 250:
break
since = pg[-1]['id']
jobs = [(p['id'], v['id'], v['sku'], 'DWTS-' + v['sku'][5:])
for p in prods if p['vendor'] == 'Tres Tintas'
for v in p['variants'] if re.match(r'^DWTB-\d', v['sku'] or '')]
print('Tres Tintas products', len(prods), '| DWTB variants to rename', len(jobs), flush=True)
if MODE == 'canary':
jobs = jobs[:4]
log = open(os.path.join(S, 'shop-tb-restore.jsonl'), 'a')
ok = fail = 0
for i, (pid, vid, old, new) in enumerate(jobs):
log.write(json.dumps({'product': pid, 'variant': vid, 'old': old, 'new': new}) + '\n')
log.flush()
got = None
try:
got = call('PUT', f'/variants/{vid}.json', {'variant': {'id': vid, 'sku': new}})['variant']['sku']
if got == new:
ok += 1
else:
fail += 1
print('MISMATCH', vid, old, got, flush=True)
except Exception as e:
fail += 1
print('ERR', vid, old, str(e)[:100], flush=True)
if MODE == 'canary':
print(' ', repr(old), '->', repr(got))
if i % 100 == 99:
print(f'progress {i + 1}/{len(jobs)} ok={ok} fail={fail}', flush=True)
time.sleep(0.5)
print(f'mode={MODE} ok={ok} fail={fail} of {len(jobs)}', flush=True)