SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
6.7 KB · 154 lines python
Raw Blame History
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