← back to Designer Wallcoverings

onboarding/sangetsu-lilycolor/scripts/sangetsu-storeapi-enrich.py

93 lines

#!/usr/bin/env python3
"""Sangetsu detail backfill from the WooCommerce Store API.

The sitemap scraper's page-regex leaves specs sparse (source pages are thin).
The Store API's product `description` carries a richer prose+spec block, so we
reuse japan-enrich's proven parse_spec()/apply_spec() to fill spec fields AND
capture the marketing Overview text (which the page scrape misses — only 4/623).

Reversible: read-only public fetch + atomic rewrite of staging/sangetsu-staging.jsonl.
Runs over ALL patterns (not just missing-both). $0 (no paid API). No ollama.
Banned-word guard: never emit "Wallpaper" -> "Wallcovering".
"""
import json, os, re, sys, time, html

HERE = os.path.dirname(os.path.abspath(__file__))
PROJ = os.path.dirname(HERE)
STAGING = os.path.join(PROJ, "staging", "sangetsu-staging.jsonl")

# reuse the proven japan-enrich parser
sys.path.insert(0, os.path.expanduser("~/Projects/japan-enrich"))
from scrape_sangetsu_specs import get, slug_of, clean_text, parse_spec, apply_spec  # noqa: E402


def extract_overview(desc_html):
    t = clean_text(desc_html)
    if not t:
        return None
    # take text after an "Overview" header up to the spec block
    m = re.search(r"\bOverview\b", t, re.I)
    body = t[m.end():] if m else t
    body = re.split(r"(?:Technical\s+Specifications|Specifications)\b", body, 1, re.I)[0].strip(" .:-")
    if not body:
        return None
    body = body[:600].strip()
    # banned-word guard (case-preserving, all cases — never emit "wallpaper")
    body = re.sub(r"WALLPAPERS", "WALLCOVERINGS", body)
    body = re.sub(r"WALLPAPER", "WALLCOVERING", body)
    body = re.sub(r"Wallpapers", "Wallcoverings", body)
    body = re.sub(r"Wallpaper", "Wallcovering", body)
    body = re.sub(r"wallpapers", "wallcoverings", body)
    body = re.sub(r"wallpaper", "wallcovering", body)
    return body or None


def main():
    rows = [json.loads(l) for l in open(STAGING) if l.strip()]
    limit = int(sys.argv[sys.argv.index("--limit") + 1]) if "--limit" in sys.argv else len(rows)
    before_spec = sum(1 for r in rows if (r.get("spec") or {}).get("width") or (r.get("spec") or {}).get("type"))
    before_ov = sum(1 for r in rows if str(r.get("overview") or "").strip())

    hit = miss = 0
    for i, r in enumerate(rows[:limit]):
        slug = slug_of(r.get("source_url"))
        d = get(f"/products?slug={slug}")
        if not d:
            miss += 1
            if i % 40 == 0:
                print(f"[{i+1}/{limit}] {r.get('pattern')}: no store-api", flush=True)
            continue
        desc = d[0].get("description", "")
        parsed = parse_spec(desc)
        apply_spec(r, parsed)
        ov = extract_overview(desc)
        if ov and not str(r.get("overview") or "").strip():
            r["overview"] = ov
        hit += 1
        if i % 40 == 0:
            sp = r.get("spec") or {}
            print(f"[{i+1}/{limit}] {r.get('pattern')}: w={sp.get('width')} t={sp.get('type')} mat={sp.get('material')} ov={'Y' if ov else '-'}", flush=True)
        time.sleep(0.15)

    # atomic write-back
    tmp = STAGING + ".tmp"
    with open(tmp, "w") as f:
        for r in rows:
            f.write(json.dumps(r) + "\n")
    os.replace(tmp, STAGING)

    after_spec = sum(1 for r in rows if (r.get("spec") or {}).get("width") or (r.get("spec") or {}).get("type"))
    after_ov = sum(1 for r in rows if str(r.get("overview") or "").strip())
    def cov(k):
        return sum(1 for r in rows if str((r.get("spec") or {}).get(k) or "").strip())
    print(json.dumps({
        "patterns": len(rows), "store_api_hit": hit, "miss": miss,
        "spec_width_or_type": f"{before_spec} -> {after_spec}",
        "overview": f"{before_ov} -> {after_ov}",
        "coverage": {k: cov(k) for k in ("width", "type", "material", "weight", "backing", "match", "repeat")},
    }, indent=2))


if __name__ == "__main__":
    main()