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 · 109 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/sherbrooke.py : connecteur Ville de Sherbrooke — calendrier officiel5#   Source    : sherbrooke.ca, « Calendrier des événements en temps réel »6#               (référencé sur Données Québec : calendrier-des-evenements-en-temps-reel)7#   Extraction: la page rend TOUT le calendrier dans window['TRANSFER_CACHE']8#               (JSON embarqué, CMS Maruche) → 1 seule requête, ~270 événements9#               (nom, périodes, gratuité `payant`, catégories, clientèles,10#               images /Fichiers/<module>/Entities/<id>/<mainPicture>).11#   Accès     : page municipale publique, GET direct, identification honnête.12#   Prix      : booléen `payant` de la source → is_free (montants non exposés).13# -----------------------------------------------------------------------------14from __future__ import annotations1516import json1718from ..normalize import utc_to_local19from ..schema import Event20from .base import BaseConnector2122PAGE = ("https://www.sherbrooke.ca/fr/culture-sports-et-loisirs/"23        "calendrier-des-activites")24SITE = "https://www.sherbrooke.ca"25# GUID du module « fichiers d'entités » du calendrier (constaté via og:image)26FILES_MODULE = "3337a882-4a53-e611-80ea-00155d09650f"272829def _find_events(o):30    """Localise la liste d'événements (dicts avec visibleId) dans le cache JSON."""31    if isinstance(o, list) and o and isinstance(o[0], dict) and "visibleId" in o[0]:32        return o33    if isinstance(o, dict):34        for v in o.values():35            r = _find_events(v)36            if r:37                return r38    if isinstance(o, list):39        for v in o:40            r = _find_events(v)41            if r:42                return r43    return None444546class SherbrookeConnector(BaseConnector):47    source_id = "sherbrooke"4849    def _extract_cache(self, html: str) -> dict:50        i = html.find("window['TRANSFER_CACHE'] =")51        if i < 0:52            raise ValueError("TRANSFER_CACHE introuvable (page modifiée ?)")53        start = html.find("{", i)54        end = html.find("</script>", start)55        return json.loads(html[start:end].strip().rstrip(";"))5657    def fetch(self) -> list[Event]:58        html = self.get(PAGE).text59        records = _find_events(self._extract_cache(html)) or []60        events: list[Event] = []61        for rec in records:62            title = (rec.get("name") or {}).get("fr", "")63            full_url = (rec.get("fullUrl") or {}).get("fr", "")64            periods = rec.get("periods") or []65            if not title or not full_url or not periods:66                continue67            # les périodes sont publiées en UTC (« …T04:00:00Z » = minuit68            # local, placeholder « toute la journée ») → date + heure locales69            start, start_time = utc_to_local(periods[0].get("dateDebut"))70            end, end_time = utc_to_local(periods[-1].get("dateFin"))7172            cats, audience = [], []73            c = rec.get("categories") or {}74            cats += [(b.get("name") or {}).get("fr", "") for b in c.get("base") or []]75            for block in c.get("block") or []:76                label = (block.get("name") or {}).get("fr", "")77                children = [(ch.get("name") or {}).get("fr", "")78                            for ch in block.get("children") or []]79                if label == "Clientèle":80                    audience += children81                elif label != "Secteur":82                    cats += children8384            image = ""85            if rec.get("mainPicture") and rec.get("id"):86                image = (f"{SITE}/Fichiers/{FILES_MODULE}/Entities/"87                         f"{rec['id']}/{rec['mainPicture']}")8889            payant = rec.get("payant")90            events.append(Event(91                source=self.source_id,92                external_id=str(rec.get("visibleId") or rec.get("id")),93                url=SITE + full_url,94                title=title,95                description=(rec.get("meta") or {}).get("fr", ""),96                raw_categories=[x for x in cats if x],97                audience=", ".join(a for a in audience if a),98                city="Sherbrooke",99                region="Estrie",100                start_date=start or None,101                start_time=start_time,102                end_date=end or start or None,103                end_time=end_time,104                is_free=(not payant) if isinstance(payant, bool) else None,105                organizer="Ville de Sherbrooke",106                image=image,107            ))108        return events109