Docs connecteurs : générateur rejouable + 11 fiches standardisées (6 plateformes, Scrapfly, verdict Ecwid, découverte-registre, enrichissement, INDEX)
Croise data/stores.json (3 224 boutiques) + introspection des connecteurs + BD live (volumétrie/complétude produits, top boutiques, gotchas). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 changed files +965 −0
added
docs/connecteurs/INDEX.md
+48 −0
@@ -0,0 +1,48 @@ | ||
| 1 | +# Fabri-Ka — Connecteurs (documentation standardisée) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +**3224 boutiques** au registre (`data/stores.json`, généré le 2026-08-12), **1 469** activées en base, **294 271 produits actifs**. Un connecteur par PLATEFORME e-commerce (dispatch `fabrika/connectors/__init__.py`), plus un transport de secours Scrapfly et deux pipelines transverses (découverte/registre, enrichissement). | |
| 6 | + | |
| 7 | +## Connecteurs-plateformes | |
| 8 | + | |
| 9 | +| Connecteur | Fiche | Boutiques (registre) | Avec produits | Produits actifs | Prix | Photos | Détails | | |
| 10 | +|---|---|---|---|---|---|---|---| | |
| 11 | +| Connecteur Shopify | [shopify](shopify.md) | 700 | 646 | 207 257 | 99.2 % | 96.5 % | 58.0 % | | |
| 12 | +| Connecteur WooCommerce (Store API) | [woocommerce](woocommerce.md) | 1167 | 395 | 68 668 | 100.0 % | 69.8 % | 33.0 % | | |
| 13 | +| Connecteur Wix Stores | [wix](wix.md) | 300 | 175 | 6 517 | 97.1 % | 99.5 % | 51.5 % | | |
| 14 | +| Connecteur Squarespace | [squarespace](squarespace.md) | 115 | 26 | 810 | 100.0 % | 99.6 % | 0.0 % | | |
| 15 | +| Connecteur Square Online | [square](square.md) | 48 | 32 | 2 713 | 99.7 % | 92.8 % | 100.0 % | | |
| 16 | +| Connecteur générique (JSON-LD / microdata / OG) | [generic](generic.md) | 68 | 24 | 8 306 | 100.0 % | 82.4 % | 0.0 % | | |
| 17 | +| Transport Scrapfly | [scrapfly-transport](scrapfly-transport.md) | — | — | — | — | — | — | | |
| 18 | +| Ecwid (verdict : non couvert) | [ecwid](ecwid.md) | 12 | 0 | 0 | — | — | — | | |
| 19 | + | |
| 20 | +Pipelines transverses : [découverte & registre](decouverte-registre.md) · [enrichissement boutiques](enrichissement-boutiques.md). | |
| 21 | + | |
| 22 | +## Top 15 boutiques par volume | |
| 23 | + | |
| 24 | +| Boutique | Domaine | Plateforme | Produits | Dernier sync | | |
| 25 | +|---|---|---|---|---| | |
| 26 | +| Ici & Là - T-shirts et Souvenirs des régions - Acheter québécois | `ici-la.co` | shopify | 20 077 | 2026-08-18 01:42 | | |
| 27 | +| GAGNON - La Grande Quincaillerie | `gagnonlgq.com` | shopify | 19 521 | 2026-08-18 03:25 | | |
| 28 | +| Pieces de remorques et accessoires - Acheter québécois | `remorquetrailer.com` | woocommerce | 19 358 | 2026-08-17 17:37 | | |
| 29 | +| La Maison du Bleuet | `lamaisondubleuet.com` | shopify | 8 631 | 2026-08-17 17:07 | | |
| 30 | +| Bijouterie Jodoin | `bijouteriejodoin.com` | shopify | 8 253 | 2026-08-18 03:02 | | |
| 31 | +| Oberson | `oberson.com` | shopify | 7 355 | 2026-08-17 17:38 | | |
| 32 | +| Coopérative La Manne - Goûtez-y! | `cooplamanne.com` | woocommerce | 7 216 | 2026-08-18 03:10 | | |
| 33 | +| LilloJEUX | `lillojeux.ca` | woocommerce | 6 609 | 2026-08-17 16:56 | | |
| 34 | +| Paperole | `paperole.com` | shopify | 4 975 | 2026-08-17 17:35 | | |
| 35 | +| Chic Placard | `chicplacard.ca` | shopify | 4 458 | 2026-08-18 03:05 | | |
| 36 | +| Veille sur toi Inc. - CMAQ | `veillesurtoi.com` | shopify | 4 435 | 2026-08-17 18:03 | | |
| 37 | +| Bijoux Medusa | `bijouxmedusa.com` | shopify | 3 823 | 2026-08-18 03:02 | | |
| 38 | +| Les Ptits Mosüs (Alice et Simone) | `lesptitsmosus.com` | shopify | 3 392 | 2026-08-17 16:59 | | |
| 39 | +| Poches & Fils | `pochesetfils.com` | shopify | 3 241 | 2026-08-12 12:59 | | |
| 40 | +| Biscotte Yarns | `biscotteyarns.com` | shopify | 3 198 | 2026-08-18 03:02 | | |
| 41 | + | |
| 42 | +## Gotchas transverses | |
| 43 | + | |
| 44 | +- **Shopify curl anti-TLS + verrou 0,7 s** : voir [shopify](shopify.md). | |
| 45 | +- **Scrapfly `large_object`** : les grosses réponses arrivent en deux temps — voir [scrapfly-transport](scrapfly-transport.md). | |
| 46 | +- **FTS par lots** (`fabrika/db.py`) : la purge/réinsertion de l'index `products_fts` se fait par lots de 500 uid (1 balayage par lot au lieu de N deletes unitaires). | |
| 47 | +- **Verrou BD** (`fabrika/ingest.py`) : écritures sérialisées via `threading.Lock` — les syncs multi-boutiques sont parallèles côté réseau, séquentiels côté SQLite. | |
| 48 | + | |
added
docs/connecteurs/decouverte-registre.md
+61 −0
@@ -0,0 +1,61 @@ | ||
| 1 | +# Découverte & registre (`data/stores.json`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +Registre généré le **2026-08-12** — **3224 boutiques** (champ `count`), toutes avec id (domaine canonique), plateforme, endpoint catalogue, classe d'origine, preuves et sources de découverte. | |
| 6 | + | |
| 7 | +## Classes d'origine (`origin_class`) | |
| 8 | + | |
| 9 | +| Classe | Boutiques | Signification | | |
| 10 | +|---|---|---| | |
| 11 | +| A | 3155 | fabrication/production au Québec attestée | | |
| 12 | +| C | 19 | marque québécoise (fabrication partielle ou incertaine) | | |
| 13 | +| E | 50 | à requalifier / preuve faible | | |
| 14 | + | |
| 15 | +Confiance : `origin_confidence` (0-1) + `origin_evidence` (texte de preuve, annuaire ou mention sur le site). | |
| 16 | + | |
| 17 | +## Annuaires & sources de découverte (top 15) | |
| 18 | + | |
| 19 | +| Source | Boutiques | | |
| 20 | +|---|---| | |
| 21 | +| `alimentsduquebec_ent` | 752 | | |
| 22 | +| `lesproduitsduquebec_ent` | 329 | | |
| 23 | +| `charlevoix` | 304 | | |
| 24 | +| `zoneboreale` | 264 | | |
| 25 | +| `erabledici` | 251 | | |
| 26 | +| `createursdesaveurs` | 249 | | |
| 27 | +| `icq_resolved` | 132 | | |
| 28 | +| `arretsgourmands` | 126 | | |
| 29 | +| `ambq_profils` | 125 | | |
| 30 | +| `goutezy` | 110 | | |
| 31 | +| `search_sweep_nonfood` | 104 | | |
| 32 | +| `search_sweep_vendors` | 97 | | |
| 33 | +| `search_sweep_cmaq` | 94 | | |
| 34 | +| `marchespublics` | 80 | | |
| 35 | +| `acheterquebecois` | 80 | | |
| 36 | + | |
| 37 | +Statuts de vérification : `verified` : 2141, `probable` : 1083 | |
| 38 | + | |
| 39 | +## Plateformes détectées au registre | |
| 40 | + | |
| 41 | +| Plateforme | Boutiques | | |
| 42 | +|---|---| | |
| 43 | +| `(vide)` | 814 | | |
| 44 | +| `shopify` | 700 | | |
| 45 | +| `woocommerce` | 662 | | |
| 46 | +| `wordpress` | 505 | | |
| 47 | +| `wix` | 300 | | |
| 48 | +| `squarespace` | 115 | | |
| 49 | +| `square` | 48 | | |
| 50 | +| `generic` | 22 | | |
| 51 | +| `lightspeed` | 17 | | |
| 52 | +| `snipcart` | 14 | | |
| 53 | +| `ecwid` | 12 | | |
| 54 | +| `prestashop` | 7 | | |
| 55 | +| `magento` | 7 | | |
| 56 | +| `bigcommerce` | 1 | | |
| 57 | + | |
| 58 | +## Re-sondage (`scripts/reprobe_stores.py`) | |
| 59 | + | |
| 60 | +Le re-sondage est **additif** : il réactive des boutiques à 0 produit (migrations de plateforme, Square Online devenu connectable en vague 2) sans jamais toucher aux boutiques déjà actives. Il met à jour `stores.json`, `data/verify_cache/`, `data/enriched/verified.jsonl` et la table `stores`. | |
| 61 | + | |
added
docs/connecteurs/ecwid.md
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +# Ecwid — verdict : NON COUVERT | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Verdict | |
| 6 | + | |
| 7 | +**Ecwid n'est pas connectable** en l'état (décision documentée, README) : | |
| 8 | + | |
| 9 | +- Le storefront Ecwid est **100 % client-side** : aucun rendu serveur du | |
| 10 | + catalogue, ni sur les *instant sites* ni via le plugin WordPress — le | |
| 11 | + connecteur générique (JSON-LD/microdata) ne voit rien. | |
| 12 | +- L'**API REST v3 exige un token secret** par boutique ; le token `pub…` | |
| 13 | + présent dans `script.js` est générique et refusé (**403**). | |
| 14 | +- Conséquence : les boutiques Ecwid restent au registre (`enabled=0`, | |
| 15 | + plateforme détectée) en attente d'une éventuelle voie d'accès. | |
| 16 | + | |
| 17 | +## Boutiques Ecwid recensées (12) | |
| 18 | + | |
| 19 | +| Boutique | Domaine | Statut registre | | |
| 20 | +|---|---|---| | |
| 21 | +| Spiritueux Alpha Tango | `alphatango.ca` | désactivée (pas d'endpoint) | | |
| 22 | +| Boswell Brasserie Artisanale | `brasserieboswell.com` | désactivée (pas d'endpoint) | | |
| 23 | +| Camp de Base Brasserie Artisanale | `brasseriecampdebase.com` | désactivée (pas d'endpoint) | | |
| 24 | +| Frescadel International Inc. | `frescadel.com` | désactivée (pas d'endpoint) | | |
| 25 | +| Fromagerie de l'Ãle-aux-Grues (Agrilait s.e.c.) | `fromagesileauxgrues.com` | désactivée (pas d'endpoint) | | |
| 26 | +| Créateurs | `lepleasant.com` | désactivée (pas d'endpoint) | | |
| 27 | +| Les Récoltes Marcotte - Goûtez-y! | `lesrecoltesmarcotte.com` | désactivée (pas d'endpoint) | | |
| 28 | +| Le Temps des Cigales | `letempsdescigales.com` | désactivée (pas d'endpoint) | | |
| 29 | +| 9431-4770 Québec inc. F.A.S.N.C Moulin de Charlevoix. | `moulin-charlevoix.net` | désactivée (pas d'endpoint) | | |
| 30 | +| Domaine du Cap | `produitsducap.com` | désactivée (pas d'endpoint) | | |
| 31 | +| Sentier des Caps de Charlevoix | `sentierdescaps.com` | désactivée (pas d'endpoint) | | |
| 32 | +| Fromagerie St-Guillaume | `stguillaume.com` | désactivée (pas d'endpoint) | | |
added
docs/connecteurs/enrichissement-boutiques.md
+16 −0
@@ -0,0 +1,16 @@ | ||
| 1 | +# Enrichissement des boutiques | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +Deux scripts rejouables complètent la table `stores` (colonnes additives `logo_url`, `cover_url`, `description_meta`, `shipping_info`). | |
| 6 | + | |
| 7 | +## `scripts/enrich_stores.py` — logo / couverture / description | |
| 8 | + | |
| 9 | +- Couverture actuelle : **logo 3220/3224** (99.9 %), **cover 1938/3224** (60.1 %). | |
| 10 | +- Cache disque `data/enrich_cache/` : 3224 fichiers (page d'accueil analysée une seule fois ; Scrapfly en secours pour les 403). | |
| 11 | + | |
| 12 | +## `scripts/enrich_shipping.py` — politiques de livraison (vague 2) | |
| 13 | + | |
| 14 | +- Couverture actuelle : **shipping_info 271/3224** (8.4 %) — ciblé sur les boutiques productives (`product_count > 0`). | |
| 15 | +- Cache disque `data/enrich_cache/shipping/` : 364 fichiers (échecs mémorisés pour ne pas re-marteler les sites). | |
| 16 | + | |
added
docs/connecteurs/generic.md
+54 −0
@@ -0,0 +1,54 @@ | ||
| 1 | +# Connecteur générique (JSON-LD / microdata / OG) (`generic`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique / endpoint | |
| 6 | + | |
| 7 | +- Sitemap → pages produit → extraction du balisage produit (JSON-LD, microdata, Open Graph). Sert PrestaShop, Magento, BigCommerce, Lightspeed, Snipcart et les sites custom (endpoint sentinelle `__generic__`). | |
| 8 | +- Module : `fabrika/connectors/generic.py` | |
| 9 | +- Plateformes BD routées ici : `generic`, `prestashop`, `magento`, `bigcommerce`, `lightspeed`, `snipcart` | |
| 10 | + | |
| 11 | +**En-tête du module :** | |
| 12 | + | |
| 13 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 14 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 15 | +> connectors/generic.py : connecteur universel par sitemap + extraction du | |
| 16 | +> balisage produit rendu côté serveur (JSON-LD schema.org Product, microdata, | |
| 17 | +> Open Graph product, blobs PrestaShop/Magento). Couvre PrestaShop, Magento, | |
| 18 | +> BigCommerce, WordPress non-Woo et sites ecommerce sur mesure. Scrapfly en | |
| 19 | +> secours pour l'anti-bot. Rendu client-only (Ecwid) non couvert ; Square | |
| 20 | +> Online est couvert par connectors/square.py (vague 2). | |
| 21 | + | |
| 22 | +## Boutiques rattachées | |
| 23 | + | |
| 24 | +- Registre `data/stores.json` : **68** boutiques | |
| 25 | +- En base (table `stores`) : **30** activées, **24** avec produits | |
| 26 | + | |
| 27 | +**Top 5 par volume :** | |
| 28 | + | |
| 29 | +| Boutique | Domaine | Produits | Dernier sync | Statut | | |
| 30 | +|---|---|---|---|---| | |
| 31 | +| Boutique Nomade | `boutiquenomade.net` | 807 | 2026-08-18 03:03 | ok | | |
| 32 | +| Alpha Signa | `alphasigna.com` | 800 | 2026-08-18 02:55 | ok | | |
| 33 | +| Bijouterie Concept | `bijouterieconcept.com` | 800 | 2026-08-12 18:23 | ok | | |
| 34 | +| Mondou | `mondou.com` | 799 | 2026-08-12 17:56 | ok | | |
| 35 | +| Bijouterie Pénélope | `bijouteriepenelope.com` | 798 | 2026-08-18 03:00 | ok | | |
| 36 | + | |
| 37 | +## Volumétrie & complétude (BD live) | |
| 38 | + | |
| 39 | +| Indicateur | Valeur | | |
| 40 | +|---|---| | |
| 41 | +| Produits actifs | 8 306 | | |
| 42 | +| Prix | 100.0 % | | |
| 43 | +| Photos | 82.4 % | | |
| 44 | +| Description | 95.5 % | | |
| 45 | +| Détails (variantes/attributs) | 0.0 % | | |
| 46 | +| — dont avis (rating/review_count) | 0.0 % | | |
| 47 | +| — dont poids/dimensions | 0.0 % | | |
| 48 | + | |
| 49 | +## Gotchas | |
| 50 | + | |
| 51 | +- Scrapfly/Firecrawl en secours anti-bot quand l'accès direct échoue (403/429). | |
| 52 | +- Rendu 100 % client (Ecwid) non couvert — voir la fiche [verdict Ecwid](ecwid.md). | |
| 53 | + | |
| 54 | +Voir aussi : [INDEX](INDEX.md) · [transport Scrapfly](scrapfly-transport.md) · [découverte & registre](decouverte-registre.md). | |
added
docs/connecteurs/scrapfly-transport.md
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +# Transport de secours Scrapfly (`scrapfly-transport`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique | |
| 6 | + | |
| 7 | +- `POST` → `https://api.scrapfly.io/scrape` avec `asp=true` (anti-bot) et `country=ca`, `render_js` optionnel. | |
| 8 | +- Employé **en dernier recours** quand l'accès direct échoue (403/429/HTML au lieu de JSON) ; chaque appel consomme des crédits et les échecs définitifs sont mémorisés. | |
| 9 | +- Clé : variable d'environnement `SCRAPFLY_API_KEY` (`.env`). | |
| 10 | +- Throttle interne : verrou global, 0,5 s minimum entre appels. | |
| 11 | + | |
| 12 | +**En-tête du module :** | |
| 13 | + | |
| 14 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 15 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 16 | +> connectors/scrapfly.py : transport de secours via l'API Scrapfly (anti-bot, | |
| 17 | +> rendu JS optionnel). Utilisé quand l'accès direct échoue (403/429/HTML | |
| 18 | +> au lieu de JSON). Chaque appel consomme des crédits : on ne l'emploie | |
| 19 | +> qu'en dernier recours et on mémorise les échecs définitifs. | |
| 20 | + | |
| 21 | +## Gotchas | |
| 22 | + | |
| 23 | +- **`large_object`** : au-delà d'une certaine taille, Scrapfly ne renvoie pas le contenu mais une URL `https://api.scrapfly.io/scrape/large_object/…` qu'il faut suivre (2e GET, avec la clé en paramètre) pour obtenir le corps réel — géré depuis le commit `4c85b1f` (repli Scrapfly robuste). | |
| 24 | +- Utilisé aussi par les scripts d'enrichissement (`enrich_stores.py`) pour les pages d'accueil en 403. | |
| 25 | + | |
added
docs/connecteurs/shopify.md
+51 −0
@@ -0,0 +1,51 @@ | ||
| 1 | +# Connecteur Shopify (`shopify`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique / endpoint | |
| 6 | + | |
| 7 | +- `GET /products.json?limit=250&page=N` — catalogue JSON public de chaque boutique (aucune clé requise). | |
| 8 | +- Module : `fabrika/connectors/shopify.py` | |
| 9 | +- Plateformes BD routées ici : `shopify` | |
| 10 | + | |
| 11 | +**En-tête du module :** | |
| 12 | + | |
| 13 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 14 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 15 | +> connectors/shopify.py : connecteur générique Shopify — catalogue complet | |
| 16 | +> via l'endpoint public /products.json (paginé, 250 produits/page). | |
| 17 | + | |
| 18 | +## Boutiques rattachées | |
| 19 | + | |
| 20 | +- Registre `data/stores.json` : **700** boutiques | |
| 21 | +- En base (table `stores`) : **662** activées, **646** avec produits | |
| 22 | + | |
| 23 | +**Top 5 par volume :** | |
| 24 | + | |
| 25 | +| Boutique | Domaine | Produits | Dernier sync | Statut | | |
| 26 | +|---|---|---|---|---| | |
| 27 | +| Ici & Là - T-shirts et Souvenirs des régions - Acheter québécois | `ici-la.co` | 20 077 | 2026-08-18 01:42 | ok | | |
| 28 | +| GAGNON - La Grande Quincaillerie | `gagnonlgq.com` | 19 521 | 2026-08-18 03:25 | ok | | |
| 29 | +| La Maison du Bleuet | `lamaisondubleuet.com` | 8 631 | 2026-08-17 17:07 | ok | | |
| 30 | +| Bijouterie Jodoin | `bijouteriejodoin.com` | 8 253 | 2026-08-18 03:02 | ok | | |
| 31 | +| Oberson | `oberson.com` | 7 355 | 2026-08-17 17:38 | ok | | |
| 32 | + | |
| 33 | +## Volumétrie & complétude (BD live) | |
| 34 | + | |
| 35 | +| Indicateur | Valeur | | |
| 36 | +|---|---| | |
| 37 | +| Produits actifs | 207 257 | | |
| 38 | +| Prix | 99.2 % | | |
| 39 | +| Photos | 96.5 % | | |
| 40 | +| Description | 94.0 % | | |
| 41 | +| Détails (variantes/attributs) | 58.0 % | | |
| 42 | +| — dont avis (rating/review_count) | 0.0 % | | |
| 43 | +| — dont poids/dimensions | 0.1 % | | |
| 44 | + | |
| 45 | +## Gotchas | |
| 46 | + | |
| 47 | +- **Transport curl anti-TLS** : l'empreinte TLS de python-requests déclenche le 429 de Shopify sous volume ; les GET passent par `curl -sS --compressed` en sous-processus. | |
| 48 | +- **Verrou global 0,7 s** (`_MIN_INTERVAL = 0.7` + lock inter-threads) entre deux requêtes Shopify, avec retry — throttle poli à l'échelle du procédé, pas par boutique. | |
| 49 | +- Les variantes/prix/images viennent du même JSON ; `details` stocke variants/options/published_at. | |
| 50 | + | |
| 51 | +Voir aussi : [INDEX](INDEX.md) · [transport Scrapfly](scrapfly-transport.md) · [découverte & registre](decouverte-registre.md). | |
added
docs/connecteurs/square.md
+54 −0
@@ -0,0 +1,54 @@ | ||
| 1 | +# Connecteur Square Online (`square`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique / endpoint | |
| 6 | + | |
| 7 | +- IDs `user_id`/`site_id` extraits du HTML de la page d'accueil → `GET /app/store/api/v13/editor/users/{user}/sites/{site}/store-pages/…/products` (API storefront publique). | |
| 8 | +- Module : `fabrika/connectors/square.py` | |
| 9 | +- Plateformes BD routées ici : `square` | |
| 10 | + | |
| 11 | +**En-tête du module :** | |
| 12 | + | |
| 13 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 14 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 15 | +> connectors/square.py : connecteur générique Square Online (Weebly). | |
| 16 | +> Les sites Square Online exposent une API JSON publique côté storefront : | |
| 17 | +> /app/store/api/v13/editor/users/<user_id>/sites/<site_id>/products | |
| 18 | +> Les identifiants user_id / site_id sont imprimés dans le HTML de chaque | |
| 19 | +> page (config JS `user_id: '…'` / `site_id: '…'`). Pagination via | |
| 20 | +> meta.pagination.total_pages, 100 produits/page. | |
| 21 | + | |
| 22 | +## Boutiques rattachées | |
| 23 | + | |
| 24 | +- Registre `data/stores.json` : **48** boutiques | |
| 25 | +- En base (table `stores`) : **32** activées, **32** avec produits | |
| 26 | + | |
| 27 | +**Top 5 par volume :** | |
| 28 | + | |
| 29 | +| Boutique | Domaine | Produits | Dernier sync | Statut | | |
| 30 | +|---|---|---|---|---| | |
| 31 | +| BIZZ coopérative d’alimentation locale | `bizzcoop.com` | 558 | 2026-08-18 03:02 | ok | | |
| 32 | +| À Outrance | `aoutrance.com` | 305 | 2026-08-18 02:55 | ok | | |
| 33 | +| Misskikooz | `melissacharette.com` | 280 | 2026-08-18 02:53 | ok | | |
| 34 | +| ÉCONOMUSÉE® du cidrier | `charlevoixenligne.com` | 217 | 2026-08-18 03:03 | ok | | |
| 35 | +| Chaga aromatisé - Acheter québécois | `sorcieredepouvoir.com` | 214 | 2026-08-18 02:53 | ok | | |
| 36 | + | |
| 37 | +## Volumétrie & complétude (BD live) | |
| 38 | + | |
| 39 | +| Indicateur | Valeur | | |
| 40 | +|---|---| | |
| 41 | +| Produits actifs | 2 713 | | |
| 42 | +| Prix | 99.7 % | | |
| 43 | +| Photos | 92.8 % | | |
| 44 | +| Description | 89.0 % | | |
| 45 | +| Détails (variantes/attributs) | 100.0 % | | |
| 46 | +| — dont avis (rating/review_count) | 3.8 % | | |
| 47 | +| — dont poids/dimensions | 0.0 % | | |
| 48 | + | |
| 49 | +## Gotchas | |
| 50 | + | |
| 51 | +- Connecteur ajouté en **vague 2** (commit `778d2b6`) — a rendu connectables les boutiques Square jusque-là à 0 produit. | |
| 52 | +- Deux requêtes minimum par boutique (HTML d'accueil + API). | |
| 53 | + | |
| 54 | +Voir aussi : [INDEX](INDEX.md) · [transport Scrapfly](scrapfly-transport.md) · [découverte & registre](decouverte-registre.md). | |
added
docs/connecteurs/squarespace.md
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +# Connecteur Squarespace (`squarespace`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique / endpoint | |
| 6 | + | |
| 7 | +- `GET /shop|/boutique|/store?format=json` — rendu JSON natif des pages boutique Squarespace, pagination par collection. | |
| 8 | +- Module : `fabrika/connectors/squarespace.py` | |
| 9 | +- Plateformes BD routées ici : `squarespace` | |
| 10 | + | |
| 11 | +**En-tête du module :** | |
| 12 | + | |
| 13 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 14 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 15 | +> connectors/squarespace.py : connecteur générique Squarespace Commerce — | |
| 16 | +> pages collection en JSON natif (?format=json). | |
| 17 | + | |
| 18 | +## Boutiques rattachées | |
| 19 | + | |
| 20 | +- Registre `data/stores.json` : **115** boutiques | |
| 21 | +- En base (table `stores`) : **27** activées, **26** avec produits | |
| 22 | + | |
| 23 | +**Top 5 par volume :** | |
| 24 | + | |
| 25 | +| Boutique | Domaine | Produits | Dernier sync | Statut | | |
| 26 | +|---|---|---|---|---| | |
| 27 | +| Apéro à zéro | `aperoazero.ca` | 238 | 2026-08-18 02:55 | ok | | |
| 28 | +| Les Mains d'ici | `lesmainsdici.com` | 106 | 2026-08-12 17:53 | error | | |
| 29 | +| Nos Cabanes S.E.N.C | `noscabanes.com` | 78 | 2026-08-12 17:53 | error | | |
| 30 | +| Hugo & Clay | `hugoandclay.com` | 42 | 2026-08-18 03:24 | ok | | |
| 31 | +| Distillerie COMONT | `comont.ca` | 40 | 2026-08-18 03:06 | ok | | |
| 32 | + | |
| 33 | +## Volumétrie & complétude (BD live) | |
| 34 | + | |
| 35 | +| Indicateur | Valeur | | |
| 36 | +|---|---| | |
| 37 | +| Produits actifs | 810 | | |
| 38 | +| Prix | 100.0 % | | |
| 39 | +| Photos | 99.6 % | | |
| 40 | +| Description | 96.3 % | | |
| 41 | +| Détails (variantes/attributs) | 0.0 % | | |
| 42 | +| — dont avis (rating/review_count) | 0.0 % | | |
| 43 | +| — dont poids/dimensions | 0.0 % | | |
| 44 | + | |
| 45 | +## Gotchas | |
| 46 | + | |
| 47 | +- L'URL de la page boutique varie (`/shop`, `/boutique`, `/store`…) : le sondage essaie les slugs usuels et mémorise le bon endpoint. | |
| 48 | + | |
| 49 | +Voir aussi : [INDEX](INDEX.md) · [transport Scrapfly](scrapfly-transport.md) · [découverte & registre](decouverte-registre.md). | |
added
docs/connecteurs/wix.md
+50 −0
@@ -0,0 +1,50 @@ | ||
| 1 | +# Connecteur Wix Stores (`wix`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique / endpoint | |
| 6 | + | |
| 7 | +- `GET /_api/v1/access-tokens` (jeton d'instance public de l'app Wix Stores) puis GraphQL storefront `getFilteredProducts` (catalogue complet, paginé). | |
| 8 | +- Module : `fabrika/connectors/wix.py` | |
| 9 | +- Plateformes BD routées ici : `wix` | |
| 10 | + | |
| 11 | +**En-tête du module :** | |
| 12 | + | |
| 13 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 14 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 15 | +> connectors/wix.py : connecteur générique Wix Stores. | |
| 16 | +> 1) GET /_api/v1/access-tokens -> jeton d'instance public de l'app Wix Stores | |
| 17 | +> 2) GraphQL storefront getFilteredProducts (catalogue complet, paginé) | |
| 18 | + | |
| 19 | +## Boutiques rattachées | |
| 20 | + | |
| 21 | +- Registre `data/stores.json` : **300** boutiques | |
| 22 | +- En base (table `stores`) : **300** activées, **175** avec produits | |
| 23 | + | |
| 24 | +**Top 5 par volume :** | |
| 25 | + | |
| 26 | +| Boutique | Domaine | Produits | Dernier sync | Statut | | |
| 27 | +|---|---|---|---|---| | |
| 28 | +| Bijouterie Alarie | `bijouteriealarie.com` | 901 | 2026-08-18 03:00 | ok | | |
| 29 | +| Savonnerie Histoire de Bulles | `histoiredebulles.com` | 514 | 2026-08-18 03:24 | ok | | |
| 30 | +| CRÉATIONS GLi - CMAQ | `bijouxgli.com` | 203 | 2026-08-18 03:00 | ok | | |
| 31 | +| Savonnerie Bon Bain | `savonneriebonbain.ca` | 180 | 2026-08-17 17:35 | ok | | |
| 32 | +| Semences Nouveau Monde | `semencesnouveaumonde.com` | 178 | 2026-08-17 17:37 | ok | | |
| 33 | + | |
| 34 | +## Volumétrie & complétude (BD live) | |
| 35 | + | |
| 36 | +| Indicateur | Valeur | | |
| 37 | +|---|---| | |
| 38 | +| Produits actifs | 6 517 | | |
| 39 | +| Prix | 97.1 % | | |
| 40 | +| Photos | 99.5 % | | |
| 41 | +| Description | 91.1 % | | |
| 42 | +| Détails (variantes/attributs) | 51.5 % | | |
| 43 | +| — dont avis (rating/review_count) | 0.0 % | | |
| 44 | +| — dont poids/dimensions | 0.0 % | | |
| 45 | + | |
| 46 | +## Gotchas | |
| 47 | + | |
| 48 | +- Le jeton est public mais par site : il faut le ré-extraire à chaque sync (pas de clé persistante). | |
| 49 | + | |
| 50 | +Voir aussi : [INDEX](INDEX.md) · [transport Scrapfly](scrapfly-transport.md) · [découverte & registre](decouverte-registre.md). | |
added
docs/connecteurs/woocommerce.md
+50 −0
@@ -0,0 +1,50 @@ | ||
| 1 | +# Connecteur WooCommerce (Store API) (`woocommerce`) | |
| 2 | + | |
| 3 | +*Généré le 2026-08-18 03:27 par `scripts/gen_connector_docs.py` — fichier produit automatiquement, ne pas éditer à la main.* | |
| 4 | + | |
| 5 | +## Mécanique / endpoint | |
| 6 | + | |
| 7 | +- `GET /wp-json/wc/store/v1/products?per_page=100&page=N` — Store API publique (sert aussi les sites `wordpress` avec Store API active). | |
| 8 | +- Module : `fabrika/connectors/woocommerce.py` | |
| 9 | +- Plateformes BD routées ici : `woocommerce`, `wordpress` | |
| 10 | + | |
| 11 | +**En-tête du module :** | |
| 12 | + | |
| 13 | +> Fabri-Ka — Agrégateur de produits québécois | |
| 14 | +> Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 15 | +> connectors/woocommerce.py : connecteur générique WooCommerce — catalogue | |
| 16 | +> complet via la Store API publique /wp-json/wc/store/v1/products. | |
| 17 | + | |
| 18 | +## Boutiques rattachées | |
| 19 | + | |
| 20 | +- Registre `data/stores.json` : **1167** boutiques | |
| 21 | +- En base (table `stores`) : **418** activées, **395** avec produits | |
| 22 | + | |
| 23 | +**Top 5 par volume :** | |
| 24 | + | |
| 25 | +| Boutique | Domaine | Produits | Dernier sync | Statut | | |
| 26 | +|---|---|---|---|---| | |
| 27 | +| Pieces de remorques et accessoires - Acheter québécois | `remorquetrailer.com` | 19 358 | 2026-08-17 17:37 | ok | | |
| 28 | +| Coopérative La Manne - Goûtez-y! | `cooplamanne.com` | 7 216 | 2026-08-18 03:10 | ok | | |
| 29 | +| LilloJEUX | `lillojeux.ca` | 6 609 | 2026-08-17 16:56 | ok | | |
| 30 | +| Létourno | `letourno.com` | 2 554 | 2026-08-17 16:53 | ok | | |
| 31 | +| Fourrures Grenier | `grenierboutique.ca` | 2 349 | 2026-08-18 03:24 | ok | | |
| 32 | + | |
| 33 | +## Volumétrie & complétude (BD live) | |
| 34 | + | |
| 35 | +| Indicateur | Valeur | | |
| 36 | +|---|---| | |
| 37 | +| Produits actifs | 68 668 | | |
| 38 | +| Prix | 100.0 % | | |
| 39 | +| Photos | 69.8 % | | |
| 40 | +| Description | 61.1 % | | |
| 41 | +| Détails (variantes/attributs) | 33.0 % | | |
| 42 | +| — dont avis (rating/review_count) | 1.4 % | | |
| 43 | +| — dont poids/dimensions | 5.8 % | | |
| 44 | + | |
| 45 | +## Gotchas | |
| 46 | + | |
| 47 | +- Alias `wordpress` : les sites WordPress dont la Store API répond sont routés vers ce connecteur (voir `PLATFORM_CONNECTORS`). | |
| 48 | +- `details` porte attributes/variations, poids et dimensions formatés quand la boutique les publie. | |
| 49 | + | |
| 50 | +Voir aussi : [INDEX](INDEX.md) · [transport Scrapfly](scrapfly-transport.md) · [découverte & registre](decouverte-registre.md). | |
added
scripts/gen_connector_docs.py
+475 −0
@@ -0,0 +1,475 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# ----------------------------------------------------------------------------- | |
| 3 | +# Fabri-Ka — Agrégateur de produits québécois | |
| 4 | +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 5 | +# scripts/gen_connector_docs.py : documentation STANDARDISÉE des connecteurs. | |
| 6 | +# | |
| 7 | +# Génère docs/connecteurs/INDEX.md + une fiche par connecteur-plateforme | |
| 8 | +# (shopify, woocommerce, wix, squarespace, square, generic, | |
| 9 | +# scrapfly-transport, ecwid/verdict) + une fiche « découverte & registre » | |
| 10 | +# (data/stores.json) + une fiche « enrichissement boutiques » | |
| 11 | +# (enrich_stores / enrich_shipping), en croisant : | |
| 12 | +# 1. le registre : data/stores.json (3 224 boutiques) | |
| 13 | +# 2. l'introspection : fabrika/connectors/* + scripts d'enrichissement | |
| 14 | +# 3. la base vivante : data/fabrika.db (stores, products, sync_log) | |
| 15 | +# | |
| 16 | +# REJOUABLE : ré-exécuter le script régénère tout docs/connecteurs/. | |
| 17 | +# Usage : python3 scripts/gen_connector_docs.py | |
| 18 | +# ----------------------------------------------------------------------------- | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import collections | |
| 22 | +import datetime | |
| 23 | +import json | |
| 24 | +import re | |
| 25 | +import sqlite3 | |
| 26 | +import sys | |
| 27 | +from pathlib import Path | |
| 28 | + | |
| 29 | +ROOT = Path(__file__).resolve().parent.parent | |
| 30 | +sys.path.insert(0, str(ROOT)) | |
| 31 | + | |
| 32 | +DATA = ROOT / "data" | |
| 33 | +OUT = ROOT / "docs" / "connecteurs" | |
| 34 | +DB = DATA / "fabrika.db" | |
| 35 | +CONN_DIR = ROOT / "fabrika" / "connectors" | |
| 36 | + | |
| 37 | +NOW = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") | |
| 38 | +GEN_NOTE = ("*Généré le {} par `scripts/gen_connector_docs.py` — fichier " | |
| 39 | + "produit automatiquement, ne pas éditer à la main.*".format(NOW)) | |
| 40 | + | |
| 41 | +# plateformes BD regroupées par connecteur | |
| 42 | +GROUPS: dict[str, list[str]] = { | |
| 43 | + "shopify": ["shopify"], | |
| 44 | + "woocommerce": ["woocommerce", "wordpress"], | |
| 45 | + "wix": ["wix"], | |
| 46 | + "squarespace": ["squarespace"], | |
| 47 | + "square": ["square"], | |
| 48 | + "generic": ["generic", "prestashop", "magento", "bigcommerce", | |
| 49 | + "lightspeed", "snipcart"], | |
| 50 | +} | |
| 51 | + | |
| 52 | +FICHES = { | |
| 53 | + "shopify": dict( | |
| 54 | + titre="Connecteur Shopify", module="shopify.py", | |
| 55 | + endpoint="`GET /products.json?limit=250&page=N` — catalogue JSON public " | |
| 56 | + "de chaque boutique (aucune clé requise).", | |
| 57 | + gotchas=[ | |
| 58 | + "**Transport curl anti-TLS** : l'empreinte TLS de python-requests " | |
| 59 | + "déclenche le 429 de Shopify sous volume ; les GET passent par " | |
| 60 | + "`curl -sS --compressed` en sous-processus.", | |
| 61 | + "**Verrou global 0,7 s** (`_MIN_INTERVAL = 0.7` + lock inter-threads) " | |
| 62 | + "entre deux requêtes Shopify, avec retry — throttle poli à l'échelle " | |
| 63 | + "du procédé, pas par boutique.", | |
| 64 | + "Les variantes/prix/images viennent du même JSON ; `details` stocke " | |
| 65 | + "variants/options/published_at.", | |
| 66 | + ]), | |
| 67 | + "woocommerce": dict( | |
| 68 | + titre="Connecteur WooCommerce (Store API)", module="woocommerce.py", | |
| 69 | + endpoint="`GET /wp-json/wc/store/v1/products?per_page=100&page=N` — " | |
| 70 | + "Store API publique (sert aussi les sites `wordpress` avec " | |
| 71 | + "Store API active).", | |
| 72 | + gotchas=[ | |
| 73 | + "Alias `wordpress` : les sites WordPress dont la Store API répond " | |
| 74 | + "sont routés vers ce connecteur (voir `PLATFORM_CONNECTORS`).", | |
| 75 | + "`details` porte attributes/variations, poids et dimensions " | |
| 76 | + "formatés quand la boutique les publie.", | |
| 77 | + ]), | |
| 78 | + "wix": dict( | |
| 79 | + titre="Connecteur Wix Stores", module="wix.py", | |
| 80 | + endpoint="`GET /_api/v1/access-tokens` (jeton d'instance public de " | |
| 81 | + "l'app Wix Stores) puis GraphQL storefront " | |
| 82 | + "`getFilteredProducts` (catalogue complet, paginé).", | |
| 83 | + gotchas=[ | |
| 84 | + "Le jeton est public mais par site : il faut le ré-extraire à " | |
| 85 | + "chaque sync (pas de clé persistante).", | |
| 86 | + ]), | |
| 87 | + "squarespace": dict( | |
| 88 | + titre="Connecteur Squarespace", module="squarespace.py", | |
| 89 | + endpoint="`GET /shop|/boutique|/store?format=json` — rendu JSON natif " | |
| 90 | + "des pages boutique Squarespace, pagination par collection.", | |
| 91 | + gotchas=[ | |
| 92 | + "L'URL de la page boutique varie (`/shop`, `/boutique`, `/store`…) : " | |
| 93 | + "le sondage essaie les slugs usuels et mémorise le bon endpoint.", | |
| 94 | + ]), | |
| 95 | + "square": dict( | |
| 96 | + titre="Connecteur Square Online", module="square.py", | |
| 97 | + endpoint="IDs `user_id`/`site_id` extraits du HTML de la page d'accueil " | |
| 98 | + "→ `GET /app/store/api/v13/editor/users/{user}/sites/{site}/" | |
| 99 | + "store-pages/…/products` (API storefront publique).", | |
| 100 | + gotchas=[ | |
| 101 | + "Connecteur ajouté en **vague 2** (commit `778d2b6`) — a rendu " | |
| 102 | + "connectables les boutiques Square jusque-là à 0 produit.", | |
| 103 | + "Deux requêtes minimum par boutique (HTML d'accueil + API).", | |
| 104 | + ]), | |
| 105 | + "generic": dict( | |
| 106 | + titre="Connecteur générique (JSON-LD / microdata / OG)", module="generic.py", | |
| 107 | + endpoint="Sitemap → pages produit → extraction du balisage produit " | |
| 108 | + "(JSON-LD, microdata, Open Graph). Sert PrestaShop, Magento, " | |
| 109 | + "BigCommerce, Lightspeed, Snipcart et les sites custom " | |
| 110 | + "(endpoint sentinelle `__generic__`).", | |
| 111 | + gotchas=[ | |
| 112 | + "Scrapfly/Firecrawl en secours anti-bot quand l'accès direct " | |
| 113 | + "échoue (403/429).", | |
| 114 | + "Rendu 100 % client (Ecwid) non couvert — voir la fiche " | |
| 115 | + "[verdict Ecwid](ecwid.md).", | |
| 116 | + ]), | |
| 117 | +} | |
| 118 | + | |
| 119 | +ECWID_VERDICT = """# Ecwid — verdict : NON COUVERT | |
| 120 | + | |
| 121 | +{gen} | |
| 122 | + | |
| 123 | +## Verdict | |
| 124 | + | |
| 125 | +**Ecwid n'est pas connectable** en l'état (décision documentée, README) : | |
| 126 | + | |
| 127 | +- Le storefront Ecwid est **100 % client-side** : aucun rendu serveur du | |
| 128 | + catalogue, ni sur les *instant sites* ni via le plugin WordPress — le | |
| 129 | + connecteur générique (JSON-LD/microdata) ne voit rien. | |
| 130 | +- L'**API REST v3 exige un token secret** par boutique ; le token `pub…` | |
| 131 | + présent dans `script.js` est générique et refusé (**403**). | |
| 132 | +- Conséquence : les boutiques Ecwid restent au registre (`enabled=0`, | |
| 133 | + plateforme détectée) en attente d'une éventuelle voie d'accès. | |
| 134 | + | |
| 135 | +## Boutiques Ecwid recensées ({n}) | |
| 136 | + | |
| 137 | +| Boutique | Domaine | Statut registre | | |
| 138 | +|---|---|---| | |
| 139 | +{rows} | |
| 140 | +""" | |
| 141 | + | |
| 142 | + | |
| 143 | +# --- helpers ------------------------------------------------------------------ | |
| 144 | +def module_header(path: Path) -> str: | |
| 145 | + try: | |
| 146 | + text = path.read_text(encoding="utf-8") | |
| 147 | + except OSError: | |
| 148 | + return "" | |
| 149 | + lines = [] | |
| 150 | + for line in text.splitlines(): | |
| 151 | + if line.startswith("#!"): | |
| 152 | + continue | |
| 153 | + if line.startswith("#"): | |
| 154 | + s = line.lstrip("#").rstrip() | |
| 155 | + if s.startswith(" "): | |
| 156 | + s = s[1:] | |
| 157 | + if set(s) <= {"-", " "}: | |
| 158 | + continue | |
| 159 | + lines.append(s) | |
| 160 | + elif lines: | |
| 161 | + break | |
| 162 | + elif line.strip(): | |
| 163 | + break | |
| 164 | + if not lines: | |
| 165 | + m = re.match(r'\s*(?:"""|\'\'\')(.*?)(?:"""|\'\'\')', text, re.S) | |
| 166 | + if m: | |
| 167 | + lines = [ln.rstrip() for ln in m.group(1).strip().splitlines()] | |
| 168 | + return "\n".join("> " + (ln or "") for ln in lines) | |
| 169 | + | |
| 170 | + | |
| 171 | +def pct(part, whole) -> str: | |
| 172 | + if not whole: | |
| 173 | + return "—" | |
| 174 | + return f"{100.0 * (part or 0) / whole:.1f} %" | |
| 175 | + | |
| 176 | + | |
| 177 | +def nfr(n) -> str: | |
| 178 | + return f"{n:,}".replace(",", " ") if isinstance(n, (int, float)) else str(n) | |
| 179 | + | |
| 180 | + | |
| 181 | +def fmt_ts(ts) -> str: | |
| 182 | + if not ts: | |
| 183 | + return "—" | |
| 184 | + return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") | |
| 185 | + | |
| 186 | + | |
| 187 | +# --- collecte ------------------------------------------------------------------- | |
| 188 | +def load_registry() -> dict: | |
| 189 | + return json.loads((DATA / "stores.json").read_text(encoding="utf-8")) | |
| 190 | + | |
| 191 | + | |
| 192 | +def db_platform_stats(con) -> dict: | |
| 193 | + """Agrégats produits par plateforme BD (une passe).""" | |
| 194 | + rows = con.execute(""" | |
| 195 | + SELECT s.platform AS plat, | |
| 196 | + COUNT(*) AS produits, | |
| 197 | + SUM(p.price IS NOT NULL) AS prix, | |
| 198 | + SUM(p.images IS NOT NULL AND p.images NOT IN ('','[]')) AS photos, | |
| 199 | + SUM(p.details IS NOT NULL) AS details, | |
| 200 | + SUM(p.details LIKE '%average_rating%' | |
| 201 | + OR p.details LIKE '%review_count%') AS rating, | |
| 202 | + SUM(p.details LIKE '%weight%') AS poids, | |
| 203 | + SUM(p.description IS NOT NULL AND p.description<>'') AS descr | |
| 204 | + FROM products p JOIN stores s ON s.id = p.store_id | |
| 205 | + WHERE p.active = 1 GROUP BY s.platform""").fetchall() | |
| 206 | + return {r["plat"]: dict(r) for r in rows} | |
| 207 | + | |
| 208 | + | |
| 209 | +def group_stats(pstats: dict, plats: list[str]) -> dict: | |
| 210 | + keys = ("produits", "prix", "photos", "details", "rating", "poids", "descr") | |
| 211 | + out = {k: 0 for k in keys} | |
| 212 | + for p in plats: | |
| 213 | + s = pstats.get(p) | |
| 214 | + if s: | |
| 215 | + for k in keys: | |
| 216 | + out[k] += s[k] or 0 | |
| 217 | + return out | |
| 218 | + | |
| 219 | + | |
| 220 | +def volumetrie_md(g: dict) -> str: | |
| 221 | + n = g["produits"] or 0 | |
| 222 | + rows = [ | |
| 223 | + ("Produits actifs", nfr(n)), | |
| 224 | + ("Prix", pct(g["prix"], n)), | |
| 225 | + ("Photos", pct(g["photos"], n)), | |
| 226 | + ("Description", pct(g["descr"], n)), | |
| 227 | + ("Détails (variantes/attributs)", pct(g["details"], n)), | |
| 228 | + ("— dont avis (rating/review_count)", pct(g["rating"], n)), | |
| 229 | + ("— dont poids/dimensions", pct(g["poids"], n)), | |
| 230 | + ] | |
| 231 | + return "| Indicateur | Valeur |\n|---|---|\n" + \ | |
| 232 | + "\n".join(f"| {k} | {v} |" for k, v in rows) | |
| 233 | + | |
| 234 | + | |
| 235 | +def top_stores(con, plats: list[str], limit=5): | |
| 236 | + q = ",".join("?" * len(plats)) | |
| 237 | + return con.execute( | |
| 238 | + f"SELECT id, name, product_count, last_sync, last_status FROM stores " | |
| 239 | + f"WHERE platform IN ({q}) AND product_count > 0 " | |
| 240 | + f"ORDER BY product_count DESC LIMIT {limit}", plats).fetchall() | |
| 241 | + | |
| 242 | + | |
| 243 | +def store_counts(con, registry, plats: list[str]) -> tuple[int, int, int]: | |
| 244 | + reg = sum(1 for s in registry["stores"] if (s.get("platform") or "") in plats) | |
| 245 | + q = ",".join("?" * len(plats)) | |
| 246 | + en, prod = con.execute( | |
| 247 | + f"SELECT SUM(enabled=1), SUM(product_count>0) FROM stores " | |
| 248 | + f"WHERE platform IN ({q})", plats).fetchone() | |
| 249 | + return reg, en or 0, prod or 0 | |
| 250 | + | |
| 251 | + | |
| 252 | +# --- fiches plateforme ------------------------------------------------------------ | |
| 253 | +def write_platform(key: str, con, registry, pstats) -> None: | |
| 254 | + f = FICHES[key] | |
| 255 | + plats = GROUPS[key] | |
| 256 | + g = group_stats(pstats, plats) | |
| 257 | + reg, en, prod = store_counts(con, registry, plats) | |
| 258 | + md = [f"# {f['titre']} (`{key}`)", "", GEN_NOTE, "", | |
| 259 | + "## Mécanique / endpoint", "", f"- {f['endpoint']}", | |
| 260 | + f"- Module : `fabrika/connectors/{f['module']}`", | |
| 261 | + f"- Plateformes BD routées ici : " + ", ".join(f"`{p}`" for p in plats), ""] | |
| 262 | + hdr = module_header(CONN_DIR / f["module"]) | |
| 263 | + if hdr: | |
| 264 | + md += ["**En-tête du module :**", "", hdr, ""] | |
| 265 | + md += ["## Boutiques rattachées", "", | |
| 266 | + f"- Registre `data/stores.json` : **{reg}** boutiques", | |
| 267 | + f"- En base (table `stores`) : **{en}** activées, **{prod}** avec produits", "", | |
| 268 | + "**Top 5 par volume :**", "", | |
| 269 | + "| Boutique | Domaine | Produits | Dernier sync | Statut |", | |
| 270 | + "|---|---|---|---|---|"] | |
| 271 | + for r in top_stores(con, plats): | |
| 272 | + md.append(f"| {r['name'] or r['id']} | `{r['id']}` | " | |
| 273 | + f"{nfr(r['product_count'])} | {fmt_ts(r['last_sync'])} | " | |
| 274 | + f"{r['last_status'] or '—'} |") | |
| 275 | + md += ["", "## Volumétrie & complétude (BD live)", "", volumetrie_md(g), "", | |
| 276 | + "## Gotchas", ""] | |
| 277 | + md += [f"- {gtc}" for gtc in f["gotchas"]] | |
| 278 | + md += ["", "Voir aussi : [INDEX](INDEX.md) · " | |
| 279 | + "[transport Scrapfly](scrapfly-transport.md) · " | |
| 280 | + "[découverte & registre](decouverte-registre.md)."] | |
| 281 | + (OUT / f"{key}.md").write_text("\n".join(md) + "\n", encoding="utf-8") | |
| 282 | + | |
| 283 | + | |
| 284 | +def write_scrapfly(con) -> None: | |
| 285 | + md = ["# Transport de secours Scrapfly (`scrapfly-transport`)", "", GEN_NOTE, "", | |
| 286 | + "## Mécanique", "", | |
| 287 | + "- `POST` → `https://api.scrapfly.io/scrape` avec `asp=true` " | |
| 288 | + "(anti-bot) et `country=ca`, `render_js` optionnel.", | |
| 289 | + "- Employé **en dernier recours** quand l'accès direct échoue " | |
| 290 | + "(403/429/HTML au lieu de JSON) ; chaque appel consomme des crédits " | |
| 291 | + "et les échecs définitifs sont mémorisés.", | |
| 292 | + "- Clé : variable d'environnement `SCRAPFLY_API_KEY` (`.env`).", | |
| 293 | + "- Throttle interne : verrou global, 0,5 s minimum entre appels.", ""] | |
| 294 | + hdr = module_header(CONN_DIR / "scrapfly.py") | |
| 295 | + if hdr: | |
| 296 | + md += ["**En-tête du module :**", "", hdr, ""] | |
| 297 | + md += ["## Gotchas", "", | |
| 298 | + "- **`large_object`** : au-delà d'une certaine taille, Scrapfly ne " | |
| 299 | + "renvoie pas le contenu mais une URL " | |
| 300 | + "`https://api.scrapfly.io/scrape/large_object/…` qu'il faut suivre " | |
| 301 | + "(2e GET, avec la clé en paramètre) pour obtenir le corps réel — " | |
| 302 | + "géré depuis le commit `4c85b1f` (repli Scrapfly robuste).", | |
| 303 | + "- Utilisé aussi par les scripts d'enrichissement " | |
| 304 | + "(`enrich_stores.py`) pour les pages d'accueil en 403.", ""] | |
| 305 | + (OUT / "scrapfly-transport.md").write_text("\n".join(md) + "\n", encoding="utf-8") | |
| 306 | + | |
| 307 | + | |
| 308 | +def write_ecwid(con, registry) -> None: | |
| 309 | + stores = [s for s in registry["stores"] if s.get("platform") == "ecwid"] | |
| 310 | + rows = "\n".join( | |
| 311 | + "| {} | `{}` | {} |".format( | |
| 312 | + s.get("name") or s["id"], s["id"], | |
| 313 | + "activée" if s.get("enabled") else "désactivée (pas d'endpoint)") | |
| 314 | + for s in sorted(stores, key=lambda x: x["id"])) | |
| 315 | + (OUT / "ecwid.md").write_text( | |
| 316 | + ECWID_VERDICT.format(gen=GEN_NOTE, n=len(stores), rows=rows), | |
| 317 | + encoding="utf-8") | |
| 318 | + | |
| 319 | + | |
| 320 | +def write_discovery(registry) -> None: | |
| 321 | + ss = registry["stores"] | |
| 322 | + oc = collections.Counter(s.get("origin_class") for s in ss) | |
| 323 | + plat = collections.Counter((s.get("platform") or "(vide)") for s in ss) | |
| 324 | + src = collections.Counter() | |
| 325 | + for s in ss: | |
| 326 | + src.update(s.get("discovery_sources") or []) | |
| 327 | + status = collections.Counter(s.get("status") for s in ss) | |
| 328 | + md = ["# Découverte & registre (`data/stores.json`)", "", GEN_NOTE, "", | |
| 329 | + f"Registre généré le **{registry.get('generated')}** — " | |
| 330 | + f"**{registry.get('count')} boutiques** (champ `count`), toutes avec " | |
| 331 | + "id (domaine canonique), plateforme, endpoint catalogue, classe " | |
| 332 | + "d'origine, preuves et sources de découverte.", "", | |
| 333 | + "## Classes d'origine (`origin_class`)", "", | |
| 334 | + "| Classe | Boutiques | Signification |", "|---|---|---|"] | |
| 335 | + signif = {"A": "fabrication/production au Québec attestée", | |
| 336 | + "B": "transformation/assemblage au Québec", | |
| 337 | + "C": "marque québécoise (fabrication partielle ou incertaine)", | |
| 338 | + "D": "revendeur/distributeur québécois", | |
| 339 | + "E": "à requalifier / preuve faible"} | |
| 340 | + for k in sorted(oc): | |
| 341 | + md.append(f"| {k} | {oc[k]} | {signif.get(k, '—')} |") | |
| 342 | + md += ["", "Confiance : `origin_confidence` (0-1) + `origin_evidence` " | |
| 343 | + "(texte de preuve, annuaire ou mention sur le site).", "", | |
| 344 | + "## Annuaires & sources de découverte (top 15)", "", | |
| 345 | + "| Source | Boutiques |", "|---|---|"] | |
| 346 | + for name, n in src.most_common(15): | |
| 347 | + md.append(f"| `{name}` | {n} |") | |
| 348 | + md += ["", f"Statuts de vérification : " + | |
| 349 | + ", ".join(f"`{k}` : {v}" for k, v in status.most_common()), "", | |
| 350 | + "## Plateformes détectées au registre", "", | |
| 351 | + "| Plateforme | Boutiques |", "|---|---|"] | |
| 352 | + for p, n in plat.most_common(): | |
| 353 | + md.append(f"| `{p}` | {n} |") | |
| 354 | + md += ["", "## Re-sondage (`scripts/reprobe_stores.py`)", ""] | |
| 355 | + hdr = module_header(ROOT / "scripts" / "reprobe_stores.py") | |
| 356 | + if hdr: | |
| 357 | + md += [hdr, ""] | |
| 358 | + md += ["Le re-sondage est **additif** : il réactive des boutiques à " | |
| 359 | + "0 produit (migrations de plateforme, Square Online devenu " | |
| 360 | + "connectable en vague 2) sans jamais toucher aux boutiques déjà " | |
| 361 | + "actives. Il met à jour `stores.json`, `data/verify_cache/`, " | |
| 362 | + "`data/enriched/verified.jsonl` et la table `stores`.", ""] | |
| 363 | + (OUT / "decouverte-registre.md").write_text("\n".join(md) + "\n", encoding="utf-8") | |
| 364 | + | |
| 365 | + | |
| 366 | +def write_enrichment(con) -> None: | |
| 367 | + logo, cover, ship, tot = con.execute( | |
| 368 | + "SELECT SUM(logo_url IS NOT NULL AND logo_url<>'')," | |
| 369 | + " SUM(cover_url IS NOT NULL AND cover_url<>'')," | |
| 370 | + " SUM(shipping_info IS NOT NULL AND shipping_info<>'')," | |
| 371 | + " COUNT(*) FROM stores").fetchone() | |
| 372 | + n_cache = len(list((DATA / "enrich_cache").glob("*.json"))) | |
| 373 | + ship_dir = DATA / "enrich_cache" / "shipping" | |
| 374 | + n_ship = len(list(ship_dir.glob("*.json"))) if ship_dir.exists() else 0 | |
| 375 | + md = ["# Enrichissement des boutiques", "", GEN_NOTE, "", | |
| 376 | + "Deux scripts rejouables complètent la table `stores` (colonnes " | |
| 377 | + "additives `logo_url`, `cover_url`, `description_meta`, " | |
| 378 | + "`shipping_info`).", "", | |
| 379 | + "## `scripts/enrich_stores.py` — logo / couverture / description", ""] | |
| 380 | + hdr = module_header(ROOT / "scripts" / "enrich_stores.py") | |
| 381 | + if hdr: | |
| 382 | + md += [hdr, ""] | |
| 383 | + md += [f"- Couverture actuelle : **logo {logo}/{tot}** ({pct(logo, tot)}), " | |
| 384 | + f"**cover {cover}/{tot}** ({pct(cover, tot)}).", | |
| 385 | + f"- Cache disque `data/enrich_cache/` : {n_cache} fichiers " | |
| 386 | + "(page d'accueil analysée une seule fois ; Scrapfly en secours " | |
| 387 | + "pour les 403).", "", | |
| 388 | + "## `scripts/enrich_shipping.py` — politiques de livraison (vague 2)", ""] | |
| 389 | + hdr = module_header(ROOT / "scripts" / "enrich_shipping.py") | |
| 390 | + if hdr: | |
| 391 | + md += [hdr, ""] | |
| 392 | + md += [f"- Couverture actuelle : **shipping_info {ship}/{tot}** " | |
| 393 | + f"({pct(ship, tot)}) — ciblé sur les boutiques productives " | |
| 394 | + "(`product_count > 0`).", | |
| 395 | + f"- Cache disque `data/enrich_cache/shipping/` : {n_ship} fichiers " | |
| 396 | + "(échecs mémorisés pour ne pas re-marteler les sites).", ""] | |
| 397 | + (OUT / "enrichissement-boutiques.md").write_text("\n".join(md) + "\n", | |
| 398 | + encoding="utf-8") | |
| 399 | + | |
| 400 | + | |
| 401 | +def write_index(con, registry, pstats) -> None: | |
| 402 | + tot_prod, tot_stores = con.execute( | |
| 403 | + "SELECT (SELECT COUNT(*) FROM products WHERE active=1)," | |
| 404 | + " (SELECT COUNT(*) FROM stores WHERE enabled=1)").fetchone() | |
| 405 | + md = ["# Fabri-Ka — Connecteurs (documentation standardisée)", "", GEN_NOTE, "", | |
| 406 | + f"**{len(registry['stores'])} boutiques** au registre " | |
| 407 | + f"(`data/stores.json`, généré le {registry.get('generated')}), " | |
| 408 | + f"**{nfr(tot_stores)}** activées en base, **{nfr(tot_prod)} produits " | |
| 409 | + "actifs**. Un connecteur par PLATEFORME e-commerce (dispatch " | |
| 410 | + "`fabrika/connectors/__init__.py`), plus un transport de secours " | |
| 411 | + "Scrapfly et deux pipelines transverses (découverte/registre, " | |
| 412 | + "enrichissement).", "", | |
| 413 | + "## Connecteurs-plateformes", "", | |
| 414 | + "| Connecteur | Fiche | Boutiques (registre) | Avec produits | " | |
| 415 | + "Produits actifs | Prix | Photos | Détails |", | |
| 416 | + "|---|---|---|---|---|---|---|---|"] | |
| 417 | + for key in GROUPS: | |
| 418 | + g = group_stats(pstats, GROUPS[key]) | |
| 419 | + reg, en, prod = store_counts(con, registry, GROUPS[key]) | |
| 420 | + md.append(f"| {FICHES[key]['titre']} | [{key}]({key}.md) | {reg} | " | |
| 421 | + f"{prod} | {nfr(g['produits'])} | " | |
| 422 | + f"{pct(g['prix'], g['produits'])} | " | |
| 423 | + f"{pct(g['photos'], g['produits'])} | " | |
| 424 | + f"{pct(g['details'], g['produits'])} |") | |
| 425 | + md += ["| Transport Scrapfly | [scrapfly-transport](scrapfly-transport.md) " | |
| 426 | + "| — | — | — | — | — | — |", | |
| 427 | + "| Ecwid (verdict : non couvert) | [ecwid](ecwid.md) | " | |
| 428 | + f"{sum(1 for s in registry['stores'] if s.get('platform') == 'ecwid')} " | |
| 429 | + "| 0 | 0 | — | — | — |", "", | |
| 430 | + "Pipelines transverses : [découverte & registre]" | |
| 431 | + "(decouverte-registre.md) · [enrichissement boutiques]" | |
| 432 | + "(enrichissement-boutiques.md).", "", | |
| 433 | + "## Top 15 boutiques par volume", "", | |
| 434 | + "| Boutique | Domaine | Plateforme | Produits | Dernier sync |", | |
| 435 | + "|---|---|---|---|---|"] | |
| 436 | + for r in con.execute("SELECT id, name, platform, product_count, last_sync " | |
| 437 | + "FROM stores ORDER BY product_count DESC LIMIT 15"): | |
| 438 | + md.append(f"| {r['name'] or r['id']} | `{r['id']}` | {r['platform']} | " | |
| 439 | + f"{nfr(r['product_count'])} | {fmt_ts(r['last_sync'])} |") | |
| 440 | + md += ["", "## Gotchas transverses", "", | |
| 441 | + "- **Shopify curl anti-TLS + verrou 0,7 s** : voir " | |
| 442 | + "[shopify](shopify.md).", | |
| 443 | + "- **Scrapfly `large_object`** : les grosses réponses arrivent en " | |
| 444 | + "deux temps — voir [scrapfly-transport](scrapfly-transport.md).", | |
| 445 | + "- **FTS par lots** (`fabrika/db.py`) : la purge/réinsertion de " | |
| 446 | + "l'index `products_fts` se fait par lots de 500 uid (1 balayage " | |
| 447 | + "par lot au lieu de N deletes unitaires).", | |
| 448 | + "- **Verrou BD** (`fabrika/ingest.py`) : écritures sérialisées via " | |
| 449 | + "`threading.Lock` — les syncs multi-boutiques sont parallèles côté " | |
| 450 | + "réseau, séquentiels côté SQLite.", ""] | |
| 451 | + (OUT / "INDEX.md").write_text("\n".join(md) + "\n", encoding="utf-8") | |
| 452 | + | |
| 453 | + | |
| 454 | +def main() -> None: | |
| 455 | + OUT.mkdir(parents=True, exist_ok=True) | |
| 456 | + for old in OUT.glob("*.md"): | |
| 457 | + old.unlink() | |
| 458 | + registry = load_registry() | |
| 459 | + con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True) | |
| 460 | + con.row_factory = sqlite3.Row | |
| 461 | + pstats = db_platform_stats(con) | |
| 462 | + for key in GROUPS: | |
| 463 | + write_platform(key, con, registry, pstats) | |
| 464 | + write_scrapfly(con) | |
| 465 | + write_ecwid(con, registry) | |
| 466 | + write_discovery(registry) | |
| 467 | + write_enrichment(con) | |
| 468 | + write_index(con, registry, pstats) | |
| 469 | + con.close() | |
| 470 | + print(f"[gen_connector_docs] {len(GROUPS)} fiches plateformes + scrapfly + " | |
| 471 | + f"ecwid + découverte + enrichissement + INDEX → {OUT}") | |
| 472 | + | |
| 473 | + | |
| 474 | +if __name__ == "__main__": | |
| 475 | + main() | |
| 476 | ||