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/eeyouistchee.py : Eeyou Istchee Baie-James (ATR, vague ATR 2026-08)5# Source : eeyouistcheebaiejames.com/fr/quoi-faire/evenements/ (domaine6# decrochezcommejamais.com → redirection) — calendrier officiel7# de l'ATR (plateforme membres rendue serveur, vérifié8# 2026-08-25 : ~28 cartes événements avec titre, ville, date FR,9# catégorie, image).10# Extraction: 1 GET liste → cartes « update-secondary » (lien fiche membre11# /fr/membres/<ville>/<noFiche>/evenements-et-festivals/, image12# fichiersUploadOpt, titre, « Région : <ville> », date13# « update-meta » : « 30 juillet au 2 août 2026 » ou « Date à14# venir »). Fiches détail incrémentales (cache disque15# data/eeyouistchee_cache.json, plafond EIB_FICHE_MAX/sync,16# re-visite 7 j) : description (paragraphe principal), code17# postal et site web de l'organisme.18# Accès : pages publiques de l'ATR — GET throttlés, identification19# honnête, lien vers la fiche originale. Ni prix ni heure20# structurés publiés → jamais inventés.21# -----------------------------------------------------------------------------22from __future__ import annotations2324import os25import re26import time2728from ..normalize import clean_text, parse_date_range_fr29from ..schema import Event30from .base import BaseConnector3132BASE = "https://www.eeyouistcheebaiejames.com"33LIST_URL = BASE + "/fr/quoi-faire/evenements/"34EIB_FICHE_MAX = int(os.environ.get("EIB_FICHE_MAX", "10"))35REFRESH_AFTER = 7 * 864003637# une carte événement : image de fond, lien fiche, titre, ville, date38_CARD_RE = re.compile(39 r'update-image" style="background-image: url\(([^)]*)\)[^>]*>\s*'40 r'<a href="(/fr/membres/[^"]+/evenements-et-festivals/)"></a>.*?'41 r'<a href="\2">([^<]*)</a>.*?'42 r'<p>R[ée]gion\s*:\s*([^<]*)</p>.*?'43 r'(?:<span class="update-meta">([^<]*)</span>.*?)?</div>\s*\n?\s*'44 r'<div class="update-actions">', re.S)4546_POSTAL_RE = re.compile(r'\b([A-Z]\d[A-Z]\s?\d[A-Z]\d)\b')47_P_RE = re.compile(r"<p[^>]*>(.*?)</p>", re.S)48_WEBSITE_RE = re.compile(49 r'href="(https?://(?!www\.eeyouistcheebaiejames)[^"]+)"[^>]*>\s*'50 r'(?:Site\s|www\.|[a-z0-9.-]+\.(?:com|ca|qc\.ca|org|net))', re.I)515253def _parse_range(raw: str) -> tuple[str | None, str | None]:54 """« 8 au 9 août 2026 » / « 30 juillet au 2 août 2026 » : la source omet55 le « du » initial → on le préfixe pour la plage partielle."""56 raw = clean_text(raw or "")57 if re.match(r"^\d", raw) and " au " in raw:58 raw = "du " + raw59 return parse_date_range_fr(raw)606162class EeyouIstcheeConnector(BaseConnector):63 source_id = "eeyouistchee"64 request_delay = 0.86566 def _cards(self, html: str) -> list[dict]:67 out = []68 for m in _CARD_RE.finditer(html):69 img, path, title, city, date_raw = m.groups()70 start, end = _parse_range(date_raw or "")71 # fr/membres/<slug-membre>/<noFiche>/evenements-et-festivals72 parts = path.strip("/").split("/")73 out.append({74 "path": path,75 "external_id": (parts[3] if len(parts) > 4 and76 parts[3].isdigit() else parts[2]),77 "title": clean_text(title),78 "city": clean_text(city),79 "start_date": start, "end_date": end,80 "image": BASE + img.strip("'\" ") if img else "",81 })82 return out8384 def _parse_fiche(self, html: str) -> dict:85 """Description (paragraphe principal), code postal, site web."""86 i = html.find("<h1")87 seg = html[i:i + 15000] if i >= 0 else html[:15000]88 out: dict = {}89 phone_re = re.compile(r"\d{3}[ .-]\d{3}[ .-]\d{4}")90 paras = [clean_text(p) for p in _P_RE.findall(seg)]91 # écarte les blocs coordonnées (adresse/téléphone/code postal)92 paras = [p for p in paras if len(p) > 8093 and not phone_re.search(p) and not _POSTAL_RE.search(p)94 # scripts anti-pourriel (codes décimaux) recrachés en texte95 and not re.search(r"(?:\d{2,3},){5,}", p)]96 if paras:97 out["description"] = max(paras, key=len)98 m = _POSTAL_RE.search(re.sub(r"<[^>]+>", " ", seg))99 if m:100 out["postal_code"] = m.group(1)101 w = _WEBSITE_RE.search(seg)102 if w:103 out["website"] = w.group(1)104 return out105106 def fetch(self) -> list[Event]:107 cards = self._cards(self.get(LIST_URL).text)108 cache = self.load_cache()109 now = time.time()110 current = {c["path"] for c in cards}111 cache = {k: v for k, v in cache.items() if k in current}112113 to_fetch = sorted(114 (c["path"] for c in cards115 if c["path"] not in cache116 or now - cache[c["path"]].get("ts", 0) > REFRESH_AFTER),117 key=lambda p: cache.get(p, {}).get("ts", 0))[:EIB_FICHE_MAX]118 for path in to_fetch:119 detail: dict = {}120 try:121 detail = self._parse_fiche(self.get(BASE + path).text)122 except Exception:123 detail = {}124 cache[path] = {"ts": now, "detail": detail}125 self.save_cache(cache)126127 events: list[Event] = []128 seen: set[str] = set()129 for c in cards:130 key = f'{c["external_id"]}:{c["title"].lower()}'131 if not c["title"] or key in seen:132 continue133 seen.add(key)134 detail = cache.get(c["path"], {}).get("detail") or {}135 events.append(Event(136 source=self.source_id,137 # une même fiche membre peut porter plusieurs événements :138 # l'identifiant inclut le titre pour rester stable et unique139 external_id=f'{c["external_id"]}-'140 + re.sub(r"[^a-z0-9]+", "-", c["title"].lower())[:60],141 url=BASE + c["path"],142 title=c["title"],143 description=detail.get("description", ""),144 raw_categories=["Événements et festivals", c["title"]],145 city=c["city"],146 postal_code=detail.get("postal_code", ""),147 tourist_region="Eeyou Istchee Baie-James",148 start_date=c["start_date"],149 end_date=c["end_date"],150 website=detail.get("website", ""),151 image=c["image"],152 ))153 return events154