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
5 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%

[ka6] fix connecteur Wix catalogue V3 (3petitscochonsverts.com, barlaitierchouinard.com): sites migrés au CATALOG_V3 — l'ancien GraphQL storefront répond numOfProducts=0 ou « Internal server error » alors que la boutique a des produits; fallback _fetch_v3 dans WixConnector: POST wixapis.com/stores/v3/products/query avec le jeton d'instance public du site (pagination par curseur, champs PLAIN_DESCRIPTION/MEDIA_ITEMS_INFO/URL/CURRENCY) — 0→9 et 0→11 produits, V1 sans régression (bijouteriealarie.com 907/907)

Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 24a2784

1 changed file +94 −0

modified fabrika/connectors/wix.py +94 −0
@@ -39,6 +39,100 @@ class WixConnector(BaseConnector):
39 39 inst = (apps.get(WIX_STORES_APP) or {}).get("instance")
40 40 if not inst:
41 41 return [] # site Wix sans app Boutique — rien à agréger
42 + out = self._fetch_v1(inst)
43 + if not out:
44 + # Sites migrés au catalogue V3 : l'ancien GraphQL storefront répond
45 + # numOfProducts=0 ou « Internal server error » alors que la boutique
46 + # a des produits (ex. barlaitierchouinard.com, 3petitscochonsverts.com)
47 + out = self._fetch_v3(inst)
48 + return out
49 +
50 + # ------------------------------------------------------------------ V3 --
51 + def _fetch_v3(self, inst: str) -> list[Product]:
52 + """Catalogue V3 : POST wixapis.com/stores/v3/products/query, le jeton
53 + d'instance public du site suffit. Pagination par curseur."""
54 + fields = ["PLAIN_DESCRIPTION", "MEDIA_ITEMS_INFO", "URL", "CURRENCY"]
55 + out: list[Product] = []
56 + cursor: str | None = None
57 + for _ in range(200): # garde-fou 20 000 produits
58 + paging: dict = {"limit": 100}
59 + if cursor:
60 + paging["cursor"] = cursor
61 + query: dict = {"cursorPaging": paging}
62 + else:
63 + query = {"filter": {"visible": True}, "cursorPaging": paging}
64 + resp = self.session.post(
65 + "https://www.wixapis.com/stores/v3/products/query",
66 + headers={"Authorization": inst, "Content-Type": "application/json"},
67 + json={"query": query, "fields": fields},
68 + timeout=self.timeout)
69 + resp.raise_for_status()
70 + data = resp.json()
71 + for it in (data.get("products") or []):
72 + out.append(self._parse_v3(it))
73 + meta = data.get("pagingMetadata") or {}
74 + cursor = (meta.get("cursors") or {}).get("next")
75 + if not meta.get("hasNext") or not cursor:
76 + break
77 + return out
78 +
79 + def _parse_v3(self, it: dict) -> Product:
80 + def amount(rng, key):
81 + try:
82 + v = ((rng or {}).get(key) or {}).get("amount")
83 + return float(v) if v not in (None, "") else None
84 + except (TypeError, ValueError):
85 + return None
86 + pmin = amount(it.get("actualPriceRange"), "minValue")
87 + pmax = amount(it.get("actualPriceRange"), "maxValue")
88 + cmin = amount(it.get("compareAtPriceRange"), "minValue")
89 + media = it.get("media") or {}
90 + imgs: list[str] = []
91 + for m in [media.get("main")] + ((media.get("itemsInfo") or {}).get("items") or []):
92 + u = ((m or {}).get("image") or {}).get("url") or ""
93 + if u and u not in imgs:
94 + imgs.append(u)
95 + url = ((it.get("url") or {}).get("url")
96 + or f"{self.base}/product-page/{it.get('slug', '')}")
97 + det: dict = {}
98 + brand = ((it.get("brand") or {}).get("name")) or ""
99 + if brand:
100 + det["brand"] = brand
101 + options = [{"title": o.get("name", ""),
102 + "selections": [c.get("name", "") for c in
103 + ((o.get("choicesSettings") or {}).get("choices") or [])]}
104 + for o in (it.get("options") or []) if o]
105 + if options:
106 + det["options"] = options
107 + nvar = (it.get("variantSummary") or {}).get("variantCount")
108 + if nvar and int(nvar) > 1:
109 + det["variations"] = int(nvar)
110 + tags = []
111 + for rb in [it.get("ribbon")] + (it.get("additionalRibbons") or []):
112 + name = (rb or {}).get("name")
113 + if name:
114 + tags.append(name)
115 + status = ((it.get("inventory") or {}).get("availabilityStatus")) or "IN_STOCK"
116 + return Product(
117 + store_id=self.store_id,
118 + external_id=str(it.get("id", "")),
119 + url=url,
120 + title=it.get("name", ""),
121 + description=_strip_html(it.get("plainDescription", "") or ""),
122 + price=pmin,
123 + price_max=pmax or pmin,
124 + compare_at_price=cmin if cmin and pmin and cmin > pmin else None,
125 + currency=it.get("currency") or "CAD",
126 + images=imgs,
127 + product_type=(it.get("productType") or "").lower(),
128 + tags=tags,
129 + vendor=brand,
130 + available=status != "OUT_OF_STOCK",
131 + details=det,
132 + )
133 +
134 + # ------------------------------------------------------------------ V1 --
135 + def _fetch_v1(self, inst: str) -> list[Product]:
42 136 out: list[Product] = []
43 137 offset, total = 0, None
44 138 while offset < (total if total is not None else 1) and offset < 20000:
45 139