#!/usr/bin/env python3 """Vague 2 — recatégorisation des produits existants avec les signaux `details`. Le classifieur (schema.infer_category) tient maintenant compte des options Shopify et des attributs WooCommerce (matériaux, formats…) stockés dans la colonne `details` depuis la vague 1. La sync n'actualise la catégorie que lorsque le hash de contenu change ; ce script applique la nouvelle logique rétroactivement à tous les produits actifs. Usage : python3 scripts/recategorize.py [--dry-run] """ import argparse import json import os import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) from fabrika import db as fdb # noqa: E402 from fabrika.schema import infer_category, details_signal_text # noqa: E402 def main(): ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() con = fdb.connect() cur = con.execute("SELECT uid, category, product_type, tags, title, " "substr(description,1,200) AS descr, details " "FROM products WHERE active=1") updates, scanned = [], 0 while True: rows = cur.fetchmany(5000) if not rows: break for r in rows: scanned += 1 tags = "" try: tags = " ".join(json.loads(r["tags"] or "[]")) except Exception: pass details = {} try: details = json.loads(r["details"]) if r["details"] else {} except Exception: pass new = infer_category(r["product_type"] or "", tags, r["title"] or "", r["descr"] or "", details_signal_text(details)) # jamais de rétrogradation vers « autre » : beaucoup de # catégories sont héritées de la boutique (enrich_products.py) if new != "autre" and new != (r["category"] or ""): updates.append((new, r["uid"])) print(f"[recat] {scanned} produits actifs analysés, {len(updates)} changements") if args.dry_run or not updates: from collections import Counter c = Counter(u[0] for u in updates) print("[recat] top nouvelles catégories:", dict(c.most_common(10))) con.close() return con.close() import sqlite3, time done = 0 for i in range(0, len(updates), 2000): batch = updates[i:i + 2000] for attempt in range(20): # la sync massive peut tenir le verrou > 60 s try: wcon = fdb.connect() wcon.executemany("UPDATE products SET category=? WHERE uid=?", batch) wcon.commit(); wcon.close() done += len(batch) break except sqlite3.OperationalError as exc: print(f" [recat] verrou ({exc}), attente… ({attempt+1})", flush=True) time.sleep(15) else: raise SystemExit("[recat] verrou persistant, abandon") print(f"[recat] {done} catégories mises à jour") if __name__ == "__main__": main()