← back to Dw Catalog 404 Audit
auto-save: 2026-06-26T11:15:12 (1 files) — fetch-publication-status.py
48a1070ff0f57bb1e2ea916bfbc962f6c28db0e5 · 2026-06-26 11:15:13 -0700 · Steve Abrams
Files touched
A fetch-publication-status.py
Diff
commit 48a1070ff0f57bb1e2ea916bfbc962f6c28db0e5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 26 11:15:13 2026 -0700
auto-save: 2026-06-26T11:15:12 (1 files) — fetch-publication-status.py
---
fetch-publication-status.py | 95 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 95 insertions(+)
diff --git a/fetch-publication-status.py b/fetch-publication-status.py
new file mode 100644
index 0000000..635eb13
--- /dev/null
+++ b/fetch-publication-status.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""
+FIX 3(a) — Full-page the Shopify Admin API for publication status of ALL ACTIVE products.
+READ-ONLY (GET only). Pages products.json at 250/page via cursor pagination, polite rate.
+Captures id, handle, status, published_at, published_scope for every active product, then
+populates the local dw_unified.shopify_products.online_store_published column.
+
+published_at IS NOT NULL -> published to a sales channel
+published_scope='global' -> published to Online Store + POS (web-visible)
+online_store_published = (published_at IS NOT NULL) [web visibility predicate]
+
+Author: vp-dw-commerce. No writes to Shopify. Local mirror column populate only.
+"""
+import os, sys, time, json, re, urllib.request, urllib.error
+import subprocess
+
+SHOP = "designer-laboratory-sandbox.myshopify.com"
+VER = "2024-10"
+ENV = os.path.expanduser("~/Projects/secrets-manager/.env")
+OUT = os.path.expanduser("~/Projects/dw-catalog-404-audit")
+
+def token():
+ for line in open(ENV):
+ if line.startswith("SHOPIFY_ADMIN_TOKEN="):
+ return line.strip().split("=", 1)[1]
+ sys.exit("no token")
+
+TOK = token()
+
+def get(url):
+ req = urllib.request.Request(url, headers={"X-Shopify-Access-Token": TOK,
+ "Accept": "application/json"})
+ for attempt in range(6):
+ try:
+ r = urllib.request.urlopen(req, timeout=40)
+ body = r.read()
+ link = r.headers.get("Link", "") or r.headers.get("link", "")
+ limit = r.headers.get("X-Shopify-Shop-Api-Call-Limit", "")
+ return json.loads(body), link, limit
+ except urllib.error.HTTPError as e:
+ if e.code == 429:
+ wait = float(e.headers.get("Retry-After", "2"))
+ sys.stderr.write(f" 429 -> sleep {wait}s (attempt {attempt})\n")
+ time.sleep(wait + 0.5)
+ continue
+ raise
+ sys.exit("too many 429s")
+
+def next_url(link):
+ for part in link.split(","):
+ if 'rel="next"' in part:
+ m = re.search(r"<([^>]+)>", part)
+ if m:
+ return m.group(1)
+ return None
+
+def main():
+ base = (f"https://{SHOP}/admin/api/{VER}/products.json"
+ f"?status=active&limit=250&fields=id,handle,status,published_at,published_scope")
+ url = base
+ rows = []
+ page = 0
+ t0 = time.time()
+ while url:
+ data, link, limit = get(url)
+ prods = data.get("products", [])
+ for p in prods:
+ rows.append((p["id"], p.get("handle"), p.get("status"),
+ p.get("published_at"), p.get("published_scope")))
+ page += 1
+ if page % 20 == 0:
+ sys.stderr.write(f" page {page}: {len(rows)} rows, call-limit {limit}, "
+ f"{time.time()-t0:.0f}s\n")
+ url = next_url(link)
+ time.sleep(0.5) # ~2 req/sec, well under the 4/sec leaky-bucket refill
+ sys.stderr.write(f"DONE: {len(rows)} active products in {page} pages, "
+ f"{time.time()-t0:.0f}s\n")
+ # write raw
+ with open(f"{OUT}/publication-status.jsonl", "w") as f:
+ for r in rows:
+ f.write(json.dumps({"id": r[0], "handle": r[1], "status": r[2],
+ "published_at": r[3], "published_scope": r[4]}) + "\n")
+ pub = sum(1 for r in rows if r[3] is not None)
+ unpub = sum(1 for r in rows if r[3] is None)
+ glob = sum(1 for r in rows if r[4] == "global")
+ web = sum(1 for r in rows if r[4] == "web")
+ summary = {"active_fetched": len(rows), "published_at_not_null": pub,
+ "published_at_null_UNPUBLISHED": unpub,
+ "scope_global": glob, "scope_web": web,
+ "scope_other": len(rows) - glob - web}
+ sys.stderr.write(json.dumps(summary, indent=2) + "\n")
+ json.dump(summary, open(f"{OUT}/publication-summary.json", "w"), indent=2)
+
+if __name__ == "__main__":
+ main()
← dd16d3d Add audit-result.json (scope + sample + known-SKU verdicts)
·
back to Dw Catalog 404 Audit
·
FIX 3(a): full-scan publication diagnostic — confirm publish fa1f6c5 →