← back to Brand Mckenzie Reprice 2026 07
reprice: 429/430 storefront backoff + paced reads (no more crash on throttle)
b2dfc5e2f6787e6cca8760fc3bd1c86acf63c399 · 2026-07-30 12:25:50 -0700 · Steve
The per-SKU storefront() read had no delay on SKIP paths and no 429/430 handling — a
Shopify throttle raised an unhandled HTTPError. Adds polite_urlopen (Retry-After-aware
backoff) on both storefront reads and Admin writes, plus a jittered pace before every
storefront read. Part of TK-10059 (stop tripping Shopify's local_rate_limited).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit b2dfc5e2f6787e6cca8760fc3bd1c86acf63c399
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 30 12:25:50 2026 -0700
reprice: 429/430 storefront backoff + paced reads (no more crash on throttle)
The per-SKU storefront() read had no delay on SKIP paths and no 429/430 handling — a
Shopify throttle raised an unhandled HTTPError. Adds polite_urlopen (Retry-After-aware
backoff) on both storefront reads and Admin writes, plus a jittered pace before every
storefront read. Part of TK-10059 (stop tripping Shopify's local_rate_limited).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
reprice.py | 24 +++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/reprice.py b/reprice.py
index d7a8a29..aaa9f60 100644
--- a/reprice.py
+++ b/reprice.py
@@ -5,7 +5,7 @@ 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, urllib.request
+import json, os, sys, time, random, urllib.request, urllib.error
ENV = os.path.expanduser("~/Projects/secrets-manager/.env")
env = {}
@@ -24,21 +24,39 @@ 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 urllib.request.urlopen(r) as resp:
+ with polite_urlopen(r) as resp:
return json.load(resp)
def storefront(handle):
- with urllib.request.urlopen(f"https://designerwallcoverings.com/products/{handle}.json") as r:
+ 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']}")
← 358fbe0 chore: ignore scratch order-scan output (session close)
·
back to Brand Mckenzie Reprice 2026 07
·
auto-save: 2026-07-30T12:48:10 (1 files) — __pycache__/repri 4f3d158 →