# ----------------------------------------------------------------------------- # Fabri-Ka — Agrégateur de produits québécois # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/square.py : connecteur générique Square Online (Weebly). # Les sites Square Online exposent une API JSON publique côté storefront : # /app/store/api/v13/editor/users//sites//products # Les identifiants user_id / site_id sont imprimés dans le HTML de chaque # page (config JS `user_id: '…'` / `site_id: '…'`). Pagination via # meta.pagination.total_pages, 100 produits/page. # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Product, parse_price from .base import BaseConnector # le HTML expose les IDs sous deux formes (config JS et JSON embarqué) _USER_RES = [re.compile(r"user_id: '(\d+)'"), re.compile(r'"user_id":"?(\d+)')] _SITE_RES = [re.compile(r"site_id: '(\d+)'"), re.compile(r'"site_id":"?(\d+)')] API_PATH = "/app/store/api/v13" def extract_ids(html: str) -> tuple[str | None, str | None]: """(user_id, site_id) depuis le HTML d'une page Square Online.""" user = site = None for rx in _USER_RES: m = rx.search(html) if m: user = m.group(1) break for rx in _SITE_RES: m = rx.search(html) if m: site = m.group(1) break return user, site class SquareConnector(BaseConnector): platform = "square" request_delay = 0.5 def _ids(self) -> tuple[str, str]: r = self.get(self.base + "/") user, site = extract_ids(r.text) if not (user and site): raise RuntimeError("IDs Square Online (user_id/site_id) introuvables dans le HTML") return user, site def fetch(self) -> list[Product]: user, site = self._ids() out: list[Product] = [] page = 1 while page <= self.max_pages: # include=images,options : galerie complète + variantes dans la même # requête paginée (levée du « mono-image » sans requête supplémentaire) url = (f"{self.base}{API_PATH}/editor/users/{user}/sites/{site}" f"/products?page={page}&per_page=100&include=images,options") data = self.get(url).json() items = data.get("data") or [] for it in items: if (it.get("visibility") or "visible") != "visible": continue price = it.get("price") or {} low = parse_price(price.get("low")) high = parse_price(price.get("high")) reg_high = parse_price(price.get("regular_high")) on_sale = bool(it.get("on_sale")) badges = it.get("badges") or {} inv = it.get("inventory") or {} available = not (badges.get("out_of_stock") or inv.get("all_variations_sold_out")) thumb = ((it.get("thumbnail") or {}).get("data") or {}) img = thumb.get("absolute_url") images: list[str] = [] for rec in ((it.get("images") or {}).get("data") or []): u = rec.get("absolute_url") or "" if u and not u.startswith("http"): u = "https://" + u.lstrip("/") if u and u not in images: images.append(u) if not images and img: images = [img if img.startswith("http") else "https://" + img.lstrip("/")] det: dict = {} opts = [] for o in ((it.get("options") or {}).get("data") or []): name = o.get("name") or "" if name: opts.append({"name": name, "values": [str(c) for c in (o.get("choice_order") or [])]}) if opts: det["options"] = opts try: if float(it.get("avg_rating") or 0) > 0: det["average_rating"] = float(it["avg_rating"]) except (TypeError, ValueError): pass if on_sale: det["on_sale"] = True if it.get("is_alcoholic"): det["is_alcoholic"] = True if it.get("sku"): det["sku"] = str(it["sku"]) if it.get("rating_count"): try: det["review_count"] = int(it["rating_count"]) except (TypeError, ValueError): pass for k in ("created_date", "updated_date"): if it.get(k): det[k] = it[k] site_link = (it.get("site_link") or "").lstrip("/") out.append(Product( store_id=self.store_id, external_id=str(it.get("id") or it.get("square_id") or ""), url=f"{self.base}/{site_link}" if site_link else self.base, title=it.get("name", "") or "", description=(it.get("short_description") or it.get("seo_page_description") or ""), price=low, price_max=high if high and low and high > low else low, compare_at_price=(reg_high if on_sale and reg_high and reg_high > (high or 0) else None), images=images, product_type=it.get("product_type", "") or "", available=available, details=det, )) pag = (data.get("meta") or {}).get("pagination") or {} if page >= int(pag.get("total_pages") or page): break page += 1 return out