[object Object]

← 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 →