# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/troisrivieres.py : connecteur Ville de Trois-Rivières — activités # Source : https://www.v3r.net/activites-et-loisirs/calendrier-des-activites # (l'API REST WP /wp-json est verrouillée en 401 — consigné # 2026-08-18 — mais le calendrier AngularJS interroge un # endpoint AJAX public du thème). # Extraction: POST JSON /ajax/wp-queries/archive-activites.php (même appel # que le frontend, découvert dans ActivitesController.js) — # 1 requête : title, startDate/endDate « dd/mm/yyyy », # description (= NOM DU LIEU, vérifié sur les 17 fiches live # 2026-08-25), imageUrl, url de la fiche. Heure non publiée # par l'endpoint → jamais inventée. # Accès : endpoint public du site municipal (aucune auth) ; robots.txt # n'interdit pas /ajax/. GET/POST directs, identification # honnête, 1 requête par sync. # Prix : non publié par l'endpoint → jamais inventé. # ----------------------------------------------------------------------------- from __future__ import annotations import re import time from ..normalize import clean_text from ..schema import Event from .base import BaseConnector API = "https://www.v3r.net/ajax/wp-queries/archive-activites.php" QUERY = { "postType": "activites", "orderBy": "meta_value date", "order": "DESC", "filters": {"category": "", "startDate": "", "endDate": ""}, } _DDMMYYYY_RE = re.compile(r"^(\d{1,2})/(\d{1,2})/(\d{4})$") def _iso(raw: str | None) -> str | None: """« 25/08/2026 » (format de l'endpoint) → « 2026-08-25 ».""" m = _DDMMYYYY_RE.match(str(raw or "").strip()) if not m: return None d, mo, y = int(m.group(1)), int(m.group(2)), int(m.group(3)) if not (1 <= d <= 31 and 1 <= mo <= 12 and 1990 <= y <= 2100): return None return f"{y:04d}-{mo:02d}-{d:02d}" class TroisRivieresConnector(BaseConnector): source_id = "troisrivieres" request_delay = 0.8 def _post_json(self) -> dict: self._throttle() resp = self.session.post(API, json={"query": QUERY}, timeout=self.timeout) self._last_request = time.time() resp.raise_for_status() return resp.json() def fetch(self) -> list[Event]: payload = self._post_json() events: list[Event] = [] seen: set[str] = set() for rec in payload.get("data") or []: title = clean_text(rec.get("title") or "") url = rec.get("url") or "" start = _iso(rec.get("startDate")) if not title or not url or not start: continue slug = url.rstrip("/").rsplit("/", 1)[-1] ext = f"{slug}-{start}" if ext in seen: continue seen.add(ext) events.append(Event( source=self.source_id, external_id=ext, url=url, title=title, raw_categories=[title], venue=clean_text(rec.get("description") or ""), city="Trois-Rivières", region="Mauricie", start_date=start, end_date=_iso(rec.get("endDate")) or start, organizer="Ville de Trois-Rivières", image=rec.get("imageUrl") or "", )) return events