Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
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