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%
5.0 KB · 112 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/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