← back to Fischbacher Onboard
fetch_stage.py
129 lines
#!/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 csv, io, 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, line, priority) — all vendor "Christian Fischbacher".
# Higher priority wins when a SKU is cross-listed (dedup on mfr_sku, not brand):
# most-specific sub-line beats the main CF fabric bucket.
CATS = [
("MTA=", "Fabric", "Benu Recycled", 3),
("OQ==", "Fabric", "Collezione Italia", 2),
("NDI=", "Wallcovering", "Wall Coverings", 2),
("MzA=", "Fabric", "Christian Fischbacher", 1),
]
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
by_sku = {} # mfr_sku -> (priority, row)
for uid, ptype, line, prio in CATS:
got = fetch_category(uid)
print(f" {line}: {len(got)} patterns", file=sys.stderr)
for p in got:
sku = p["sku"]
if sku in by_sku and by_sku[sku][0] >= prio:
continue # keep the more-specific line already recorded
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 {}
by_sku[sku] = (prio, {
"mfr_sku": sku,
"pattern_name": p["name"],
"product_type": ptype,
"source_category": line,
"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"),
})
rows = [r for _, r in by_sku.values()]
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],
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
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)