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/ilesdelamadeleine.py : Tourisme Îles-de-la-Madeleine (ATR)5# Source : tourismeilesdelamadeleine.com/fr/decouvrir-les-iles/experiences/6# culture/festivals-et-evenements/ — calendrier officiel de l'ATR7# (plateforme PHP maison, rendue serveur ; vérifié 2026-08-25 :8# ~10 cartes avec titre, plage de dates FR « Du 22 février au9# 1 mars 2026 », catégorie, image).10# Extraction: 1 GET liste → cartes (lien /fr/<slug>/festivals-et-evenements/,11# image listesite_, titre, dates FR, catégorie). Fiches détail12# incrémentales (cache data/ilesdelamadeleine_cache.json,13# plafond IDM_FICHE_MAX/sync, re-visite 7 j) : microdonnées14# schema.org (streetAddress, addressLocality, postalCode,15# telephone) + description du bloc principal (itemprop) +16# plage de dates de la fiche (souvent plus précise).17# Accès : robots.txt « Allow: / » (hors utilitaires) — GET throttlés,18# identification honnête, lien vers la fiche originale.19# Ni prix ni heure publiés de façon structurée → jamais inventés.20# -----------------------------------------------------------------------------21from __future__ import annotations2223import os24import re25import time2627from ..normalize import clean_text, parse_date_range_fr28from ..schema import Event29from .base import BaseConnector3031BASE = "https://www.tourismeilesdelamadeleine.com"32LIST_URL = (BASE + "/fr/decouvrir-les-iles/experiences/culture/"33 "festivals-et-evenements/")34IDM_FICHE_MAX = int(os.environ.get("IDM_FICHE_MAX", "10"))35REFRESH_AFTER = 7 * 864003637# une carte du listing : lien fiche, image, titre + « aside » (dates, catégorie)38_CARD_RE = re.compile(39 r'<a href="(/fr/[^"]+/festivals-et-evenements/)">.*?'40 r'(?:<img src="([^"]*)" class="imageFiche"[^>]*>)?.*?'41 r'<h3>\s*(.*?)<aside>(.*?)</aside>', re.S)42_ASIDE_DATE_RE = re.compile(r'<span[^>]*>([^<]*)</span>')43_CAT_RE = re.compile(r'titreCategorie">([^<]*)')4445_STREET_RE = re.compile(r'itemprop="streetAddress"[^>]*>([^<]*)')46_CITY_RE = re.compile(r'itemprop="addressLocality"[^>]*>([^<]*)')47_POSTAL_RE = re.compile(r'itemprop="postalCode"[^>]*>([^<]*)')48_DESC_RE = re.compile(r'itemprop="description"[^>]*>(.*?)</div>', re.S)49_DATE_TXT_RE = re.compile(50 r'[Dd]u \d{1,2}(?:er)?[^<]{0,60}?\d{4}|\d{1,2}(?:er)?\s+\w+\s+\d{4}')515253class IlesDeLaMadeleineConnector(BaseConnector):54 source_id = "ilesdelamadeleine"55 request_delay = 0.85657 def _cards(self, html: str) -> list[dict]:58 out = []59 for m in _CARD_RE.finditer(html):60 path, img, title, aside = m.groups()61 spans = _ASIDE_DATE_RE.findall(aside or "")62 date_raw = clean_text(spans[0]) if spans else ""63 cat = _CAT_RE.search(aside or "")64 start, end = parse_date_range_fr(date_raw)65 out.append({66 "path": path,67 "external_id": path.strip("/").split("/")[1],68 "title": clean_text(title),69 "raw_categories": [clean_text(cat.group(1)) if cat else "",70 clean_text(title)],71 "start_date": start, "end_date": end,72 "image": BASE + img if img else "",73 })74 return out7576 def _parse_fiche(self, html: str) -> dict:77 """Adresse/ville/description du bloc principal de la fiche membre."""78 i = html.find("<h1")79 seg = html[i:i + 12000] if i >= 0 else html[:12000]80 out: dict = {}81 for key, rx in (("address", _STREET_RE), ("city", _CITY_RE),82 ("postal_code", _POSTAL_RE)):83 m = rx.search(seg) or rx.search(html)84 if m:85 out[key] = clean_text(m.group(1))86 m = _DESC_RE.search(seg)87 if m:88 out["description"] = clean_text(m.group(1))89 d = _DATE_TXT_RE.search(m.group(1))90 if d:91 start, end = parse_date_range_fr(d.group(0))92 if start or end:93 out["start_date"], out["end_date"] = start, end94 return out9596 def fetch(self) -> list[Event]:97 cards = self._cards(self.get(LIST_URL).text)98 cache = self.load_cache()99 now = time.time()100 current = {c["path"] for c in cards}101 cache = {k: v for k, v in cache.items() if k in current}102103 to_fetch = sorted(104 (c["path"] for c in cards105 if c["path"] not in cache106 or now - cache[c["path"]].get("ts", 0) > REFRESH_AFTER),107 key=lambda p: cache.get(p, {}).get("ts", 0))[:IDM_FICHE_MAX]108 for path in to_fetch:109 detail: dict = {}110 try:111 detail = self._parse_fiche(self.get(BASE + path).text)112 except Exception:113 detail = {}114 cache[path] = {"ts": now, "detail": detail}115 self.save_cache(cache)116117 events: list[Event] = []118 seen: set[str] = set()119 for c in cards:120 if not c["title"] or c["external_id"] in seen:121 continue122 seen.add(c["external_id"])123 detail = cache.get(c["path"], {}).get("detail") or {}124 events.append(Event(125 source=self.source_id,126 external_id=c["external_id"],127 url=BASE + c["path"],128 title=c["title"],129 description=detail.get("description", ""),130 raw_categories=[x for x in c["raw_categories"] if x],131 address=detail.get("address", ""),132 city=detail.get("city", ""),133 postal_code=detail.get("postal_code", ""),134 tourist_region="Îles-de-la-Madeleine",135 start_date=c["start_date"] or detail.get("start_date"),136 end_date=c["end_date"] or detail.get("end_date"),137 image=c["image"],138 ))139 return events140