← back to All Designerwallcoverings
scripts/build-data.py
135 lines
#!/usr/bin/env python3
"""Build data/vendors.json for all.designerwallcoverings.com.
Sources (local dw_unified mirror, read-only):
vendor_registry — every vendor id (the "all vendor ids" requirement)
shopify_products — live ACTIVE counts + sample images per front-facing vendor
Front-facing rules enforced here:
- display name = private_label_name when is_private_label, else vendor_name
- parenthetical source notes stripped ("York (Jeffrey Stevens source)" -> handled
by private_label_name; leftover "(...)" stripped defensively)
- BANNED upstream names never ship: Brewster, York, WallQuest, NewWall,
Command54, Justin David, Nicolette Mayer, Seabrook, Chesapeake, NextWall,
Desima, Carlsten. A registry row whose display name still contains one is
dropped (its products already surface under the private label).
"""
import json, re, subprocess, sys, os
DB = "postgresql:///dw_unified?host=/tmp"
OUT = os.path.join(os.path.dirname(__file__), "..", "data", "vendors.json")
BANNED = re.compile(
r"brewster|york|wallquest|wall\s*quest|newwall|new\s*wall\b|command\s*54|"
r"justin\s*david|nicolette\s*mayer|seabrook|chesapeake|nextwall|desima|carlsten",
re.I,
)
def q(sql):
r = subprocess.run(["psql", DB, "-tA", "-c", sql], capture_output=True, text=True)
if r.returncode != 0:
sys.exit(f"psql failed: {r.stderr}")
return r.stdout.strip()
# 1. Live store: per-vendor active count + 4 newest sample images
live = json.loads(q("""
SELECT COALESCE(json_agg(t), '[]') FROM (
SELECT vendor,
count(*) AS active_count,
(SELECT json_agg(u) FROM (
SELECT image_url AS u FROM shopify_products sp2
WHERE sp2.vendor = sp.vendor AND upper(sp2.status)='ACTIVE'
AND sp2.image_url IS NOT NULL AND sp2.image_url <> ''
ORDER BY sp2.created_at_shopify DESC NULLS LAST LIMIT 4
) s) AS images,
max(created_at_shopify) AS newest
FROM shopify_products sp
WHERE upper(status)='ACTIVE' AND vendor IS NOT NULL AND vendor <> ''
GROUP BY vendor
) t;"""))
# 2. Registry: every vendor id
reg = json.loads(q("""
SELECT COALESCE(json_agg(t), '[]') FROM (
SELECT id, vendor_name, vendor_code, is_private_label, private_label_name,
is_active, shopify_count, total_products
FROM vendor_registry
) t;"""))
def display_name(row):
name = (row.get("private_label_name") or "").strip() if row.get("is_private_label") else ""
name = name or (row.get("vendor_name") or "").strip()
name = re.sub(r"\s*\(.*?\)\s*", " ", name).strip() # strip "(... source)" notes
return name
live_by_name = { (v["vendor"] or "").strip().lower(): v for v in live }
vendors = {}
def upsert(name, **kw):
key = name.strip().lower()
# front-facing: never carry vendor's own website — links go into the DW store only
v = vendors.setdefault(key, {
"name": name.strip(), "registry_ids": [], "active_count": 0,
"images": [], "is_private_label": False,
})
for ids in kw.pop("registry_ids", []):
if ids not in v["registry_ids"]:
v["registry_ids"].append(ids)
for k, val in kw.items():
if k == "active_count":
v[k] = max(v[k], val or 0)
elif k == "images":
v[k] = v[k] or (val or [])
elif val and not v.get(k):
v[k] = val
return v
# INTERNAL lines — deliberately NEVER front-facing (config/internal-lines.json). Their vendor
# names are dropped from this front-facing vendor directory, same as BANNED. (Steve 2026-07-09)
INTERNAL_VENDORS = {"fromental", "gracie", "zuber", "crezana design"}
for _p in (os.path.join(os.path.dirname(__file__), "..", "..", "Designer-Wallcoverings", "shopify", "config", "internal-lines.json"),
"/root/Projects/Designer-Wallcoverings/shopify/config/internal-lines.json"):
try:
with open(_p) as _f:
_j = json.load(_f)
if isinstance(_j.get("vendors"), list) and _j["vendors"]:
INTERNAL_VENDORS = {str(x).strip().lower() for x in _j["vendors"]}
break
except Exception:
pass
dropped = []
for row in reg:
name = display_name(row)
if not name or BANNED.search(name) or name.strip().lower() in INTERNAL_VENDORS:
dropped.append(row.get("vendor_name"))
continue
lv = live_by_name.get(name.lower())
upsert(name,
registry_ids=[row["id"]],
active_count=(lv or {}).get("active_count", 0),
images=(lv or {}).get("images"),
is_private_label=bool(row.get("is_private_label")))
# Live store vendors with no registry row still belong in a front-facing directory
for lname, lv in live_by_name.items():
if BANNED.search(lv["vendor"] or "") or (lv["vendor"] or "").strip().lower() in INTERNAL_VENDORS:
dropped.append(lv["vendor"])
continue
if lname not in vendors:
upsert(lv["vendor"], registry_ids=[], active_count=lv["active_count"], images=lv["images"])
out = sorted(vendors.values(), key=lambda v: (-v["active_count"], v["name"].lower()))
for v in out:
v["shop_url"] = ("https://www.designerwallcoverings.com/collections/vendors?q="
+ v["name"].replace(" ", "+"))
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w") as f:
json.dump({"generated_note": "built from dw_unified mirror", "vendors": out}, f)
n_live = sum(1 for v in out if v["active_count"] > 0)
print(f"vendors={len(out)} with_live_products={n_live} dropped_banned={len(dropped)}")
for d in dropped[:15]:
print(" dropped:", d)