# ----------------------------------------------------------------------------- # Food-Ka — connecteur Akhavan (akhavanfood.com — supermarché moyen-oriental, # Montréal / NDG) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # WooCommerce : l'API Store publique (/wp-json/wc/store/v1/products) répond # sans authentification — noms, prix (en cents, minor units), solde, stock, # catégories et images. ~700 produits au total ; on pagine par 100 avec un # plafond max_pages pour garder la synchro modeste. Aucun rendu JS requis. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from bs4 import BeautifulSoup from ..schema import Product, normalize_category BASE = "https://akhavanfood.com" API = f"{BASE}/wp-json/wc/store/v1/products" from .base import BaseConnector # format dans le nom : « Mix Shoor – Kambiz – 670 g (شور مخلوط) » -> « 670 g » _SIZE_IN_NAME_RE = re.compile( r"(\d+(?:[.,]\d+)?)\s*(kg|g|gr|ml|l|lb|lbs|oz)\b", re.IGNORECASE) def _minor(value: str | None, unit: int) -> float | None: """Prix WooCommerce Store API : chaîne en unités mineures (« 599 » = 5,99 $).""" if not value: return None try: price = int(value) / (10 ** unit) except ValueError: return None return price if price > 0 else None class AkhavanConnector(BaseConnector): source_id = "akhavan" per_page: int = 100 max_pages: int = 4 # 4 x 100 = ~400 produits par synchro def _parse_item(self, item: dict) -> Product | None: name = _html.unescape(item.get("name") or "").strip() if not name or not item.get("id"): return None prices = item.get("prices") or {} unit = int(prices.get("currency_minor_unit") or 2) price = _minor(prices.get("price"), unit) regular = _minor(prices.get("regular_price"), unit) if price is None: return None # pas de prix affiché = produit inutilisable cats = [_html.unescape(c.get("name") or "") for c in (item.get("categories") or [])] category_raw = " / ".join(c for c in cats if c) brands = item.get("brands") or [] brand = _html.unescape(brands[0].get("name") or "") if brands else "" m = _SIZE_IN_NAME_RE.search(name) size_label = "" if m: u = m.group(2).lower() size_label = f"{m.group(1)} {'g' if u == 'gr' else u}" desc_html = item.get("short_description") or "" description = BeautifulSoup(desc_html, "html.parser").get_text( " ", strip=True) if desc_html else "" images = [img.get("src") for img in (item.get("images") or []) if img.get("src")] return Product( source=self.source_id, external_id=str(item["id"]), url=item.get("permalink") or f"{BASE}/?p={item['id']}", name=name, brand=brand, category=normalize_category(category_raw), category_raw=category_raw, size_label=size_label, price=price, regular_price=regular if regular and price and regular > price else None, on_sale=bool(item.get("on_sale")), in_stock=item.get("is_in_stock"), description=description, images=images[:3], ) def fetch(self) -> list[Product]: products: list[Product] = [] seen: set[str] = set() for page in range(1, self.max_pages + 1): try: data = self.get( API, params={"per_page": self.per_page, "page": page}).json() except Exception: # une page qui casse ne bloque pas le reste break if not isinstance(data, list) or not data: break for item in data: prod = self._parse_item(item) if prod is not None and prod.uid not in seen: seen.add(prod.uid) products.append(prod) if len(data) < self.per_page: break return products