SPB Git

spb/food-ka Public

Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com

Python 57.7% TypeScript 24.9% CSS 16.7% HTML 0.6%
4.1 KB · 114 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Food-Ka — connecteur Akhavan (akhavanfood.com — supermarché moyen-oriental,3# Montréal / NDG)4# Auteur : Simon-Pierre Boucher — contact@spboucher.ai5# WooCommerce : l'API Store publique (/wp-json/wc/store/v1/products) répond6# sans authentification — noms, prix (en cents, minor units), solde, stock,7# catégories et images. ~700 produits au total ; on pagine par 100 avec un8# plafond max_pages pour garder la synchro modeste. Aucun rendu JS requis.9# -----------------------------------------------------------------------------10from __future__ import annotations1112import html as _html13import re1415from bs4 import BeautifulSoup1617from ..schema import Product, normalize_category1819BASE = "https://akhavanfood.com"20API = f"{BASE}/wp-json/wc/store/v1/products"2122from .base import BaseConnector2324# format dans le nom : « Mix Shoor – Kambiz – 670 g (شور مخلوط) » -> « 670 g »25_SIZE_IN_NAME_RE = re.compile(26    r"(\d+(?:[.,]\d+)?)\s*(kg|g|gr|ml|l|lb|lbs|oz)\b", re.IGNORECASE)272829def _minor(value: str | None, unit: int) -> float | None:30    """Prix WooCommerce Store API : chaîne en unités mineures (« 599 » = 5,99 $)."""31    if not value:32        return None33    try:34        price = int(value) / (10 ** unit)35    except ValueError:36        return None37    return price if price > 0 else None383940class AkhavanConnector(BaseConnector):41    source_id = "akhavan"4243    per_page: int = 10044    max_pages: int = 4          # 4 x 100 = ~400 produits par synchro4546    def _parse_item(self, item: dict) -> Product | None:47        name = _html.unescape(item.get("name") or "").strip()48        if not name or not item.get("id"):49            return None5051        prices = item.get("prices") or {}52        unit = int(prices.get("currency_minor_unit") or 2)53        price = _minor(prices.get("price"), unit)54        regular = _minor(prices.get("regular_price"), unit)55        if price is None:56            return None            # pas de prix affiché = produit inutilisable5758        cats = [_html.unescape(c.get("name") or "")59                for c in (item.get("categories") or [])]60        category_raw = " / ".join(c for c in cats if c)6162        brands = item.get("brands") or []63        brand = _html.unescape(brands[0].get("name") or "") if brands else ""6465        m = _SIZE_IN_NAME_RE.search(name)66        size_label = ""67        if m:68            u = m.group(2).lower()69            size_label = f"{m.group(1)} {'g' if u == 'gr' else u}"7071        desc_html = item.get("short_description") or ""72        description = BeautifulSoup(desc_html, "html.parser").get_text(73            " ", strip=True) if desc_html else ""7475        images = [img.get("src") for img in (item.get("images") or [])76                  if img.get("src")]7778        return Product(79            source=self.source_id,80            external_id=str(item["id"]),81            url=item.get("permalink") or f"{BASE}/?p={item['id']}",82            name=name,83            brand=brand,84            category=normalize_category(category_raw),85            category_raw=category_raw,86            size_label=size_label,87            price=price,88            regular_price=regular if regular and price and regular > price else None,89            on_sale=bool(item.get("on_sale")),90            in_stock=item.get("is_in_stock"),91            description=description,92            images=images[:3],93        )9495    def fetch(self) -> list[Product]:96        products: list[Product] = []97        seen: set[str] = set()98        for page in range(1, self.max_pages + 1):99            try:100                data = self.get(101                    API, params={"per_page": self.per_page, "page": page}).json()102            except Exception:   # une page qui casse ne bloque pas le reste103                break104            if not isinstance(data, list) or not data:105                break106            for item in data:107                prod = self._parse_item(item)108                if prod is not None and prod.uid not in seen:109                    seen.add(prod.uid)110                    products.append(prod)111            if len(data) < self.per_page:112                break113        return products114