# ----------------------------------------------------------------------------- # Food-Ka — connecteur Aubut (aubut.ca — grossiste cash & carry, Montréal) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # Site maison (Vue.js) : les grilles de catégories (/produits/?p=N, # 32 tuiles/page) embarquent un composant dont # l'attribut :product contient TOUT le produit en JSON (prix, format, solde, # inventaire, image) — parsing trivial et fiable. # ----------------------------------------------------------------------------- from __future__ import annotations import json from bs4 import BeautifulSoup from ..schema import Product from .base import BaseConnector BASE = "https://www.aubut.ca" # (slug de catégorie, catégorie canonique) CATEGORIES = [ ("fruits-et-legumes-9", "Fruits et légumes"), ("viandes-poissons-frais-8", "Viandes et volailles"), ("charcuteries-7", "Charcuteries et fromages"), ("produits-laitiers-6", "Produits laitiers et œufs"), ("boulangerie-5", "Boulangerie"), ("surgele-4", "Surgelés"), ("epicerie-1", "Garde-manger"), ("boissons-2", "Boissons"), ] def _price(value) -> float | None: """« 31.99 » (chaîne du JSON Aubut) -> float, None si vide/zéro.""" try: price = float(value) except (TypeError, ValueError): return None return price if price > 0 else None class AubutConnector(BaseConnector): source_id = "aubut" max_categories: int | None = None # borne pour les tests pages_per_category: int = 2 # 32 tuiles/page -> ~512 produits max def _parse_tiles(self, html: str, category: str, slug: str) -> list[Product]: soup = BeautifulSoup(html, "html.parser") products: list[Product] = [] for tag in soup.select("app-product-ecommerce[\\:product]"): try: data = json.loads(tag[":product"]) except (ValueError, KeyError): continue name = (data.get("title") or "").strip() pid = str(data.get("product_id") or data.get("id") or "") if not name or not pid: continue # prix « unité » ; *_special > 0 = prix en solde regular = _price(data.get("unite_price")) special = _price(data.get("unite_price_special")) price = special if special is not None else regular if special is None: regular = None # pas de solde -> pas de prix régulier distinct image = data.get("picture_thumb_path") or "" in_stock = None if str(data.get("has_inventaire") or "") in ("0", "1"): in_stock = data.get("has_inventaire") == "1" products.append(Product( source=self.source_id, external_id=pid, url=data.get("detail") or f"{BASE}/produits/details/{data.get('slug', '')}-{pid}", name=name, brand=(data.get("brand_title") or "").strip(), category=category, category_raw=slug, size_label=(data.get("unite_format") or "").strip(), price=price, regular_price=regular, on_sale=special is not None, unit_price_label=(data.get("price_100") or "").strip(), in_stock=in_stock, images=[image] if image.startswith("http") else [], )) return products def fetch(self) -> list[Product]: products: list[Product] = [] seen: set[str] = set() categories = CATEGORIES[: self.max_categories] for slug, category in categories: for page in range(1, self.pages_per_category + 1): url = f"{BASE}/produits/{slug}" if page > 1: url += f"?p={page}" try: html = self.get(url).text except Exception: # une page qui casse ne bloque pas le reste break new = 0 for prod in self._parse_tiles(html, category, slug): if prod.uid not in seen: seen.add(prod.uid) products.append(prod) new += 1 if new == 0: # page vide ou répétée : catégorie épuisée break return products