← back to Brand Mckenzie Reprice 2026 07
reprice.py
84 lines
#!/usr/bin/env python3
"""Reprice Brand McKenzie newer-wave SKUs (Sancar cost basis, no phantom 50% discount).
Tiers: old 117.65 (assumed cost $65) -> 235.29 (cost $130 / 0.65 / 0.85)
old 179.19 (assumed cost $99) -> 358.37 (cost $198 / 0.65 / 0.85)
Old-handle duplicates dwbm-260113 / dwbm-260114 are archived (new-handle twins carry the SKU).
"""
import json, os, sys, time, random, urllib.request, urllib.error
ENV = os.path.expanduser("~/Projects/secrets-manager/.env")
env = {}
for line in open(ENV):
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
env[k] = v.strip().strip("'\"")
STORE = env["SHOPIFY_STORE_DOMAIN"] if "myshopify" in env.get("SHOPIFY_STORE_DOMAIN", "") else env["SHOPIFY_STORE"]
if not STORE.endswith("myshopify.com"):
STORE = STORE + ".myshopify.com" if "." not in STORE else STORE
TOKEN = env["SHOPIFY_ADMIN_TOKEN"]
API = f"https://{STORE}/admin/api/2024-01"
NEW_PRICE = {"117.65": "235.29", "179.19": "358.37"}
ARCHIVE_HANDLES = {"dwbm-260113", "dwbm-260114"}
DRY = "--apply" not in sys.argv
POLITE = 0.4 # base pace between storefront reads; jittered
# Honor Retry-After / back off on a 429/430 storefront throttle (Shopify's `local_rate_limited`)
# instead of crashing on the unhandled HTTPError the bare urlopen used to raise.
def polite_urlopen(req_or_url, tries=3):
for attempt in range(tries + 1):
try:
return urllib.request.urlopen(req_or_url)
except urllib.error.HTTPError as e:
if e.code in (429, 430) and attempt < tries:
ra = e.headers.get("Retry-After")
backoff = int(ra) if (ra and str(ra).isdigit()) else [30, 60, 120][min(attempt, 2)]
backoff = max(1, backoff * (1 + random.uniform(-0.15, 0.15)))
print(f" [polite] {e.code} — backing off {backoff:.0f}s (attempt {attempt+1}/{tries})")
time.sleep(backoff)
continue
raise
def req(method, url, body=None):
r = urllib.request.Request(url, method=method,
headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"},
data=json.dumps(body).encode() if body else None)
with polite_urlopen(r) as resp:
return json.load(resp)
def storefront(handle):
with polite_urlopen(f"https://designerwallcoverings.com/products/{handle}.json") as r:
return json.load(r)["product"]
repriced = archived = skipped = 0
for line in open(os.path.join(os.path.dirname(__file__), "targets.txt")):
handle, sku, tier = line.strip().split("|")
time.sleep(POLITE * (1 + random.uniform(-0.15, 0.15))) # pace every storefront read (incl. SKIP paths)
p = storefront(handle)
if handle in ARCHIVE_HANDLES:
print(f"ARCHIVE {handle} ({sku}) product {p['id']}")
if not DRY:
req("PUT", f"{API}/products/{p['id']}.json",
{"product": {"id": p["id"], "status": "archived"}})
archived += 1
time.sleep(0.6)
continue
bolt = [v for v in p["variants"] if "ample" not in v["title"]]
if len(bolt) != 1:
print(f"SKIP {handle}: {len(bolt)} non-sample variants"); skipped += 1; continue
v = bolt[0]
if v["price"] != tier:
print(f"SKIP {handle} ({sku}): live {v['price']} != expected {tier}"); skipped += 1; continue
new = NEW_PRICE[tier]
print(f"REPRICE {sku} {handle[:40]} {v['price']} -> {new}")
if not DRY:
req("PUT", f"{API}/variants/{v['id']}.json",
{"variant": {"id": v["id"], "price": new}})
repriced += 1
time.sleep(0.6)
print(f"\n{'DRY RUN — ' if DRY else ''}repriced={repriced} archived={archived} skipped={skipped}")