← back to Fischbacher Onboard

push_draft.py

113 lines

#!/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:
            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)