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/saintjerome.py : connecteur Ville de Saint-Jérôme — calendrier5# Source : https://www.vsj.ca/calendrier/ (WordPress + FacetWP).6# Extraction: le listing FacetWP se pagine CÔTÉ SERVEUR par paramètres7# d'URL (vérifié 2026-08-25) : GET /calendrier/?fwp_calendar_8# dates=<aujourd'hui>&fwp_paged=N → cartes HTML rendues9# (10/page) : date FR, heure « 19 h », lieu, titre, url fiche,10# extrait, image. On pagine jusqu'à épuisement (plafond11# VSJ_PAGES). Le flux RSS /calendrier/feed/ est périmé (2019)12# et l'API REST wp/v2/event ne publie pas les dates.13# Accès : site municipal public, robots.txt permissif (wp-admin14# seulement). GET directs throttlés, identification honnête.15# Prix : non publié dans les cartes → jamais inventé.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import html as _html20import os21import re22from datetime import date2324from ..normalize import clean_text, parse_date_fr, parse_time25from ..schema import Event26from .base import BaseConnector2728LIST_URL = "https://www.vsj.ca/calendrier/"29PAGES_MAX = int(os.environ.get("VSJ_PAGES", "12"))3031# la classe exacte est « event__single ... » — ne pas confondre avec les32# sous-blocs event__single--image / --content33_CARD_RE = re.compile(34 r'<div[^>]*class="event__single(?:\s[^"]*)?"[^>]*>(.*?)(?=<div[^>]*class="event__single(?:\s[^"]*)?"|<!-- \.facetwp-template|$)',35 re.S)36_DATE_RE = re.compile(r'class="event__date">([^<]+)</h4>')37_TITLE_RE = re.compile(38 r'class="event__title">\s*<a href="([^"]+)">([^<]+)</a>', re.S)39_TIME_RE = re.compile(r'class="event__time">(?:<i[^>]*></i>)?([^<]+)</div>')40_PLACE_RE = re.compile(r'class="event__place">(?:<i[^>]*></i>)?([^<]+)</span>')41_DESC_RE = re.compile(r'class="event__desc">\s*<p>(.*?)</p>', re.S)42# rendu FacetWP côté serveur : <img src='...'> (pas de lazy-load)43_IMG_RE = re.compile(44 r"<img[^>]+src=['\"](https?://[^'\"]*wp-content/uploads/[^'\"]+)['\"]|"45 r"data-lazy-src=\"([^\"]+)\"")464748class SaintJeromeConnector(BaseConnector):49 source_id = "saintjerome"50 request_delay = 0.85152 def _parse_page(self, html: str) -> list[dict]:53 rows: list[dict] = []54 for block in _CARD_RE.findall(html):55 t = _TITLE_RE.search(block)56 d = _DATE_RE.search(block)57 if not t or not d:58 continue59 start = parse_date_fr(d.group(1))60 if not start:61 continue62 tm = _TIME_RE.search(block)63 start_time = parse_time(tm.group(1)) if tm else None64 pl = _PLACE_RE.search(block)65 venue = address = ""66 if pl:67 # « Hôtel de ville (300, rue Parent) » → lieu + adresse68 m = re.match(r"^(.*?)\s*(?:\(([^)]+)\))?\s*$",69 clean_text(_html.unescape(pl.group(1))))70 venue, address = m.group(1), m.group(2) or ""71 desc = _DESC_RE.search(block)72 img = _IMG_RE.search(block)73 image = (img.group(1) or img.group(2)) if img else ""74 rows.append({75 "url": t.group(1),76 "title": clean_text(_html.unescape(t.group(2))),77 "start": start,78 "start_time": start_time,79 "venue": venue,80 "address": address,81 "description": clean_text(re.sub(82 r"<[^>]+>", " ", _html.unescape(desc.group(1)))) if desc else "",83 "image": image,84 })85 return rows8687 def fetch(self) -> list[Event]:88 rows: list[dict] = []89 today = date.today().isoformat()90 for page in range(1, PAGES_MAX + 1):91 params = {"fwp_calendar_dates": today}92 if page > 1:93 params["fwp_paged"] = str(page)94 batch = self._parse_page(self.get(LIST_URL, params=params).text)95 rows += batch96 if len(batch) < 10:97 break98 events: list[Event] = []99 seen: set[str] = set()100 for r in rows:101 slug = r["url"].rstrip("/").rsplit("/", 1)[-1]102 ext = f"{slug}-{r['start']}"103 if ext in seen:104 continue105 seen.add(ext)106 events.append(Event(107 source=self.source_id,108 external_id=ext,109 url=r["url"],110 title=r["title"],111 description=r["description"],112 raw_categories=[r["title"]],113 venue=r["venue"],114 address=r["address"],115 city="Saint-Jérôme",116 region="Laurentides",117 start_date=r["start"],118 start_time=r["start_time"],119 end_date=r["start"],120 organizer="Ville de Saint-Jérôme",121 image=r["image"],122 ))123 return events124