[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)
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 | |