[object Object]

← back to Fischbacher Onboard

auto-save: 2026-07-22T09:43:01 (2 files) — data/ push_draft.py

2b70920fd27788c1e8931941c03e29830dd4b31f · 2026-07-22 09:43:03 -0700 · Steve Abrams

Files touched

Diff

commit 2b70920fd27788c1e8931941c03e29830dd4b31f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 09:43:03 2026 -0700

    auto-save: 2026-07-22T09:43:01 (2 files) — data/ push_draft.py
---
 data/pushed_manifest.json | 429 ++++++++++++++++++++++++++++++++++++++++++++++
 push_draft.py             | 112 ++++++++++++
 2 files changed, 541 insertions(+)

diff --git a/data/pushed_manifest.json b/data/pushed_manifest.json
new file mode 100644
index 0000000..db6eb6c
--- /dev/null
+++ b/data/pushed_manifest.json
@@ -0,0 +1,429 @@
+{
+"600109.902": {
+"id": 7896367661107,
+"handle": "bloom-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 1
+},
+"600105": {
+"id": 7896370741299,
+"handle": "dorato-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"WP0001": {
+"id": 7896371068979,
+"handle": "kalahari-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 8
+},
+"WP0005": {
+"id": 7896371462195,
+"handle": "kiondo-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 3
+},
+"WP0006": {
+"id": 7896371822643,
+"handle": "kwazulu-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"WP0002": {
+"id": 7896372084787,
+"handle": "lake-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 3
+},
+"WP0003": {
+"id": 7896372510771,
+"handle": "pool-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"WP0004": {
+"id": 7896373297203,
+"handle": "salonga-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"600106": {
+"id": 7896373526579,
+"handle": "tagua-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"600110.": {
+"id": 7896374214707,
+"handle": "turan-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"600107": {
+"id": 7896375132211,
+"handle": "wadi-wallpaper",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"600099": {
+"id": 7896375853107,
+"handle": "wallpaper-botanica",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 1
+},
+"600100": {
+"id": 7896376508467,
+"handle": "wallpaper-gardenia",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 1
+},
+"600094": {
+"id": 7896377262131,
+"handle": "wallpaper-kotori",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 4
+},
+"600101": {
+"id": 7896377819187,
+"handle": "wallpaper-newtopia",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"600102": {
+"id": 7896378507315,
+"handle": "wallpaper-palmeria",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"600103": {
+"id": 7896379490355,
+"handle": "wallpaper-so-avantgardening",
+"line": "Wall Coverings",
+"type": "Wallcovering",
+"variants": 2
+},
+"14644": {
+"id": 7896380211251,
+"handle": "benu-cocoon-recycled-fr",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 3
+},
+"14560": {
+"id": 7896381587507,
+"handle": "benu-engadina-recycled",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 1
+},
+"14647": {
+"id": 7896382799923,
+"handle": "benu-garden-recycled",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 1
+},
+"14559": {
+"id": 7896383291443,
+"handle": "benu-jack-recycled",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 2
+},
+"14645": {
+"id": 7896384864307,
+"handle": "benu-net-recycled",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 4
+},
+"14497": {
+"id": 7896385749043,
+"handle": "benu-oscar-recycled-fr",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 15
+},
+"14643": {
+"id": 7896386404403,
+"handle": "benu-pure-recycled-fr",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 6
+},
+"14454": {
+"id": 7896387321907,
+"handle": "benu-remix-recycled",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 25
+},
+"14646": {
+"id": 7896388468787,
+"handle": "benu-soul-recycled",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 1
+},
+"14619": {
+"id": 7896389189683,
+"handle": "benu-talent-recycled-fr",
+"line": "Benu Recycled",
+"type": "Fabric",
+"variants": 30
+},
+"14467": {
+"id": 7896390107187,
+"handle": "absolut",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 8
+},
+"14516": {
+"id": 7896392138803,
+"handle": "acadenza",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 7
+},
+"14528": {
+"id": 7896394170419,
+"handle": "achat",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"14538": {
+"id": 7896396103731,
+"handle": "adam",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 11
+},
+"14539": {
+"id": 7896397709363,
+"handle": "adele",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 4
+},
+"14426": {
+"id": 7896398135347,
+"handle": "adewisi",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 6
+},
+"14555": {
+"id": 7896398659635,
+"handle": "aim",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 19
+},
+"14534": {
+"id": 7896399577139,
+"handle": "alibi",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"14527": {
+"id": 7896400396339,
+"handle": "alpha",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 13
+},
+"14545": {
+"id": 7896402067507,
+"handle": "alrun",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"2808": {
+"id": 7896403279923,
+"handle": "altubic",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 10
+},
+"2773": {
+"id": 7896404623411,
+"handle": "alvear-palace",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"14513": {
+"id": 7896405770291,
+"handle": "ammirato",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 20
+},
+"14411": {
+"id": 7896407539763,
+"handle": "antoinette",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 3
+},
+"14300": {
+"id": 7896408391731,
+"handle": "apollodor",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 35
+},
+"10747": {
+"id": 7896409800755,
+"handle": "arabis",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"10797": {
+"id": 7896411701299,
+"handle": "argentario-chevron",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"2803": {
+"id": 7896412684339,
+"handle": "ari",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 4
+},
+"14541": {
+"id": 7896413929523,
+"handle": "aron",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"14468": {
+"id": 7896413962291,
+"handle": "artemis",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"2829": {
+"id": 7896414781491,
+"handle": "athens",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 7
+},
+"10750": {
+"id": 7896415371315,
+"handle": "atlantis-palace",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"14537": {
+"id": 7896416321587,
+"handle": "augustina",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 10
+},
+"14029": {
+"id": 7896416845875,
+"handle": "auri",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 30
+},
+"14529": {
+"id": 7896417108019,
+"handle": "authentic",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 2
+},
+"14603": {
+"id": 7896418451507,
+"handle": "avance",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 7
+},
+"14496": {
+"id": 7896420286515,
+"handle": "avanti",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 3
+},
+"14494": {
+"id": 7896421367859,
+"handle": "aviano",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 5
+},
+"14221": {
+"id": 7896423366707,
+"handle": "avienus",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 13
+},
+"10758": {
+"id": 7896423530547,
+"handle": "baleno",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 4
+},
+"2805": {
+"id": 7896424153139,
+"handle": "barre",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 6
+},
+"14507": {
+"id": 7896426315827,
+"handle": "belle-epoque",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 6
+},
+"14410": {
+"id": 7896427364403,
+"handle": "beluna",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 25
+},
+"14551": {
+"id": 7896427659315,
+"handle": "bonus",
+"line": "Christian Fischbacher",
+"type": "Fabric",
+"variants": 6
+}
+}
\ No newline at end of file
diff --git a/push_draft.py b/push_draft.py
new file mode 100644
index 0000000..734b402
--- /dev/null
+++ b/push_draft.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+"""Create the staged Christian Fischbacher catalog as DRAFT products on the LIVE store.
+Steve-approved 2026-07-22: Draft (not published), NO price (trade-gated feed), sub-line
+collections created + all kept in main. Idempotent via manifest — safe to re-run.
+Cost: $0 (Shopify Admin API has no per-call charge)."""
+import json, os, time, sys, subprocess, urllib.request, urllib.error
+
+STORE = "designer-laboratory-sandbox.myshopify.com"
+API = "2024-10"
+ENV = "/Users/macstudio3/Projects/secrets-manager/.env"
+TOKEN = next(l.split("=",1)[1].strip() for l in open(ENV) if l.startswith("SHOPIFY_ADMIN_TOKEN="))
+BASE = f"https://{STORE}/admin/api/{API}"
+HDR = {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"}
+MANIFEST = "/Users/macstudio3/Projects/fischbacher-onboard/data/pushed_manifest.json"
+LINE_TAG_COLLECTIONS = {"Collezione Italia": "collezione-italia", "Benu Recycled": "benu-recycled"}
+
+def api(method, path, body=None):
+    for attempt in range(6):
+        try:
+            req = urllib.request.Request(BASE+path, method=method,
+                    data=json.dumps(body).encode() if body else None, headers=HDR)
+            with urllib.request.urlopen(req, timeout=60) as r:
+                return json.load(r)
+        except urllib.error.HTTPError as e:
+            if e.code == 429:
+                time.sleep(2 + attempt*2); continue
+            print(f"   HTTP {e.code} {method} {path}: {e.read()[:200]}", file=sys.stderr)
+            raise
+        except Exception as ex:
+            if attempt < 5: time.sleep(1+attempt); continue
+            raise
+    raise RuntimeError("retries exhausted")
+
+def load_rows():
+    sql = ("SELECT json_agg(row_to_json(t)) FROM (SELECT mfr_sku,pattern_name,product_type,"
+           "source_category,pdp_url,all_images,colorways,colorway_count FROM fischbacher_catalog "
+           "ORDER BY product_type DESC, source_category, pattern_name) t;")
+    out = subprocess.run(["psql","host=/tmp dbname=dw_unified","-tAc",sql],
+                         capture_output=True,text=True)
+    return json.loads(out.stdout.strip())
+
+manifest = json.load(open(MANIFEST)) if os.path.exists(MANIFEST) else {}
+rows = load_rows()
+LIMIT = int(os.environ.get("LIMIT","0"))
+if LIMIT: rows = rows[:LIMIT]
+print(f"Loaded {len(rows)} staged rows; {len(manifest)} already pushed", file=sys.stderr)
+
+created = skipped = failed = 0
+for i, r in enumerate(rows, 1):
+    sku = r["mfr_sku"]
+    if sku in manifest:
+        skipped += 1; continue
+    line = r["source_category"]
+    tags = ["Christian Fischbacher", line, r["product_type"], "Fischbacher-staged-2026-07"]
+    imgs = [{"src": u} for u in (r.get("all_images") or [])][:15]
+    colorways = r.get("colorways") or []
+    seen, variants = set(), []
+    for cw in colorways:
+        opt = cw.split(".",1)[1] if "." in cw else cw
+        base_opt = opt; n = 1
+        while opt in seen:  # ensure unique option value
+            n += 1; opt = f"{base_opt}-{n}"
+        seen.add(opt)
+        variants.append({"option1": opt, "sku": cw, "price": "0.00",
+                         "inventory_management": None, "taxable": True})
+    if not variants:
+        variants = [{"option1": "Default", "sku": sku, "price": "0.00"}]
+    product = {"product": {
+        "title": r["pattern_name"],
+        "vendor": "Christian Fischbacher",
+        "product_type": r["product_type"],
+        "status": "draft",
+        "tags": ", ".join(tags),
+        "options": [{"name": "Colorway"}],
+        "variants": variants,
+        "images": imgs,
+        "metafields": [
+            {"namespace":"fischbacher","key":"mfr_sku","value":sku,"type":"single_line_text_field"},
+            {"namespace":"fischbacher","key":"line","value":line,"type":"single_line_text_field"},
+        ] + ([{"namespace":"fischbacher","key":"source_pdp","value":r["pdp_url"],"type":"url"}] if r.get("pdp_url") else []),
+    }}
+    try:
+        res = api("POST", "/products.json", product)
+        pid = res["product"]["id"]
+        manifest[sku] = {"id": pid, "handle": res["product"]["handle"],
+                         "line": line, "type": r["product_type"], "variants": len(variants)}
+        created += 1
+        if created % 20 == 0:
+            json.dump(manifest, open(MANIFEST,"w"), indent=0)
+            print(f"   [{i}/{len(rows)}] created={created} skipped={skipped} failed={failed}", file=sys.stderr)
+    except Exception as ex:
+        failed += 1
+        print(f"   FAIL {sku} ({r['pattern_name']}): {ex}", file=sys.stderr)
+    time.sleep(0.55)  # ~2 req/sec
+
+json.dump(manifest, open(MANIFEST,"w"), indent=0)
+print(f"PRODUCTS DONE: created={created} skipped={skipped} failed={failed} total_manifest={len(manifest)}", file=sys.stderr)
+
+# --- sub-line smart collections (tag rule) + keep-in-main is automatic via vendor rule ---
+existing = {c["handle"]: c["id"] for c in api("GET","/smart_collections.json?limit=250").get("smart_collections",[])}
+for tag, handle in LINE_TAG_COLLECTIONS.items():
+    if handle in existing:
+        print(f"   collection {handle} exists (id {existing[handle]})", file=sys.stderr); continue
+    body = {"smart_collection": {"title": tag, "handle": handle, "disjunctive": False,
+            "rules": [{"column":"tag","relation":"equals","condition":tag}]}}
+    try:
+        res = api("POST","/smart_collections.json", body)
+        print(f"   CREATED collection {handle} id {res['smart_collection']['id']}", file=sys.stderr)
+    except Exception as ex:
+        print(f"   collection FAIL {handle}: {ex}", file=sys.stderr)
+    time.sleep(0.6)
+print("ALL DONE", file=sys.stderr)

← 20b86e5 Add Collezione Italia (60) + Benu Recycled (10) sub-lines; d  ·  back to Fischbacher Onboard  ·  Push all 202 Fischbacher patterns as DRAFT to live store (0 45262da →