Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
1# -----------------------------------------------------------------------------2# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/abitibi.py : Tourisme Abitibi-Témiscamingue (ATR, vague ATR 2026-08)5# Source : abitibi-temiscamingue.org/festivals-et-evenements/ (domaine6# tourisme-abitibi-temiscamingue.org → redirection) — répertoire7# officiel des festivals et événements de l'ATR (WordPress,8# gisement Tourinsoft : images mto.media.tourinsoft.eu).9# Extraction: pages liste RENDUES SERVEUR /festivals-et-evenements/[page/N/]10# (~3 pages × 12 cartes vérifié 2026-08-25) : lien11# /evenements/<slug>/, image, ville, titre, plage de dates FR12# (« Du 26 au 30 août 2026 »). Le site est derrière un challenge13# Cloudflare (403 + « Just a moment ») → chaque GET passe par14# l'AUTO-ESCALADE anti-bot de BaseConnector.get() (Oxylabs →15# Scrapfly ASP → Bright Data) ; budget borné : ABT_PAGE_MAX16# pages/sync, AUCUNE fiche détail (la carte porte tout ce que la17# source publie de structuré ; wp-json est aussi derrière le18# challenge et n'expose pas de CPT événement).19# Accès : pages publiques de l'ATR — requêtes throttlées et plafonnées20# (coût par requête), identification honnête, lien vers la fiche21# originale. Ni prix, ni heure, ni description structurés sur la22# carte → jamais inventés.23# -----------------------------------------------------------------------------24from __future__ import annotations2526import os27import re2829from ..normalize import clean_text, parse_date_range_fr30from ..schema import Event31from .base import BaseConnector3233BASE = "https://abitibi-temiscamingue.org"34LIST_URL = BASE + "/festivals-et-evenements/"35ABT_PAGE_MAX = int(os.environ.get("ABT_PAGE_MAX", "4"))3637# une carte du répertoire : post-id, image de fond, lien, ville, titre, date38_CARD_RE = re.compile(39 r'data-post-id="(\d+)">\s*'40 r'<div class="quanto-blog-thumb">\s*'41 r'<a class="aspect-ratio" href="(https://abitibi-temiscamingue\.org/'42 r'evenements/[^"]+)"\s*style="background-image: url\(\'([^\']*)\'\);">'43 r'</a>.*?<li><span>([^<]*?)\s*<svg.*?'44 r'<p class="h6"><a href="\2">(.*?)</a></p>\s*'45 r'(?:<p class="h6 repertoire-preview-date"[^>]*>([^<]*)</p>)?', re.S)464748class AbitibiConnector(BaseConnector):49 source_id = "abitibi"50 request_delay = 1.0 # chaque requête peut coûter un crédit Scrapfly51 timeout = 1205253 def _cards(self, html: str) -> list[dict]:54 out = []55 for m in _CARD_RE.finditer(html):56 pid, url, img, city, title, date_raw = m.groups()57 start, end = parse_date_range_fr(date_raw or "")58 out.append({59 "external_id": pid,60 "url": url,61 "title": clean_text(title),62 "city": clean_text(city),63 "start_date": start, "end_date": end,64 "image": (img or "").strip(),65 })66 return out6768 def fetch(self) -> list[Event]:69 events: list[Event] = []70 seen: set[str] = set()71 for page in range(1, ABT_PAGE_MAX + 1):72 url = LIST_URL if page == 1 else f"{LIST_URL}page/{page}/"73 try:74 cards = self._cards(self.get(url).text)75 except Exception:76 break77 new = [c for c in cards if c["external_id"] not in seen]78 if not new:79 break80 for c in new:81 seen.add(c["external_id"])82 if not c["title"]:83 continue84 events.append(Event(85 source=self.source_id,86 external_id=c["external_id"],87 url=c["url"],88 title=c["title"],89 raw_categories=["Festivals et événements", c["title"]],90 city=c["city"],91 tourist_region="Abitibi-Témiscamingue",92 start_date=c["start_date"],93 end_date=c["end_date"],94 image=c["image"],95 ))96 if len(cards) < 12: # page incomplète (12/page) = dernière97 break98 return events99