← 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)})")