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/tourismelaval.py : Tourisme Laval (ATR, vague ATR 2026-08)5# Source : tourismelaval.com/explorer-possibilites/evenements/ — calendrier6# officiel de l'ATR (WordPress + FacetWP, CPT « event », cartes7# RENDUES SERVEUR ; vérifié 2026-08-25 : 21 événements sur8# 2 pages). NB : le calendrier « tribe » du même site9# (/calendrier/) est mort à la source (derniers événements10# décembre 2025) ; le CPT event est le vivant. L'API REST11# wp/v2/event est verrouillée (401 Kadence Security) → parse des12# cartes SSR.13# Extraction: GET /explorer-possibilites/evenements/ (+ /page/N/) →14# <article class="grid__item item--event post-<id>"> : lien,15# image, catégorie, titre, extrait, plage de dates FR16# (« 18 août 2026 - 25 août 2026 »). Pas de fiche détail17# (adresse/heure non structurées sur les fiches, vérifié).18# Accès : pages publiques — GET throttlés, identification honnête, lien19# vers la fiche originale. Ni prix ni heure structurés → jamais20# inventés. Ville = Laval (périmètre de l'ATR, île de Laval).21# -----------------------------------------------------------------------------22from __future__ import annotations2324import os25import re2627from ..normalize import clean_text, parse_date_range_fr28from ..schema import Event29from .base import BaseConnector3031BASE = "https://www.tourismelaval.com"32LIST_URL = BASE + "/explorer-possibilites/evenements/"33LVT_PAGE_MAX = int(os.environ.get("LVT_PAGE_MAX", "5"))3435_ARTICLE_RE = re.compile(36 r'<article class="grid__item item--event post-(\d+)(.*?)</article>', re.S)37_LINK_RE = re.compile(r'<a class="item__inner" href="([^"]+)"')38_IMG_RE = re.compile(r'<img[^>]+src="([^"]+)"')39_CAT_RE = re.compile(r'post__cat">\s*<p[^>]*>([^<]*)</p>')40_TITLE_RE = re.compile(r'grid__title"[^>]*>([^<]*)</h5>')41_EXCERPT_RE = re.compile(r'post__excerpt"\s*>\s*(.*?)</div>', re.S)42_DATE_RE = re.compile(r'post__date">.*?<div[^>]*>([^<]*)</div>', re.S)434445class TourismeLavalConnector(BaseConnector):46 source_id = "tourismelaval"47 request_delay = 0.84849 def _articles(self, html: str) -> list[dict]:50 out = []51 for m in _ARTICLE_RE.finditer(html):52 eid, body = m.groups()53 link = _LINK_RE.search(body)54 title = _TITLE_RE.search(body)55 if not (link and title):56 continue57 date_m = _DATE_RE.search(body)58 start = end = None59 if date_m:60 # « 18 août 2026 - 25 août 2026 » ou « 27 juin 2026 »61 parts = [p.strip() for p in date_m.group(1).split(" - ")]62 start, _ = parse_date_range_fr(parts[0])63 end = (parse_date_range_fr(parts[1])[0]64 if len(parts) > 1 else start)65 img = _IMG_RE.search(body)66 cat = _CAT_RE.search(body)67 excerpt = _EXCERPT_RE.search(body)68 out.append({69 "external_id": eid,70 "url": link.group(1),71 "title": clean_text(title.group(1)),72 "raw_categories": [clean_text(cat.group(1)) if cat else "",73 clean_text(title.group(1))],74 "description": clean_text(excerpt.group(1)) if excerpt else "",75 "start_date": start, "end_date": end,76 "image": img.group(1) if img else "",77 })78 return out7980 def fetch(self) -> list[Event]:81 events: list[Event] = []82 seen: set[str] = set()83 for page in range(1, LVT_PAGE_MAX + 1):84 url = LIST_URL if page == 1 else f"{LIST_URL}page/{page}/"85 try:86 articles = self._articles(self.get(url).text)87 except Exception:88 break89 new = [a for a in articles if a["external_id"] not in seen]90 if not new:91 break92 for a in new:93 seen.add(a["external_id"])94 if not a["title"]:95 continue96 events.append(Event(97 source=self.source_id,98 external_id=a["external_id"],99 url=a["url"],100 title=a["title"],101 description=a["description"],102 raw_categories=[c for c in a["raw_categories"] if c],103 city="Laval",104 tourist_region="Laval",105 start_date=a["start_date"],106 end_date=a["end_date"],107 image=a["image"],108 ))109 if len(articles) < 12: # page incomplète (12/page) = dernière110 break111 return events112