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.1 KB · 140 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/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