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%
4.6 KB · 103 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/gatineau.py : connecteur Ville de Gatineau — calendrier officiel5#   Source    : https://calendrier.gatineau.cloud — application calendrier6#               dédiée de la Ville (gatineau.ca/calendrier y redirige).7#   Extraction: API JSON interne POST /calendrier/json/evenements.aspx8#               (même appel que le frontend, découvert dans scripts.js) —9#               ~160 événements / ~250 occurrences en 1 requête : titre,10#               description, image, lien officiel, occurrences horodatées11#               (UTC → heure locale via utc_to_local), emplacements12#               (nom, adresse, secteur). Vérifié 2026-08-25.13#   Accès     : application municipale publique, robots.txt « robots: all »14#               sur la page, GET/POST directs, identification honnête.15#   Prix      : non publié par l'API → jamais inventé (les liens « Acheter16#               des billets » sont conservés dans website).17# -----------------------------------------------------------------------------18from __future__ import annotations1920import os21import time2223from ..normalize import clean_text, utc_to_local24from ..schema import Event25from .base import BaseConnector2627BASE = "https://calendrier.gatineau.cloud"28API = BASE + "/calendrier/json/evenements.aspx"29FICHE = BASE + "/calendrier/evenement.aspx?langue=fr-ca&id_evenement={id}"30LIGNES = int(os.environ.get("GATINEAU_LIGNES", "500"))   # plafond de lignes313233class GatineauConnector(BaseConnector):34    source_id = "gatineau"35    request_delay = 0.83637    def _post_json(self, data: dict):38        self._throttle()39        resp = self.session.post(API, data=data, timeout=self.timeout)40        self._last_request = time.time()41        resp.raise_for_status()42        return resp.json()4344    def fetch(self) -> list[Event]:45        records = self._post_json({"langue": "fr-ca", "lignes": str(LIGNES)})46        events: list[Event] = []47        seen: set[str] = set()48        for rec in records:49            ev_id = str(rec.get("id") or "")50            title = clean_text(rec.get("evenement") or "")51            if not ev_id or not title:52                continue53            description = clean_text(rec.get("description") or "")54            image = rec.get("image") or ""55            website = rec.get("url") or ""56            # billets éventuels (« Acheter des billets ») → site officiel57            if not website:58                for act in rec.get("actions") or []:59                    if act.get("url"):60                        website = act["url"]61                        break62            cats = [clean_text(e.get("etiquette") or str(e))63                    for e in rec.get("etiquettes") or [] if e]64            for occ in rec.get("occurrences") or []:65                ext = f"{ev_id}-{occ.get('id')}"66                if ext in seen:67                    continue68                seen.add(ext)69                # horodatages publiés en UTC (« ...Z ») → date + heure locales70                start, start_time = utc_to_local(occ.get("date_heure_debut"))71                end, end_time = utc_to_local(occ.get("date_heure_fin"))72                if not start:73                    continue74                venue = address = ""75                emp = (occ.get("emplacements") or [{}])[0]76                venue = clean_text(emp.get("emplacement_primaire")77                                   or emp.get("emplacement") or "")78                address = clean_text(emp.get("adresse") or "")79                desc = description or clean_text(occ.get("description") or "")80                mention = clean_text(occ.get("mention_speciale") or "")81                if mention:82                    desc = f"{desc}\n{mention}".strip()83                events.append(Event(84                    source=self.source_id,85                    external_id=ext,86                    url=FICHE.format(id=ev_id),87                    title=title,88                    description=desc,89                    raw_categories=[c for c in cats if c] or [title],90                    venue=venue,91                    address=address,92                    city="Gatineau",93                    region="Outaouais",94                    start_date=start,95                    start_time=start_time,96                    end_date=end or start,97                    end_time=end_time,98                    organizer="Ville de Gatineau",99                    website=website,100                    image=image,101                ))102        return events103