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%
4.6 KB · 107 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/granby.py : connecteur Ville de Granby — calendrier des événements5#   Source    : https://www.granby.ca/fr/calendrier-evenements (Drupal).6#   Extraction: la Ville ne publie PAS de calendrier structuré (jsonapi7#               désactivé, page « actualités/événements » vide côté serveur,8#               vérifié 2026-08-25) — la page calendrier est une liste9#               ÉDITORIALE rendue serveur : blocs riches <h5><strong>Titre10#               </strong></h5> suivi d'un <p> de dates FR (« Les 22 et 23 mai11#               2026 », « Du 28 au 30 mai 2026 ») et d'un lien « Découvrez12#               l'événement » (souvent externe, vers l'organisateur).13#               Parsing tolérant : un bloc sans date parseable est ignoré.14#               Volume faible (~15-25 événements saisonniers), 1 requête.15#   Accès     : site municipal public, GET direct throttlé, identification16#               honnête ; robots.txt permissif.17#   Prix      : non publié par la source → jamais inventé. Lieu/heure non18#               publiés dans la liste → laissés vides.19# -----------------------------------------------------------------------------20from __future__ import annotations2122import html as _html23import re2425from ..normalize import _FR_MONTHS, clean_text, strip_accents  # type: ignore26from ..schema import Event27from .base import BaseConnector2829LIST_URL = "https://www.granby.ca/fr/calendrier-evenements"3031_BLOCK_RE = re.compile(32    r"<h5><strong>(.*?)</strong></h5>(.*?)(?=<h5><strong>|</section>|$)", re.S)33_LINK_RE = re.compile(r'<a href="([^"]+)"[^>]*>\s*D&eacute;couvrez|'34                      r'<a href="([^"]+)"[^>]*>\s*Découvrez')35# « les 22 et 23 mai 2026 », « du 28 au 30 mai 2026 », « le 24 juin 2026 »,36# « les 4, 5 et 6 juin 2026 » → jours multiples, mois+année en fin37_DAYS_RE = re.compile(38    r"((?:\d{1,2}(?:er)?[,\s]*(?:et\s+|au\s+)?)+)\s*([a-z]+)\s+(\d{4})")394041def _range(txt: str) -> tuple[str | None, str | None]:42    """Dates éditoriales FR (jours multiples, un mois+année) → (début, fin)."""43    k = strip_accents(clean_text(txt)).lower()44    dates: list[str] = []45    for m in _DAYS_RE.finditer(k):46        mo, y = _FR_MONTHS.get(m.group(2)), int(m.group(3))47        if not mo or not (1990 <= y <= 2100):48            continue49        for dm in re.finditer(r"\d{1,2}", m.group(1)):50            d = int(dm.group(0))51            if 1 <= d <= 31:52                dates.append(f"{y:04d}-{mo:02d}-{d:02d}")53    if not dates:54        return None, None55    return min(dates), max(dates)565758class GranbyConnector(BaseConnector):59    source_id = "granby"6061    def _parse_page(self, html: str) -> list[dict]:62        rows: list[dict] = []63        for title_html, body in _BLOCK_RE.findall(html):64            title = clean_text(_html.unescape(65                re.sub(r"<[^>]+>", " ", title_html)))66            if not title:67                continue68            # 1er <p> avec une date parseable = la plage de l'événement69            start = end = None70            for p in re.findall(r"<p[^>]*>(.*?)</p>", body, re.S):71                start, end = _range(_html.unescape(re.sub(r"<[^>]+>", " ", p)))72                if start:73                    break74            if not start:75                continue76            link = _LINK_RE.search(body)77            website = (link.group(1) or link.group(2)).strip() if link else ""78            rows.append({"title": title, "start": start, "end": end,79                         "website": website})80        return rows8182    def fetch(self) -> list[Event]:83        rows = self._parse_page(self.get(LIST_URL).text)84        events: list[Event] = []85        seen: set[str] = set()86        for r in rows:87            slug = re.sub(r"[^a-z0-9]+", "-",88                          strip_accents(r["title"]).lower()).strip("-")89            ext = f"{slug}-{r['start']}"90            if ext in seen:91                continue92            seen.add(ext)93            events.append(Event(94                source=self.source_id,95                external_id=ext,96                url=r["website"] or LIST_URL,97                title=r["title"],98                raw_categories=[r["title"]],99                city="Granby",100                region="Estrie",101                start_date=r["start"],102                end_date=r["end"] or r["start"],103                organizer="Ville de Granby",104                website=r["website"],105            ))106        return events107