# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/sherbrooke.py : connecteur Ville de Sherbrooke — calendrier officiel # Source : sherbrooke.ca, « Calendrier des événements en temps réel » # (référencé sur Données Québec : calendrier-des-evenements-en-temps-reel) # Extraction: la page rend TOUT le calendrier dans window['TRANSFER_CACHE'] # (JSON embarqué, CMS Maruche) → 1 seule requête, ~270 événements # (nom, périodes, gratuité `payant`, catégories, clientèles, # images /Fichiers//Entities//). # Accès : page municipale publique, GET direct, identification honnête. # Prix : booléen `payant` de la source → is_free (montants non exposés). # ----------------------------------------------------------------------------- from __future__ import annotations import json from ..normalize import utc_to_local from ..schema import Event from .base import BaseConnector PAGE = ("https://www.sherbrooke.ca/fr/culture-sports-et-loisirs/" "calendrier-des-activites") SITE = "https://www.sherbrooke.ca" # GUID du module « fichiers d'entités » du calendrier (constaté via og:image) FILES_MODULE = "3337a882-4a53-e611-80ea-00155d09650f" def _find_events(o): """Localise la liste d'événements (dicts avec visibleId) dans le cache JSON.""" if isinstance(o, list) and o and isinstance(o[0], dict) and "visibleId" in o[0]: return o if isinstance(o, dict): for v in o.values(): r = _find_events(v) if r: return r if isinstance(o, list): for v in o: r = _find_events(v) if r: return r return None class SherbrookeConnector(BaseConnector): source_id = "sherbrooke" def _extract_cache(self, html: str) -> dict: i = html.find("window['TRANSFER_CACHE'] =") if i < 0: raise ValueError("TRANSFER_CACHE introuvable (page modifiée ?)") start = html.find("{", i) end = html.find("", start) return json.loads(html[start:end].strip().rstrip(";")) def fetch(self) -> list[Event]: html = self.get(PAGE).text records = _find_events(self._extract_cache(html)) or [] events: list[Event] = [] for rec in records: title = (rec.get("name") or {}).get("fr", "") full_url = (rec.get("fullUrl") or {}).get("fr", "") periods = rec.get("periods") or [] if not title or not full_url or not periods: continue # les périodes sont publiées en UTC (« …T04:00:00Z » = minuit # local, placeholder « toute la journée ») → date + heure locales start, start_time = utc_to_local(periods[0].get("dateDebut")) end, end_time = utc_to_local(periods[-1].get("dateFin")) cats, audience = [], [] c = rec.get("categories") or {} cats += [(b.get("name") or {}).get("fr", "") for b in c.get("base") or []] for block in c.get("block") or []: label = (block.get("name") or {}).get("fr", "") children = [(ch.get("name") or {}).get("fr", "") for ch in block.get("children") or []] if label == "Clientèle": audience += children elif label != "Secteur": cats += children image = "" if rec.get("mainPicture") and rec.get("id"): image = (f"{SITE}/Fichiers/{FILES_MODULE}/Entities/" f"{rec['id']}/{rec['mainPicture']}") payant = rec.get("payant") events.append(Event( source=self.source_id, external_id=str(rec.get("visibleId") or rec.get("id")), url=SITE + full_url, title=title, description=(rec.get("meta") or {}).get("fr", ""), raw_categories=[x for x in cats if x], audience=", ".join(a for a in audience if a), city="Sherbrooke", region="Estrie", start_date=start or None, start_time=start_time, end_date=end or start or None, end_time=end_time, is_free=(not payant) if isinstance(payant, bool) else None, organizer="Ville de Sherbrooke", image=image, )) return events