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%
6.4 KB · 152 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/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