← back to Elitis Price 2026

exec/ws3b_bucketB.py

108 lines

#!/usr/bin/env python3
"""WS3b - Bucket B recovery: build the staging colorways whose pattern did NOT
exact-match the catalog only because of ACCENT / CASE / SPACING drift
(e.g. 'Eclat'->'Éclat', 'Panthere'->'Panthère', 'Colosse frutier'->'fruitier').

Strategy (cheaper than re-scraping, per FOLLOWUP_scrape_403.md):
  1. Normalize both sides: casefold + strip accents (NFKD) + collapse whitespace.
  2. Exact-normalized match  -> HIGH confidence -> build.
  3. difflib close match >=0.90 -> FUZZY (single-char typos) -> build, logged FUZZY.
  4. else -> leave for the /elitis scrape (Bucket A / D).

Reuses ws3_drafts.create_one (same DRAFT + Needs-Image + variant recipe). $0.
Steve-approved via AskUserQuestion 2026-07-11 (option C)."""
import sys
import os
import csv
import json
import unicodedata
import difflib

sys.path.insert(0, os.path.dirname(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "lib"))
import ws3_drafts as W

ROOT = os.path.join(os.path.dirname(__file__), "..")
CAT = os.path.join(ROOT, "elitis_catalog.json")
SJ = os.path.join(ROOT, "OUT_577_staging_join.csv")


def norm(s):
    s = unicodedata.normalize("NFKD", s or "")
    s = "".join(c for c in s if not unicodedata.combining(c))
    return " ".join(s.casefold().split())


def build_bucketB():
    cat = json.load(open(CAT))
    sj = list(csv.DictReader(open(SJ)))
    exact_pats = set(e["pattern_name"] for e in cat)
    cat_by_norm = {}
    for e in cat:
        cat_by_norm.setdefault(norm(e["pattern_name"]), []).append(e)
    norm_keys = list(cat_by_norm.keys())

    high, fuzzy, miss = [], [], []
    for r in sj:
        pat = r["pattern"]
        if pat in exact_pats:
            continue  # already handled by WS3 exact pass
        nk = norm(pat)
        ents = cat_by_norm.get(nk)
        conf = "HIGH"
        if not ents:
            close = difflib.get_close_matches(nk, norm_keys, n=1, cutoff=0.90)
            if close:
                ents = cat_by_norm[close[0]]
                conf = f"FUZZY:{difflib.SequenceMatcher(None, nk, close[0]).ratio():.2f}:{close[0]}"
            else:
                miss.append(pat)
                continue
        for e in ents:
            if e.get("on_shopify"):
                continue
            tgt = {
                "list_sku": r["list_sku"], "pattern": e["pattern_name"], "unit": r["unit"],
                "retail": r["retail"], "mfr_sku": e["mfr_sku"], "color": e["color_name"],
                "width": e["width"], "product_type": e["product_type"], "_conf": conf,
                "_list_pat": pat,
            }
            (high if conf == "HIGH" else fuzzy).append(tgt)
    return high, fuzzy, sorted(set(miss))


if __name__ == "__main__":
    high, fuzzy, miss = build_bucketB()
    if "--report" in sys.argv or "--build" not in sys.argv:
        print("Bucket B recovery (accent/case/spacing + fuzzy):")
        print(f"  HIGH  (normalized exact) colorways: {len(high)}")
        print(f"  FUZZY (>=0.90 close)      colorways: {len(fuzzy)}")
        print(f"  still-missing distinct patterns    : {len(miss)} -> route to scrape")
        print("\n  -- FUZZY joins (eyeball these) --")
        for t in fuzzy:
            print(f"    {t['_list_pat']!r} -> {t['pattern']!r}  [{t['_conf']}]  {t['mfr_sku']}")
        print("\n  -- still missing (Bucket A / D scrape) --")
        for p in miss:
            print(f"    {p!r}")
        sys.exit(0)

    # --build
    seen = W.seen_dw()
    n_ok = n_skip = n_fail = 0
    allt = high + fuzzy
    for i, t in enumerate(allt):
        if W.dw_sku(t["mfr_sku"]) in seen:
            n_skip += 1
            continue
        st = W.create_one(t)
        if st == "OK":
            n_ok += 1
        elif st.startswith("FAIL"):
            n_fail += 1
        else:
            n_skip += 1
        if (i + 1) % 20 == 0:
            print(f"  ...{i+1}/{len(allt)} ok={n_ok} skip={n_skip} fail={n_fail}")
    print(f"WS3b DONE: ok={n_ok} skip={n_skip} fail={n_fail} "
          f"(high={len(high)} fuzzy={len(fuzzy)} still-missing={len(miss)})")