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/laval.py : connecteur Ville de Laval — calendrier des activités5# Source : donneesquebec.ca, jeu « Calendrier des activités » (laval.ca)6# Extraction: fichier JSON officiel (données ouvertes, licence CC 4.0)7# Couverture: ~300 activités municipales (bibliothèques, culture, famille,8# plein air) avec dates, catégories, clientèles, lieux, images.9# https://www.donneesquebec.ca/recherche/dataset/calendrier-des-activites10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from ..normalize import utc_to_local16from ..schema import Event17from .base import BaseConnector1819# la Ville publie l'annulation DANS le titre : « ANNULÉ - Rencontre avec … »20_CANCELLED_RE = re.compile(r"^\s*annul[eé]e?\s*[-–—:]\s*", re.I)2122FEED = ("https://www.donneesquebec.ca/recherche/dataset/"23 "df7add74-a741-4374-b3bd-22da8921c8b3/resource/"24 "b51a25de-bd06-4247-87ba-2b1ea8228005/download/calendrier-activites.json")25SITE = "https://www.laval.ca"262728def _labels(items) -> list[str]:29 return [i.get("Label", "") for i in items or [] if i and i.get("Label")]303132class LavalConnector(BaseConnector):33 source_id = "laval"3435 def fetch(self) -> list[Event]:36 events: list[Event] = []37 for rec in self.get_json(FEED):38 title = (rec.get("Title") or "").strip()39 page = (rec.get("PageUrl") or "").strip()40 if not title or not page:41 continue42 # statut : préfixe « ANNULÉ - » du titre → cancelled (titre nettoyé)43 status = ""44 if _CANCELLED_RE.match(title):45 status = "cancelled"46 title = _CANCELLED_RE.sub("", title).strip()47 image = rec.get("ImageUrl") or ""48 # une activité récurrente = une entrée par occurrence, même PageUrl49 # → l'id externe inclut la date pour ne pas écraser les occurrences50 slug = page.rsplit("/", 1)[-1].removesuffix(".aspx")51 day = (rec.get("EventDate") or "")[:10]52 # EventDate/EndDate sont publiés en UTC (vérifié 2026-08 : les53 # activités d'été tombent à 18h30 locales une fois converties) →54 # date + heure locales Québec ; minuit local = « toute la journée ».55 start_local, time_local = utc_to_local(rec.get("EventDate"))56 end_local, _ = utc_to_local(rec.get("EndDate"))57 events.append(Event(58 source=self.source_id,59 external_id=f"{slug}-{day}" if day else slug,60 url=SITE + page,61 title=title,62 description=rec.get("Excerpt") or "",63 raw_categories=(_labels(rec.get("Categories"))64 + _labels(rec.get("Subcategories"))65 + _labels(rec.get("Tags"))),66 audience=", ".join(_labels(rec.get("Clienteles"))),67 venue=", ".join(_labels(rec.get("Locations"))),68 city="Laval",69 region="Laval",70 start_date=start_local or rec.get("EventDate"),71 start_time=time_local,72 end_date=end_local or rec.get("EndDate"),73 status=status,74 organizer="Ville de Laval",75 image=(SITE + image) if image.startswith("/") else image,76 ))77 return events78