← back to Japan Enrich

fix_sangetsu_stragglers.py

93 lines

#!/usr/bin/env python3
"""
Fix the last ~7 Sangetsu patterns the main backfill couldn't complete: they DO
have colorway SKUs in the 'Full Collection' attribute (e.g. 2321-01…2321-08) but
the vendor published only ONE shared cover image (COVER-…jpg), no per-colorway
swatches — so the SKU→image prefix match found nothing and skipped them.

Here we accept that reality: fill colorway_skus from the attribute and map every
colorway to the shared cover image, so the pattern renders (real SKU chips + an
image) instead of a blank card. Atomic temp+rename; only touches still-empty rows.

Usage: fix_sangetsu_stragglers.py
"""
import json, os, re, time, urllib.request, urllib.error

ROOT = os.path.dirname(__file__)
STAGING = os.path.join(ROOT, "staging", "sangetsu-staging.jsonl")
API = "https://www.sangetsu-goodrich.co.th/wp-json/wc/store/v1"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) japan-enrich/sangetsu-stragglers"
DIM = re.compile(r"-\d+x\d+(?=\.\w+$)")


def get(path, tries=3):
    for i in range(tries):
        try:
            req = urllib.request.Request(API + path, headers={"User-Agent": UA, "Accept": "application/json"})
            with urllib.request.urlopen(req, timeout=45) as r:
                return json.loads(r.read().decode("utf-8"))
        except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
            if i == tries - 1:
                return None
            time.sleep(1.2 * (i + 1))
    return None


def cover_and_skus(url):
    slug = (url or "").rstrip("/").rsplit("/", 1)[-1]
    plist = get(f"/products?slug={slug}")
    if not (isinstance(plist, list) and plist):
        return None, None, None
    p = plist[0]
    skus = []
    for a in p.get("attributes") or []:
        terms = [t.get("name", "").strip() for t in (a.get("terms") or []) if t.get("name")]
        if len(terms) > len(skus):
            skus = terms
    cover = None
    for im in (p.get("images") or []):
        if im.get("src"):
            cover = DIM.sub("", im["src"])
            break
    if not cover:                                   # fall back to any variation image
        var = get(f"/products?type=variation&parent={p['id']}&per_page=1") or []
        for v in (var if isinstance(var, list) else []):
            for im in (v.get("images") or []):
                if im.get("src"):
                    cover = DIM.sub("", im["src"]); break
    pm = re.match(r"^[A-Za-z]+", skus[0]) if skus else None
    return skus, cover, (pm.group(0) if pm else None)


def main():
    rows = [json.loads(l) for l in open(STAGING) if l.strip()]
    empties = [r for r in rows if not r.get("colorway_skus")]
    print(f"stragglers: {len(empties)} still-empty patterns")
    fixed = 0
    for r in empties:
        skus, cover, prefix = cover_and_skus(r.get("source_url"))
        if skus and cover:
            r["colorway_skus"] = skus
            r["colorway_count"] = len(skus)
            r["sku_images"] = {s: cover for s in skus}      # shared cover (vendor has no per-colorway swatch)
            r["cover_only"] = True
            r["backfilled"] = True
            if prefix:
                r["mfr_prefix"] = prefix
            fixed += 1
            print(f"  {(r.get('pattern') or '?'):22} +{len(skus)} SKUs (shared cover)")
        else:
            print(f"  {(r.get('pattern') or '?'):22} — still no data")
        time.sleep(0.25)
    tmp = STAGING + ".tmp"
    with open(tmp, "w") as f:
        for r in rows:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")
    os.replace(tmp, STAGING)
    still = sum(1 for r in rows if not r.get("colorway_skus"))
    print(f"\nfixed {fixed}; still-empty now {still}  → {STAGING}")


if __name__ == "__main__":
    main()