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