Agrégateur de produits québécois — www.fabri-ka.com
Python 38.4%
HTML 30.3%
TypeScript 17.2%
CSS 11%
JavaScript 3.2%
1# -----------------------------------------------------------------------------2# Fabri-Ka — Agrégateur de produits québécois3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/square.py : connecteur générique Square Online (Weebly).5# Les sites Square Online exposent une API JSON publique côté storefront :6# /app/store/api/v13/editor/users/<user_id>/sites/<site_id>/products7# Les identifiants user_id / site_id sont imprimés dans le HTML de chaque8# page (config JS `user_id: '…'` / `site_id: '…'`). Pagination via9# meta.pagination.total_pages, 100 produits/page.10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from ..schema import Product, parse_price16from .base import BaseConnector1718# le HTML expose les IDs sous deux formes (config JS et JSON embarqué)19_USER_RES = [re.compile(r"user_id: '(\d+)'"), re.compile(r'"user_id":"?(\d+)')]20_SITE_RES = [re.compile(r"site_id: '(\d+)'"), re.compile(r'"site_id":"?(\d+)')]2122API_PATH = "/app/store/api/v13"232425def extract_ids(html: str) -> tuple[str | None, str | None]:26 """(user_id, site_id) depuis le HTML d'une page Square Online."""27 user = site = None28 for rx in _USER_RES:29 m = rx.search(html)30 if m:31 user = m.group(1)32 break33 for rx in _SITE_RES:34 m = rx.search(html)35 if m:36 site = m.group(1)37 break38 return user, site394041class SquareConnector(BaseConnector):42 platform = "square"43 request_delay = 0.54445 def _ids(self) -> tuple[str, str]:46 r = self.get(self.base + "/")47 user, site = extract_ids(r.text)48 if not (user and site):49 raise RuntimeError("IDs Square Online (user_id/site_id) introuvables dans le HTML")50 return user, site5152 def fetch(self) -> list[Product]:53 user, site = self._ids()54 out: list[Product] = []55 page = 156 while page <= self.max_pages:57 # include=images,options : galerie complète + variantes dans la même58 # requête paginée (levée du « mono-image » sans requête supplémentaire)59 url = (f"{self.base}{API_PATH}/editor/users/{user}/sites/{site}"60 f"/products?page={page}&per_page=100&include=images,options")61 data = self.get(url).json()62 items = data.get("data") or []63 for it in items:64 if (it.get("visibility") or "visible") != "visible":65 continue66 price = it.get("price") or {}67 low = parse_price(price.get("low"))68 high = parse_price(price.get("high"))69 reg_high = parse_price(price.get("regular_high"))70 on_sale = bool(it.get("on_sale"))71 badges = it.get("badges") or {}72 inv = it.get("inventory") or {}73 available = not (badges.get("out_of_stock")74 or inv.get("all_variations_sold_out"))75 thumb = ((it.get("thumbnail") or {}).get("data") or {})76 img = thumb.get("absolute_url")77 images: list[str] = []78 for rec in ((it.get("images") or {}).get("data") or []):79 u = rec.get("absolute_url") or ""80 if u and not u.startswith("http"):81 u = "https://" + u.lstrip("/")82 if u and u not in images:83 images.append(u)84 if not images and img:85 images = [img if img.startswith("http") else "https://" + img.lstrip("/")]86 det: dict = {}87 opts = []88 for o in ((it.get("options") or {}).get("data") or []):89 name = o.get("name") or ""90 if name:91 opts.append({"name": name,92 "values": [str(c) for c in (o.get("choice_order") or [])]})93 if opts:94 det["options"] = opts95 try:96 if float(it.get("avg_rating") or 0) > 0:97 det["average_rating"] = float(it["avg_rating"])98 except (TypeError, ValueError):99 pass100 if on_sale:101 det["on_sale"] = True102 if it.get("is_alcoholic"):103 det["is_alcoholic"] = True104 if it.get("sku"):105 det["sku"] = str(it["sku"])106 if it.get("rating_count"):107 try:108 det["review_count"] = int(it["rating_count"])109 except (TypeError, ValueError):110 pass111 for k in ("created_date", "updated_date"):112 if it.get(k):113 det[k] = it[k]114 site_link = (it.get("site_link") or "").lstrip("/")115 out.append(Product(116 store_id=self.store_id,117 external_id=str(it.get("id") or it.get("square_id") or ""),118 url=f"{self.base}/{site_link}" if site_link else self.base,119 title=it.get("name", "") or "",120 description=(it.get("short_description")121 or it.get("seo_page_description") or ""),122 price=low,123 price_max=high if high and low and high > low else low,124 compare_at_price=(reg_high if on_sale and reg_high125 and reg_high > (high or 0) else None),126 images=images,127 product_type=it.get("product_type", "") or "",128 available=available,129 details=det,130 ))131 pag = (data.get("meta") or {}).get("pagination") or {}132 if page >= int(pag.get("total_pages") or page):133 break134 page += 1135 return out136