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.2 KB · 145 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/sainthyacinthe.py : connecteur Ville de Saint-Hyacinthe —5#   calendrier des événements (généré par les organismes accrédités).6#   Source    : https://www.st-hyacinthe.ca/loisirs-et-culture/evenement/7#               calendrier-des-evenements (CMS municipal maison, rendu8#               serveur Bootstrap).9#   Extraction: GET ?ms=2 (« Tous » les mois, découvert dans le formulaire de10#               recherche) → toutes les cartes en 1 requête : id + titre11#               (fShowInfo), plage de dates FR, image, organisateur,12#               catégorie. Enrichissement par fiche modale13#               /php/load-modal-info.php?evenement=<id> (lieu, clientèle,14#               gratuit/payant, site web) — CACHE INCRÉMENTAL disque : seules15#               les fiches jamais vues sont requêtées (plafond STH_FICHES).16#   Accès     : site municipal public, GET directs throttlés, identification17#               honnête ; robots.txt ne restreint pas ces chemins.18#   Prix      : la fiche publie « Gratuit »/« Payant » → is_free fidèle,19#               jamais de montant inventé.20# -----------------------------------------------------------------------------21from __future__ import annotations2223import html as _html24import os25import re2627from ..normalize import clean_text, parse_date_range_fr28from ..schema import Event29from .base import BaseConnector3031BASE = "https://www.st-hyacinthe.ca"32LIST_URL = BASE + "/loisirs-et-culture/evenement/calendrier-des-evenements"33MODAL_URL = BASE + "/php/load-modal-info.php?evenement={id}"34FICHES_MAX = int(os.environ.get("STH_FICHES", "60"))3536_CARD_RE = re.compile(r'<div class="card h-100">(.*?)</div>\s*</div>\s*</div>',37                      re.S)38_INFO_RE = re.compile(r"fShowInfo\('(\d+)','(.*?)'\)")39_DATES_RE = re.compile(r"<strong>(.*?)</strong>", re.S)40_IMG_RE = re.compile(r'background:url\(([^)]+)\)')41_ORG_RE = re.compile(r"Organisateur de l'événement\"></i>\s*([^<]+)</p>")42_CAT_RE = re.compile(r"Type\(s\) de l'événement\"></i>\s*([^<]+)</p>")43# fiche modale44_M_VENUE_RE = re.compile(r"Lieu de l'événement\"></i>\s*([^<]+)</p>")45_M_AUD_RE = re.compile(r"Clientèle\(s\) de l'événement\"></i>\s*([^<]+)</p>")46_M_PRICE_RE = re.compile(47    r'data-content="Cette activité (?:est gratuite|n\'est pas gratuite)"></i>\s*([^<]+)</p>')48_M_SITE_RE = re.compile(r'<a href="([^"]+)"[^>]*>Consulter le site internet')49_M_HOURS_RE = re.compile(r'fa-clock[^>]*"></i>\s*([^<]+)</p>')505152class SaintHyacintheConnector(BaseConnector):53    source_id = "sainthyacinthe"54    request_delay = 0.85556    def _parse_list(self, html: str) -> list[dict]:57        rows: list[dict] = []58        for block in _CARD_RE.findall(html):59            m = _INFO_RE.search(block)60            d = _DATES_RE.search(block)61            if not m or not d:62                continue63            start, end = parse_date_range_fr(clean_text(d.group(1)))64            if not start and not end:65                continue66            img = _IMG_RE.search(block)67            org = _ORG_RE.search(block)68            cat = _CAT_RE.search(block)69            rows.append({70                "id": m.group(1),71                "title": clean_text(_html.unescape(m.group(2))),72                "start": start or end,73                "end": end or start,74                "image": (BASE + img.group(1)) if img75                         and img.group(1).startswith("/") else76                         (img.group(1) if img else ""),77                "organizer": clean_text(org.group(1)) if org else "",78                "category": clean_text(cat.group(1)) if cat else "",79            })80        return rows8182    def _parse_modal(self, html: str) -> dict:83        out: dict = {}84        m = _M_VENUE_RE.search(html)85        if m:86            out["venue"] = clean_text(m.group(1))87        m = _M_AUD_RE.search(html)88        if m:89            out["audience"] = clean_text(m.group(1))90        m = _M_PRICE_RE.search(html)91        if m:92            out["price_label"] = clean_text(m.group(1))   # Gratuit | Payant93        m = _M_SITE_RE.search(html)94        if m:95            out["website"] = m.group(1).strip()96        m = _M_HOURS_RE.search(html)97        if m:98            out["hours"] = clean_text(m.group(1))99        return out100101    def fetch(self) -> list[Event]:102        rows = self._parse_list(self.get(LIST_URL, params={"ms": "2"}).text)103        cache = self.load_cache()104        fetched = 0105        for r in rows:106            if r["id"] not in cache and fetched < FICHES_MAX:107                try:108                    cache[r["id"]] = self._parse_modal(109                        self.get(MODAL_URL.format(id=r["id"])).text)110                    fetched += 1111                except Exception as exc:   # fiche cassée ≠ source cassée112                    print(f"[sorti-ka] sainthyacinthe : fiche {r['id']} "113                          f"ignorée : {exc}")114        if fetched:115            self.save_cache(cache)116        events: list[Event] = []117        seen: set[str] = set()118        for r in rows:119            ext = f"{r['id']}-{r['start']}"120            if ext in seen:121                continue122            seen.add(ext)123            extra = cache.get(r["id"]) or {}124            label = extra.get("price_label", "")125            events.append(Event(126                source=self.source_id,127                external_id=ext,128                url=LIST_URL + "?ms=2",   # pas de fiche pérenne (modale)129                title=r["title"],130                raw_categories=[r["category"]] if r["category"] else [r["title"]],131                audience=extra.get("audience", ""),132                venue=extra.get("venue", ""),133                city="Saint-Hyacinthe",134                region="Montérégie",135                start_date=r["start"],136                end_date=r["end"],137                is_free=(True if label == "Gratuit"138                         else False if label == "Payant" else None),139                price_label=label,140                organizer=r["organizer"],141                website=extra.get("website", ""),142                image=r["image"],143            ))144        return events145