[object Object]

← back to Fentucci Naturals

gated importer scaffold (create-drafts.py, resumable, day-capped, leak tripwire) — NOT executed; approval memo filed to pending-approval

73c34348510e0d810a6eb35b4a1bbcb50dbe3539 · 2026-07-22 18:01:50 -0700 · Steve Abrams

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 73c34348510e0d810a6eb35b4a1bbcb50dbe3539
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 18:01:50 2026 -0700

    gated importer scaffold (create-drafts.py, resumable, day-capped, leak tripwire) — NOT executed; approval memo filed to pending-approval
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 scripts/create-drafts.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 77 insertions(+)

diff --git a/scripts/create-drafts.py b/scripts/create-drafts.py
new file mode 100644
index 0000000..e5019fa
--- /dev/null
+++ b/scripts/create-drafts.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+"""GATED importer — creates DRAFT products on the LIVE DW Shopify store from
+data/shopify-drafts.jsonl. DO NOT RUN without Steve's approval of
+~/.claude/yolo-queue/pending-approval/fentucci-tokiwa-shopify-import.md.
+
+Resumable: appends to data/created.jsonl and skips SKUs already there.
+Day-cap aware: --limit N products per run (504 products x 2 variants = 1,008
+variants total → run as --limit 490 day 1, remainder day 2 to respect the
+1k variants/day cap). 90s pause every BATCH products per bulk-push rule.
+
+Usage:  source ~/Projects/secrets-manager/.env
+        python3 scripts/create-drafts.py --limit 490
+"""
+import argparse, base64, json, os, sys, time, urllib.request
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+ROOT = os.path.dirname(HERE)
+DRAFTS = os.path.join(ROOT, "data", "shopify-drafts.jsonl")
+CREATED = os.path.join(ROOT, "data", "created.jsonl")
+SHOP = "designer-laboratory-sandbox.myshopify.com"
+API = "2024-10"
+BATCH = 25          # products between 90s pauses
+PAUSE = 90
+
+TOKEN = os.environ.get("SHOPIFY_ADMIN_TOKEN")
+if not TOKEN:
+    sys.exit("SHOPIFY_ADMIN_TOKEN not set — source ~/Projects/secrets-manager/.env")
+
+def api(path, method="GET", body=None):
+    req = urllib.request.Request(
+        f"https://{SHOP}/admin/api/{API}/{path}", method=method,
+        data=json.dumps(body).encode() if body else None,
+        headers={"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
+    for attempt in range(5):
+        try:
+            with urllib.request.urlopen(req, timeout=60) as r:
+                return json.loads(r.read() or "{}")
+        except urllib.error.HTTPError as e:
+            if e.code == 429:
+                time.sleep(4 * (attempt + 1)); continue
+            raise
+    raise RuntimeError(f"429 storm on {path}")
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--limit", type=int, default=490)
+    args = ap.parse_args()
+
+    done = set()
+    if os.path.exists(CREATED):
+        done = {json.loads(l)["sku"] for l in open(CREATED)}
+
+    rows = [json.loads(l) for l in open(DRAFTS)]
+    todo = [r for r in rows if r["sku"] not in done][: args.limit]
+    print(f"{len(done)} already created, importing {len(todo)} this run")
+
+    with open(CREATED, "a") as log:
+        for i, r in enumerate(todo):
+            # leak tripwire — belt & suspenders
+            assert "tokiwa" not in json.dumps(r["product"]).lower(), r["sku"]
+            assert r["product"]["status"] == "draft", r["sku"]
+            img = base64.b64encode(open(r["local_image_path"], "rb").read()).decode()
+            payload = dict(r["product"])
+            payload["images"] = [{"attachment": img, "filename": os.path.basename(r["local_image_path"])}]
+            resp = api("products.json", "POST", {"product": payload})
+            pid = resp["product"]["id"]
+            for mf in r["metafields"]:
+                api(f"products/{pid}/metafields.json", "POST", {"metafield": mf})
+            log.write(json.dumps({"sku": r["sku"], "product_id": pid}) + "\n"); log.flush()
+            print(f"  [{i+1}/{len(todo)}] {r['sku']} -> {pid}", flush=True)
+            time.sleep(1)
+            if (i + 1) % BATCH == 0 and i + 1 < len(todo):
+                print(f"  batch pause {PAUSE}s"); time.sleep(PAUSE)
+    print("run complete — remember: PG mirror re-syncs from Shopify; keep drafts until activation gate passes")
+
+if __name__ == "__main__":
+    main()

← ea8b38b enrich: 504/504 rows via Mac1 qwen3:14b (64 pattern calls, 3  ·  back to Fentucci Naturals  ·  internal line viewer :9981 (fentucci-viewer pm2) — full inte 934b282 →