Agrégateur de produits québécois — www.fabri-ka.com
Python 38.4%
HTML 30.3%
TypeScript 17.2%
CSS 11%
JavaScript 3.2%
1#!/usr/bin/env python32"""Vague 2 — recatégorisation des produits existants avec les signaux `details`.34Le classifieur (schema.infer_category) tient maintenant compte des options5Shopify et des attributs WooCommerce (matériaux, formats…) stockés dans la6colonne `details` depuis la vague 1. La sync n'actualise la catégorie que7lorsque le hash de contenu change ; ce script applique la nouvelle logique8rétroactivement à tous les produits actifs.910Usage : python3 scripts/recategorize.py [--dry-run]11"""12import argparse13import json14import os15import sys1617ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))18sys.path.insert(0, ROOT)1920from fabrika import db as fdb # noqa: E40221from fabrika.schema import infer_category, details_signal_text # noqa: E402222324def main():25 ap = argparse.ArgumentParser()26 ap.add_argument("--dry-run", action="store_true")27 args = ap.parse_args()2829 con = fdb.connect()30 cur = con.execute("SELECT uid, category, product_type, tags, title, "31 "substr(description,1,200) AS descr, details "32 "FROM products WHERE active=1")33 updates, scanned = [], 034 while True:35 rows = cur.fetchmany(5000)36 if not rows:37 break38 for r in rows:39 scanned += 140 tags = ""41 try:42 tags = " ".join(json.loads(r["tags"] or "[]"))43 except Exception:44 pass45 details = {}46 try:47 details = json.loads(r["details"]) if r["details"] else {}48 except Exception:49 pass50 new = infer_category(r["product_type"] or "", tags, r["title"] or "",51 r["descr"] or "", details_signal_text(details))52 # jamais de rétrogradation vers « autre » : beaucoup de53 # catégories sont héritées de la boutique (enrich_products.py)54 if new != "autre" and new != (r["category"] or ""):55 updates.append((new, r["uid"]))56 print(f"[recat] {scanned} produits actifs analysés, {len(updates)} changements")57 if args.dry_run or not updates:58 from collections import Counter59 c = Counter(u[0] for u in updates)60 print("[recat] top nouvelles catégories:", dict(c.most_common(10)))61 con.close()62 return63 con.close()64 import sqlite3, time65 done = 066 for i in range(0, len(updates), 2000):67 batch = updates[i:i + 2000]68 for attempt in range(20): # la sync massive peut tenir le verrou > 60 s69 try:70 wcon = fdb.connect()71 wcon.executemany("UPDATE products SET category=? WHERE uid=?", batch)72 wcon.commit(); wcon.close()73 done += len(batch)74 break75 except sqlite3.OperationalError as exc:76 print(f" [recat] verrou ({exc}), attente… ({attempt+1})", flush=True)77 time.sleep(15)78 else:79 raise SystemExit("[recat] verrou persistant, abandon")80 print(f"[recat] {done} catégories mises à jour")818283if __name__ == "__main__":84 main()85