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%
8.2 KB

# Food·Ka

# Tous les prix d'épicerie du Québec. Un seul endroit.

www.food-ka.com

Python FastAPI React SQLite PWA

Bannières Connecteurs Produits Couverture

Agrégateur indépendant de produits d'épicerie — chaque produit avec son prix courant, son prix régulier, son prix unitaire comparable ($/100 g) et un lien direct vers la fiche originale de la bannière. Toujours à jour, automatiquement.


# Pourquoi Food-Ka ?

Comparer les prix d'épicerie au Québec, c'est ouvrir Metro, IGA, Maxi, Super C, Provigo, Walmart… chacun avec sa propre navigation, son propre panier, son propre format. Food-Ka retourne le problème : un connecteur dédié par bannière visite chaque site, normalise chaque produit vers un schéma unique, et détecte les changements de prix en continu.

Les épiceries n'offrent pas de webhooks. Food-Ka reproduit l'équivalent : synchronisation périodique + hash de contenu → nouveaux produits, changements de prix et retraits détectés automatiquement. Chaque variation de prix est historisée (price_log) — les soldes deviennent traçables.

# L'architecture en 30 secondes

flowchart LR
    subgraph Sources["Bannières d'épicerie"]
        S1["Metro · Super C · IGA/Voilà<br/>Maxi · Provigo · Walmart<br/>Mayrand · Avril · PA · Tau<br/>… 24 connecteurs actifs"]
    end
    subgraph FoodKa["Food-Ka"]
        C["Connecteurs<br/><i>1 adaptateur / bannière</i>"] --> N["Normalisation<br/><i>schéma Product unique</i>"]
        N --> D[("SQLite<br/>hash + diff + prix")]
        D --> A["API FastAPI<br/>/api/products · /api/facets"]
        A --> F["React 18 + Vite<br/>PWA mobile"]
    end
    W["⏱ Watcher (PM2)"] -.-> C
    S1 --> C
    F --> U["🛒 Consommateur"]
Couche Rôle Fichiers
Connecteurs 1 module Python par bannière : HTML rendu serveur, API JSON ouvertes (Shopify products.json, WooCommerce Store API), __NEXT_DATA__ (Loblaw), ou Scrapfly (anti-bot ASP + rendu JS) / Firecrawl pour les sites protégés foodka/connectors/*.py
Schéma Product standardisé : nom, marque, format, prix, prix régulier, prix unitaire $/100 g, catégorie canonique, images foodka/schema.py
Diff engine Upsert par hash de contenu — nouveau / modifié / disparu (désactivé), historique de prix foodka/db.py
API Filtres catégorie / bannière / marque / prix / soldes / recherche, tris (prix, prix unitaire, rabais), facettes, stats foodka/web.py
Frontend Design « éditorial sharp » : Space Grotesk, ombres décalées, accent lime, ticker temps réel, fiches produit avec comparaison inter-bannières, PWA installable frontend/

# Démarrage rapide

bash
git clone https://github.com/spboucher-ai/food-ka.git && cd food-ka

# Backend
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

# Frontend
cd frontend && npm install && npm run build && cd ..

# Sites protégés (anti-bot) et rendu JavaScript
cat > .env <<'ENV'
SCRAPFLY_API_KEY=scp-live-votre-cle
FIRECRAWL_API_KEY=fc-votre-cle
ENV

# Ingestion puis service
.venv/bin/python run.py sync            # toutes les sources (ou: run.py sync metro iga)
.venv/bin/python run.py serve 8080      # → http://localhost:8080
.venv/bin/python run.py watch 360       # resynchronisation en boucle (minutes)

# Ajouter une bannière (≈ 30 lignes)

L'enregistrement est auto-découvrant : déposez un module dans foodka/connectors/, c'est tout — aucun fichier partagé à modifier.

python
# foodka/connectors/mon_epicerie.py
from ..schema import Product, normalize_category, parse_price
from .base import BaseConnector

class MonEpicerieConnector(BaseConnector):
    source_id = "mon_epicerie"

    def fetch(self) -> list[Product]:
        html = self.get("https://mon-epicerie.ca/produits").text   # throttlé, poli
        # ... ou self.get_scrapfly(url) si le site est protégé ...
        return [Product(
            source=self.source_id, external_id="123",
            url="https://mon-epicerie.ca/produit/123",
            name="Beurre d'arachide croquant", brand="Kraft",
            category=normalize_category("Garde-manger"),
            size_label="500 g", price=parse_price("4,99 $"),
            regular_price=6.49, images=[...],
        )]

Puis : .venv/bin/python run.py sync mon_epicerie — et les produits apparaissent sur le site, avec prix unitaire calculé et comparaison inter-bannières. Ajoutez l'entrée correspondante dans data/sources.json pour la page Sources.

# API

Endpoint Description
GET /api/products?category=&source=&brand=&price_min=&price_max=&on_sale=1&q=&sort= Recherche filtrée (tris : prix, prix unitaire, rabais, nom, récents)
GET /api/products/{uid} Fiche complète + historique de prix + comparaison inter-bannières
GET /api/facets Valeurs distinctes pour construire les filtres
GET /api/sources Registre des bannières + compteurs + dernière sync
GET /api/stats Totaux, soldes actifs, meilleures aubaines, journal de synchronisation
POST /api/sync Déclenche une synchronisation en arrière-plan

# Couverture

Grandes bannières — Metro, Super C, IGA (Voilà), Maxi, Provigo, Walmart Canada, Costco Canada (épicerie livrée), Marché Adonis.

Spécialisées & indépendantes — Mayrand (grossiste), Avril Supermarché Santé, Marché Tau, PA Supermarché, Aubut, Maturin (producteurs québécois), Giant Tiger, Epipresto, Marché Nuvo, La Boîte à Grains, BocoBoco, Aliments Merci, Club Entrepôt, Épiceries LOCO, Akhavan, T&T Supermarket.

Chaque bannière non-connectable est documentée avec sa raison dans data/sources.json (ex. : prix liés à une session Instacart/DoorDash, catalogue sans prix, anti-bot strict).

# Production

Déployé sous PM2 (3 processus) derrière ngrok :

text
food-ka-web    .venv/bin/python run.py serve 8096      # API + frontend
food-ka-sync   .venv/bin/python run.py watch 360       # resync aux 6 h
food-ka-ngrok  ngrok http --url=www.food-ka.com 8096   # tunnel

Philosophie d'exploitation : on ne pousse que le code — le serveur maintient ses données lui-même.

# Principes

  1. Politesse — délai ≥ 0,5 s entre requêtes, périmètre de crawl borné, User-Agent identifié.
  2. Fidélité — aucun prix inventé : si la source n'affiche pas de prix, price = null ; un « prix régulier » incohérent est rejeté.
  3. Traçabilité — chaque fiche renvoie vers le produit original de la bannière, et chaque changement de prix est historisé.
  4. Robustesse — un connecteur qui casse n'affecte jamais les autres (auto-découverte tolérante, try/except par produit, détection de dérive, journal sync_log).

# Auteur

Simon-Pierre Boucher

Email GitHub

Conçu, construit et déployé en une journée — de la recherche de marché (30 bannières recensées et vérifiées) au produit en production.

© 2026 Simon-Pierre Boucher — tous droits réservés.