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%
5.1 KB · 124 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/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