# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/abitibi.py : Tourisme Abitibi-Témiscamingue (ATR, vague ATR 2026-08) # Source : abitibi-temiscamingue.org/festivals-et-evenements/ (domaine # tourisme-abitibi-temiscamingue.org → redirection) — répertoire # officiel des festivals et événements de l'ATR (WordPress, # gisement Tourinsoft : images mto.media.tourinsoft.eu). # Extraction: pages liste RENDUES SERVEUR /festivals-et-evenements/[page/N/] # (~3 pages × 12 cartes vérifié 2026-08-25) : lien # /evenements//, image, ville, titre, plage de dates FR # (« Du 26 au 30 août 2026 »). Le site est derrière un challenge # Cloudflare (403 + « Just a moment ») → chaque GET passe par # l'AUTO-ESCALADE anti-bot de BaseConnector.get() (Oxylabs → # Scrapfly ASP → Bright Data) ; budget borné : ABT_PAGE_MAX # pages/sync, AUCUNE fiche détail (la carte porte tout ce que la # source publie de structuré ; wp-json est aussi derrière le # challenge et n'expose pas de CPT événement). # Accès : pages publiques de l'ATR — requêtes throttlées et plafonnées # (coût par requête), identification honnête, lien vers la fiche # originale. Ni prix, ni heure, ni description structurés sur la # carte → jamais inventés. # ----------------------------------------------------------------------------- from __future__ import annotations import os import re from ..normalize import clean_text, parse_date_range_fr from ..schema import Event from .base import BaseConnector BASE = "https://abitibi-temiscamingue.org" LIST_URL = BASE + "/festivals-et-evenements/" ABT_PAGE_MAX = int(os.environ.get("ABT_PAGE_MAX", "4")) # une carte du répertoire : post-id, image de fond, lien, ville, titre, date _CARD_RE = re.compile( r'data-post-id="(\d+)">\s*' r'
\s*' r'' r'.*?
  • ([^<]*?)\s*(.*?)

    \s*' r'(?:

    ]*>([^<]*)

    )?', re.S) class AbitibiConnector(BaseConnector): source_id = "abitibi" request_delay = 1.0 # chaque requête peut coûter un crédit Scrapfly timeout = 120 def _cards(self, html: str) -> list[dict]: out = [] for m in _CARD_RE.finditer(html): pid, url, img, city, title, date_raw = m.groups() start, end = parse_date_range_fr(date_raw or "") out.append({ "external_id": pid, "url": url, "title": clean_text(title), "city": clean_text(city), "start_date": start, "end_date": end, "image": (img or "").strip(), }) return out def fetch(self) -> list[Event]: events: list[Event] = [] seen: set[str] = set() for page in range(1, ABT_PAGE_MAX + 1): url = LIST_URL if page == 1 else f"{LIST_URL}page/{page}/" try: cards = self._cards(self.get(url).text) except Exception: break new = [c for c in cards if c["external_id"] not in seen] if not new: break for c in new: seen.add(c["external_id"]) if not c["title"]: continue events.append(Event( source=self.source_id, external_id=c["external_id"], url=c["url"], title=c["title"], raw_categories=["Festivals et événements", c["title"]], city=c["city"], tourist_region="Abitibi-Témiscamingue", start_date=c["start_date"], end_date=c["end_date"], image=c["image"], )) if len(cards) < 12: # page incomplète (12/page) = dernière break return events