# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/ilesdelamadeleine.py : Tourisme Îles-de-la-Madeleine (ATR) # Source : tourismeilesdelamadeleine.com/fr/decouvrir-les-iles/experiences/ # culture/festivals-et-evenements/ — calendrier officiel de l'ATR # (plateforme PHP maison, rendue serveur ; vérifié 2026-08-25 : # ~10 cartes avec titre, plage de dates FR « Du 22 février au # 1 mars 2026 », catégorie, image). # Extraction: 1 GET liste → cartes (lien /fr//festivals-et-evenements/, # image listesite_, titre, dates FR, catégorie). Fiches détail # incrémentales (cache data/ilesdelamadeleine_cache.json, # plafond IDM_FICHE_MAX/sync, re-visite 7 j) : microdonnées # schema.org (streetAddress, addressLocality, postalCode, # telephone) + description du bloc principal (itemprop) + # plage de dates de la fiche (souvent plus précise). # Accès : robots.txt « Allow: / » (hors utilitaires) — GET throttlés, # identification honnête, lien vers la fiche originale. # Ni prix ni heure publiés de façon structurée → jamais inventés. # ----------------------------------------------------------------------------- from __future__ import annotations import os import re import time from ..normalize import clean_text, parse_date_range_fr from ..schema import Event from .base import BaseConnector BASE = "https://www.tourismeilesdelamadeleine.com" LIST_URL = (BASE + "/fr/decouvrir-les-iles/experiences/culture/" "festivals-et-evenements/") IDM_FICHE_MAX = int(os.environ.get("IDM_FICHE_MAX", "10")) REFRESH_AFTER = 7 * 86400 # une carte du listing : lien fiche, image, titre + « aside » (dates, catégorie) _CARD_RE = re.compile( r'.*?' r'(?:]*>)?.*?' r'

\s*(.*?)', re.S) _ASIDE_DATE_RE = re.compile(r']*>([^<]*)') _CAT_RE = re.compile(r'titreCategorie">([^<]*)') _STREET_RE = re.compile(r'itemprop="streetAddress"[^>]*>([^<]*)') _CITY_RE = re.compile(r'itemprop="addressLocality"[^>]*>([^<]*)') _POSTAL_RE = re.compile(r'itemprop="postalCode"[^>]*>([^<]*)') _DESC_RE = re.compile(r'itemprop="description"[^>]*>(.*?)', re.S) _DATE_TXT_RE = re.compile( r'[Dd]u \d{1,2}(?:er)?[^<]{0,60}?\d{4}|\d{1,2}(?:er)?\s+\w+\s+\d{4}') class IlesDeLaMadeleineConnector(BaseConnector): source_id = "ilesdelamadeleine" request_delay = 0.8 def _cards(self, html: str) -> list[dict]: out = [] for m in _CARD_RE.finditer(html): path, img, title, aside = m.groups() spans = _ASIDE_DATE_RE.findall(aside or "") date_raw = clean_text(spans[0]) if spans else "" cat = _CAT_RE.search(aside or "") start, end = parse_date_range_fr(date_raw) out.append({ "path": path, "external_id": path.strip("/").split("/")[1], "title": clean_text(title), "raw_categories": [clean_text(cat.group(1)) if cat else "", clean_text(title)], "start_date": start, "end_date": end, "image": BASE + img if img else "", }) return out def _parse_fiche(self, html: str) -> dict: """Adresse/ville/description du bloc principal de la fiche membre.""" i = html.find("= 0 else html[:12000] out: dict = {} for key, rx in (("address", _STREET_RE), ("city", _CITY_RE), ("postal_code", _POSTAL_RE)): m = rx.search(seg) or rx.search(html) if m: out[key] = clean_text(m.group(1)) m = _DESC_RE.search(seg) if m: out["description"] = clean_text(m.group(1)) d = _DATE_TXT_RE.search(m.group(1)) if d: start, end = parse_date_range_fr(d.group(0)) if start or end: out["start_date"], out["end_date"] = start, end return out def fetch(self) -> list[Event]: cards = self._cards(self.get(LIST_URL).text) cache = self.load_cache() now = time.time() current = {c["path"] for c in cards} cache = {k: v for k, v in cache.items() if k in current} to_fetch = sorted( (c["path"] for c in cards if c["path"] not in cache or now - cache[c["path"]].get("ts", 0) > REFRESH_AFTER), key=lambda p: cache.get(p, {}).get("ts", 0))[:IDM_FICHE_MAX] for path in to_fetch: detail: dict = {} try: detail = self._parse_fiche(self.get(BASE + path).text) except Exception: detail = {} cache[path] = {"ts": now, "detail": detail} self.save_cache(cache) events: list[Event] = [] seen: set[str] = set() for c in cards: if not c["title"] or c["external_id"] in seen: continue seen.add(c["external_id"]) detail = cache.get(c["path"], {}).get("detail") or {} events.append(Event( source=self.source_id, external_id=c["external_id"], url=BASE + c["path"], title=c["title"], description=detail.get("description", ""), raw_categories=[x for x in c["raw_categories"] if x], address=detail.get("address", ""), city=detail.get("city", ""), postal_code=detail.get("postal_code", ""), tourist_region="Îles-de-la-Madeleine", start_date=c["start_date"] or detail.get("start_date"), end_date=c["end_date"] or detail.get("end_date"), image=c["image"], )) return events