[object Object]

← back to Fischbacher Onboard

Onboard Christian Fischbacher: feed-first stage of 133 patterns (116 fabric + 17 wallcovering) to dw_unified.fischbacher_catalog

4493f41e69de4ccc34031e30de1395ed0aa7135b · 2026-07-22 07:23:19 -0700 · Steve Abrams

Files touched

Diff

commit 4493f41e69de4ccc34031e30de1395ed0aa7135b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 07:23:19 2026 -0700

    Onboard Christian Fischbacher: feed-first stage of 133 patterns (116 fabric + 17 wallcovering) to dw_unified.fischbacher_catalog
---
 .gitignore     |   6 +++
 fetch_stage.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 128 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1dbb2ae
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+data/*.jsonl
diff --git a/fetch_stage.py b/fetch_stage.py
new file mode 100644
index 0000000..a59a87b
--- /dev/null
+++ b/fetch_stage.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+"""Christian Fischbacher feed-first onboarder (Magento GraphQL, $0).
+Pulls the CF fabric line + Wall Coverings from fischbacher1819.com's open GraphQL
+endpoint, classifies wallcovering-vs-fabric, and stages into dw_unified.fischbacher_catalog.
+SCRAPE + STAGE ONLY — no Shopify push.
+"""
+import json, time, urllib.request, subprocess, sys
+
+GQL = "https://fischbacher1819.com/graphql"
+UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
+
+# category_uid -> (product_type, source_category)
+CATS = {
+    "MzA=": ("Fabric", "Fabrics > Christian Fischbacher"),
+    "NDI=": ("Wallcovering", "Wall Coverings"),
+}
+
+def gql(query):
+    req = urllib.request.Request(GQL, data=json.dumps({"query": query}).encode(),
+                                 headers={"Content-Type": "application/json", "User-Agent": UA})
+    with urllib.request.urlopen(req, timeout=45) as r:
+        return json.load(r)
+
+def fetch_category(uid):
+    items, page = [], 1
+    while True:
+        q = ('{products(filter:{category_uid:{eq:"%s"}},pageSize:50,currentPage:%d){'
+             'total_count items{sku name url_key description{html} short_description{html} '
+             'price_range{minimum_price{regular_price{value currency}}} '
+             'image{url label} media_gallery{url} categories{name} '
+             '...on ConfigurableProduct{variants{product{sku name}}}}}}' % (uid, page))
+        d = gql(q)["data"]["products"]
+        items.extend(d["items"])
+        total = d["total_count"]
+        if len(items) >= total or not d["items"]:
+            break
+        page += 1
+        time.sleep(0.4)
+    return items
+
+rows = []
+for uid, (ptype, srccat) in CATS.items():
+    got = fetch_category(uid)
+    print(f"  {srccat}: {len(got)} patterns", file=sys.stderr)
+    for p in got:
+        variants = [v["product"]["sku"] for v in (p.get("variants") or []) if v.get("product")]
+        imgs = [g["url"] for g in (p.get("media_gallery") or []) if g.get("url")]
+        primary = (p.get("image") or {}).get("url") or (imgs[0] if imgs else None)
+        price = ((p.get("price_range") or {}).get("minimum_price") or {}).get("regular_price") or {}
+        rows.append({
+            "mfr_sku": p["sku"],
+            "pattern_name": p["name"],
+            "product_type": ptype,
+            "source_category": srccat,
+            "pdp_url": f"https://fischbacher1819.com/{p['url_key']}.html" if p.get("url_key") else None,
+            "image_url": primary,
+            "all_images": imgs,
+            "colorways": variants,
+            "colorway_count": len(variants),
+            "description_html": (p.get("description") or {}).get("html") or "",
+            "short_description": (p.get("short_description") or {}).get("html") or "",
+            "price_value": price.get("value"),
+            "price_currency": price.get("currency"),
+        })
+
+with open("data/fischbacher_staged.jsonl", "w") as f:
+    for r in rows:
+        f.write(json.dumps(r) + "\n")
+print(f"TOTAL rows: {len(rows)}", file=sys.stderr)
+
+# --- stage to dw_unified ---
+ddl = """
+CREATE TABLE IF NOT EXISTS fischbacher_catalog (
+  id             SERIAL PRIMARY KEY,
+  vendor         TEXT NOT NULL DEFAULT 'Christian Fischbacher',
+  mfr_sku        TEXT UNIQUE NOT NULL,
+  pattern_name   TEXT,
+  product_type   TEXT,                 -- Wallcovering | Fabric
+  source_category TEXT,
+  pdp_url        TEXT,
+  image_url      TEXT,
+  all_images     JSONB,
+  colorways      JSONB,
+  colorway_count INT,
+  description_html TEXT,
+  short_description TEXT,
+  price_value    NUMERIC,
+  price_currency TEXT,
+  palette        JSONB,                -- populated by enrich-ai-tags (pending, paid)
+  scraped_at     TIMESTAMPTZ DEFAULT now(),
+  method         TEXT DEFAULT 'feed-first:magento-graphql'
+);
+"""
+def psql(sql, inp=None):
+    return subprocess.run(["psql", "host=/tmp dbname=dw_unified", "-v", "ON_ERROR_STOP=1", "-c", sql] if inp is None
+                          else ["psql", "host=/tmp dbname=dw_unified", "-v", "ON_ERROR_STOP=1", "-c", sql],
+                          input=inp, capture_output=True, text=True)
+
+r = psql(ddl)
+if r.returncode: print("DDL ERROR:", r.stderr, file=sys.stderr); sys.exit(1)
+
+# Build a \copy via COPY FROM STDIN as CSV
+import csv, io
+buf = io.StringIO()
+w = csv.writer(buf)
+for row in rows:
+    w.writerow([
+        row["mfr_sku"], row["pattern_name"], row["product_type"], row["source_category"],
+        row["pdp_url"], row["image_url"], json.dumps(row["all_images"]), json.dumps(row["colorways"]),
+        row["colorway_count"], row["description_html"], row["short_description"],
+        row["price_value"] if row["price_value"] is not None else "",
+        row["price_currency"] or "",
+    ])
+copy_sql = ("BEGIN; TRUNCATE fischbacher_catalog RESTART IDENTITY; "
+            "COPY fischbacher_catalog(mfr_sku,pattern_name,product_type,source_category,pdp_url,"
+            "image_url,all_images,colorways,colorway_count,description_html,short_description,"
+            "price_value,price_currency) FROM STDIN WITH (FORMAT csv); COMMIT;")
+p = subprocess.run(["psql", "host=/tmp dbname=dw_unified", "-v", "ON_ERROR_STOP=1", "-c", copy_sql],
+                   input=buf.getvalue(), capture_output=True, text=True)
+print("COPY stdout:", p.stdout.strip(), file=sys.stderr)
+if p.returncode: print("COPY ERROR:", p.stderr, file=sys.stderr); sys.exit(1)
+print("STAGED OK", file=sys.stderr)

(oldest)  ·  back to Fischbacher Onboard  ·  Add Collezione Italia (60) + Benu Recycled (10) sub-lines; d 20b86e5 →