# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/eeyouistchee.py : Eeyou Istchee Baie-James (ATR, vague ATR 2026-08) # Source : eeyouistcheebaiejames.com/fr/quoi-faire/evenements/ (domaine # decrochezcommejamais.com → redirection) — calendrier officiel # de l'ATR (plateforme membres rendue serveur, vérifié # 2026-08-25 : ~28 cartes événements avec titre, ville, date FR, # catégorie, image). # Extraction: 1 GET liste → cartes « update-secondary » (lien fiche membre # /fr/membres///evenements-et-festivals/, image # fichiersUploadOpt, titre, « Région : », date # « update-meta » : « 30 juillet au 2 août 2026 » ou « Date à # venir »). Fiches détail incrémentales (cache disque # data/eeyouistchee_cache.json, plafond EIB_FICHE_MAX/sync, # re-visite 7 j) : description (paragraphe principal), code # postal et site web de l'organisme. # Accès : pages publiques de l'ATR — GET throttlés, identification # honnête, lien vers la fiche originale. Ni prix ni heure # structurés publiés → jamais inventés. # ----------------------------------------------------------------------------- from __future__ import annotations import os import re import time from ..normalize import clean_text, parse_date_range_fr from ..schema import Event from .base import BaseConnector BASE = "https://www.eeyouistcheebaiejames.com" LIST_URL = BASE + "/fr/quoi-faire/evenements/" EIB_FICHE_MAX = int(os.environ.get("EIB_FICHE_MAX", "10")) REFRESH_AFTER = 7 * 86400 # une carte événement : image de fond, lien fiche, titre, ville, date _CARD_RE = re.compile( r'update-image" style="background-image: url\(([^)]*)\)[^>]*>\s*' r'.*?' r'([^<]*).*?' r'

R[ée]gion\s*:\s*([^<]*)

.*?' r'(?:([^<]*).*?)?\s*\n?\s*' r'
', re.S) _POSTAL_RE = re.compile(r'\b([A-Z]\d[A-Z]\s?\d[A-Z]\d)\b') _P_RE = re.compile(r"]*>(.*?)

", re.S) _WEBSITE_RE = re.compile( r'href="(https?://(?!www\.eeyouistcheebaiejames)[^"]+)"[^>]*>\s*' r'(?:Site\s|www\.|[a-z0-9.-]+\.(?:com|ca|qc\.ca|org|net))', re.I) def _parse_range(raw: str) -> tuple[str | None, str | None]: """« 8 au 9 août 2026 » / « 30 juillet au 2 août 2026 » : la source omet le « du » initial → on le préfixe pour la plage partielle.""" raw = clean_text(raw or "") if re.match(r"^\d", raw) and " au " in raw: raw = "du " + raw return parse_date_range_fr(raw) class EeyouIstcheeConnector(BaseConnector): source_id = "eeyouistchee" request_delay = 0.8 def _cards(self, html: str) -> list[dict]: out = [] for m in _CARD_RE.finditer(html): img, path, title, city, date_raw = m.groups() start, end = _parse_range(date_raw or "") # fr/membres///evenements-et-festivals parts = path.strip("/").split("/") out.append({ "path": path, "external_id": (parts[3] if len(parts) > 4 and parts[3].isdigit() else parts[2]), "title": clean_text(title), "city": clean_text(city), "start_date": start, "end_date": end, "image": BASE + img.strip("'\" ") if img else "", }) return out def _parse_fiche(self, html: str) -> dict: """Description (paragraphe principal), code postal, site web.""" i = html.find("= 0 else html[:15000] out: dict = {} phone_re = re.compile(r"\d{3}[ .-]\d{3}[ .-]\d{4}") paras = [clean_text(p) for p in _P_RE.findall(seg)] # écarte les blocs coordonnées (adresse/téléphone/code postal) paras = [p for p in paras if len(p) > 80 and not phone_re.search(p) and not _POSTAL_RE.search(p) # scripts anti-pourriel (codes décimaux) recrachés en texte and not re.search(r"(?:\d{2,3},){5,}", p)] if paras: out["description"] = max(paras, key=len) m = _POSTAL_RE.search(re.sub(r"<[^>]+>", " ", seg)) if m: out["postal_code"] = m.group(1) w = _WEBSITE_RE.search(seg) if w: out["website"] = w.group(1) return out def fetch(self) -> list[Event]: cards = self._cards(self.get(LIST_URL).text) cache = self.load_cache() now = time.time() current = {c["path"] for c in cards} cache = {k: v for k, v in cache.items() if k in current} to_fetch = sorted( (c["path"] for c in cards if c["path"] not in cache or now - cache[c["path"]].get("ts", 0) > REFRESH_AFTER), key=lambda p: cache.get(p, {}).get("ts", 0))[:EIB_FICHE_MAX] for path in to_fetch: detail: dict = {} try: detail = self._parse_fiche(self.get(BASE + path).text) except Exception: detail = {} cache[path] = {"ts": now, "detail": detail} self.save_cache(cache) events: list[Event] = [] seen: set[str] = set() for c in cards: key = f'{c["external_id"]}:{c["title"].lower()}' if not c["title"] or key in seen: continue seen.add(key) detail = cache.get(c["path"], {}).get("detail") or {} events.append(Event( source=self.source_id, # une même fiche membre peut porter plusieurs événements : # l'identifiant inclut le titre pour rester stable et unique external_id=f'{c["external_id"]}-' + re.sub(r"[^a-z0-9]+", "-", c["title"].lower())[:60], url=BASE + c["path"], title=c["title"], description=detail.get("description", ""), raw_categories=["Événements et festivals", c["title"]], city=c["city"], postal_code=detail.get("postal_code", ""), tourist_region="Eeyou Istchee Baie-James", start_date=c["start_date"], end_date=c["end_date"], website=detail.get("website", ""), image=c["image"], )) return events