← 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()