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/mauricie.py : Tourisme Mauricie (ATR) — RÉHABILITÉE (vague ATR5# 2026-08 ; écartée Phase 3 « liste rendue JS », l'endpoint XHR est percé)6# Source : tourismemauricie.com/evenements — calendrier officiel de l'ATR7# (thème WordPress « Acolyte », listing Vue.js).8# Extraction: API JSON interne du thème (celle que le composant Vue appelle,9# vérifiée 2026-08-25) :10# GET /wp-json/acolyte/v1/get-events-data?lang=fr&page=N11# → ~174 événements paginés 9/page : id, titre, url, images CDN,12# ville, salle (venue_name), catégories, date FR, GPS. Fiches13# détail incrémentales (cache data/mauricie_cache.json, plafond14# MAU_FICHE_MAX/sync, re-visite 7 j) : JSON-LD Event → date de15# début ISO, adresse civique, code postal, description.16# Accès : API REST WordPress publique du thème (aucune auth), robots.txt17# sans interdiction — GET throttlés, UA honnête. Ni prix ni heure18# structurés publiés → jamais inventés. Région administrative19# résolue par la VILLE (l'ATR couvre aussi des villes du20# Centre-du-Québec : Bécancour, Nicolet…), repli « Mauricie ».21# -----------------------------------------------------------------------------22from __future__ import annotations2324import json25import os26import re27import time2829from ..normalize import clean_text, parse_date_range_fr30from ..regions import region_from_city31from ..schema import Event32from .base import BaseConnector3334BASE = "https://tourismemauricie.com"35API = BASE + "/wp-json/acolyte/v1/get-events-data"36MAU_PAGE_MAX = int(os.environ.get("MAU_PAGE_MAX", "40"))37MAU_FICHE_MAX = int(os.environ.get("MAU_FICHE_MAX", "12"))38REFRESH_AFTER = 7 * 864003940_LDJSON_RE = re.compile(41 r'<script type="application/ld\+json"[^>]*>(.*?)</script>', re.S)424344def _ld_event(html: str) -> dict | None:45 for m in _LDJSON_RE.findall(html):46 try:47 data = json.loads(m)48 except ValueError:49 continue50 for item in (data if isinstance(data, list) else [data]):51 if isinstance(item, dict) and item.get("@type") == "Event":52 return item53 return None545556class MauricieConnector(BaseConnector):57 source_id = "mauricie"58 request_delay = 0.85960 def _list_events(self) -> list[dict]:61 rows: list[dict] = []62 page, total_pages = 1, 163 while page <= min(total_pages, MAU_PAGE_MAX):64 data = self.get_json(API, params={"lang": "fr", "page": page})65 batch = data.get("events") or []66 if not batch:67 break68 rows.extend(batch)69 total_pages = int(data.get("total_pages") or 1)70 page += 171 return rows7273 def _parse_fiche(self, html: str) -> dict:74 """JSON-LD Event de la fiche : date ISO, adresse, description."""75 ld = _ld_event(html)76 if not ld:77 return {}78 addr = ((ld.get("location") or {}).get("address") or {})79 return {80 "start_date": (ld.get("startDate") or "")[:10],81 "end_date": (ld.get("endDate") or "")[:10],82 "description": clean_text(ld.get("description") or ""),83 "address": clean_text(addr.get("streetAddress") or ""),84 "city": clean_text(addr.get("addressLocality") or ""),85 "postal_code": (addr.get("postalCode") or "").strip(),86 }8788 def fetch(self) -> list[Event]:89 rows = self._list_events()90 cache = self.load_cache()91 now = time.time()92 current = {str(r.get("id")) for r in rows}93 cache = {k: v for k, v in cache.items() if k in current}9495 to_fetch = sorted(96 (r for r in rows97 if r.get("url")98 and (str(r.get("id")) not in cache99 or now - cache[str(r.get("id"))].get("ts", 0) > REFRESH_AFTER)),100 key=lambda r: cache.get(str(r.get("id")), {}).get("ts", 0)101 )[:MAU_FICHE_MAX]102 for row in to_fetch:103 eid = str(row.get("id"))104 detail: dict = {}105 try:106 detail = self._parse_fiche(self.get(row["url"]).text)107 except Exception:108 detail = {}109 cache[eid] = {"ts": now, "detail": detail}110 self.save_cache(cache)111112 events: list[Event] = []113 seen: set[str] = set()114 for r in rows:115 eid = str(r.get("id") or "")116 title = clean_text(r.get("title") or "")117 if not eid or eid in seen or not title:118 continue119 seen.add(eid)120 detail = cache.get(eid, {}).get("detail") or {}121 start, end = parse_date_range_fr(r.get("date") or "")122 pos = r.get("position") or {}123 city = clean_text(r.get("city") or "") or detail.get("city", "")124 cats = [clean_text(c.get("title", "")) for c in125 (r.get("categories") or []) if isinstance(c, dict)]126 # la source recopie parfois le nom de l'événement comme nom de127 # salle — on ne garde le lieu que s'il diffère du titre128 venue = clean_text(r.get("venue_name") or "")129 if venue.lower() == title.lower():130 venue = ""131 events.append(Event(132 source=self.source_id,133 external_id=eid,134 url=r.get("url", ""),135 title=title,136 description=detail.get("description", ""),137 raw_categories=[c for c in cats if c] + [title],138 venue=venue,139 address=detail.get("address", ""),140 city=city,141 # l'ATR couvre des villes du Centre-du-Québec : la ville prime142 region=region_from_city(city),143 postal_code=detail.get("postal_code", ""),144 tourist_region="Mauricie",145 lat=pos.get("lat"), lng=pos.get("lng"),146 start_date=detail.get("start_date") or start,147 end_date=detail.get("end_date") or end,148 image=r.get("image_large") or r.get("image_medium")149 or r.get("image_small") or "",150 ))151 return events152