SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
4 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%

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>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent 778d2b6

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