← back to Roll On Fabric Fix
fix.py
60 lines
#!/usr/bin/env python3
"""
Roll-on-fabric fix. PREP / DRY-RUN BY DEFAULT.
45 Mulberry fabrics carry a stray legacy metafield global.unit_of_measure = "Full Roll"
alongside the correct global.Unit of measure = "YARD". This DELETES the stray legacy key
(the canonical YARD stays), which also de-noises the roll-on-fabric canary.
Each target was pre-verified: legacy key present AND canonical YARD present (data/worklist.json).
SAFE BY CONSTRUCTION:
* DRY-RUN default. Prints the plan, ZERO Shopify writes.
* Live requires BOTH: --apply AND env ROLLFIX_CONFIRM=YES
* --canary N limits to first N. ~0.4s paced.
* Fully reversible: reverse.py re-creates the exact legacy metafield from the worklist.
* The REAL root-cause fix (mirror sync replace-not-merge to prune deleted keys) is separate
and code-side; this only removes the live stray keys.
Usage:
python3 fix.py # DRY-RUN all 45 (no writes)
python3 fix.py --canary 5 # DRY-RUN first 5
ROLLFIX_CONFIRM=YES python3 fix.py --apply --canary 5 # LIVE canary (Steve-gated)
"""
import os, sys, json, time, urllib.request, urllib.error
HERE = os.path.dirname(os.path.abspath(__file__))
SHOP = "designer-laboratory-sandbox.myshopify.com"; API = "2024-10"
def token():
for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
if l.startswith("SHOPIFY_ADMIN_TOKEN="): return l.split("=", 1)[1].strip()
sys.exit("no SHOPIFY_ADMIN_TOKEN")
def main():
apply = "--apply" in sys.argv
live = apply and os.environ.get("ROLLFIX_CONFIRM") == "YES"
if apply and not live:
print("REFUSING --apply without ROLLFIX_CONFIRM=YES. DRY-RUN instead.\n")
canary = int(sys.argv[sys.argv.index("--canary") + 1]) if "--canary" in sys.argv else None
work = json.load(open(os.path.join(HERE, "data", "worklist.json")))
if canary: work = work[:canary]
print(f"=== roll-on-fabric fix [{'LIVE' if live else 'DRY-RUN'}] — delete legacy "
f"global.unit_of_measure on {len(work)} Mulberry fabrics (reversible) ===\n")
tok = token() if live else None
done = err = 0
for i, w in enumerate(work, 1):
line = f"[{i}/{len(work)}] product {w['num']} mf_id={w['mf_id']} (was '{w['legacy_value']}', canon={w['canon']})"
if not live:
print("WOULD DELETE:", line); continue
try:
req = urllib.request.Request(f"https://{SHOP}/admin/api/{API}/metafields/{w['mf_id']}.json",
method="DELETE", headers={"X-Shopify-Access-Token": tok})
urllib.request.urlopen(req, timeout=40); done += 1; print("DELETED:", line)
except urllib.error.HTTPError as e:
err += 1; print(f"FAIL {line} -> HTTP {e.code}")
time.sleep(0.4)
print(f"\n{'deleted=%d failed=%d. Reverse with reverse.py' % (done, err) if live else 'DRY-RUN complete. Nothing written.'}")
if __name__ == "__main__":
main()