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.1 KB · 121 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/quebeccite.py : Québec cité / Destination Québec cité (ATR,5#   vague ATR 2026-08)6#   Source    : quebec-cite.com/fr/quoi-faire-quebec/evenements — calendrier7#               officiel de l'office de tourisme de la région de Québec8#               (Drupal, listing servi par le même index Algolia que le site).9#   Extraction: index Algolia « default_fr_event_date_start_asc » (appId/clé10#               de recherche publiques, embarquées dans le JS du thème —11#               vérifié 2026-08-25 : ~840 fiches avec titre, résumé, lieu12#               (event_location), GPS, prix texte (« À partir de 98,73 $ »),13#               gratuité event_free, dates start/end en timestamps Unix,14#               type/sujets/thèmes, image, lien externe officiel). 1-215#               requêtes/sync (hitsPerPage=1000, filtre event_date_end >=16#               hier). Aucune fiche détail nécessaire.17#   Accès     : clé Algolia « search-only » publiée par le site lui-même —18#               requêtes throttlées, identification honnête, lien vers la19#               fiche originale. is_free seulement si event_free=true ;20#               price_label = texte source (jamais inventé).21# -----------------------------------------------------------------------------22from __future__ import annotations2324import os25import time26from datetime import datetime, timezone27from ..normalize import clean_text28from ..schema import Event29from .base import BaseConnector3031BASE = "https://www.quebec-cite.com"32ALGOLIA_URL = ("https://WOZPDCK6IL-dsn.algolia.net/1/indexes/"33               "default_fr_event_date_start_asc/query")34ALGOLIA_HEADERS = {35    "X-Algolia-Application-Id": "WOZPDCK6IL",36    "X-Algolia-API-Key": "c1c4abd63a65bcf4efc646449d5c52ed",37    "Content-Type": "application/json",38}39QBC_PAGE_MAX = int(os.environ.get("QBC_PAGE_MAX", "5"))404142def _txt(v) -> str:43    """Champ Algolia texte OU liste de textes → chaîne nettoyée."""44    if isinstance(v, (list, tuple)):45        return clean_text(", ".join(str(x) for x in v if x))46    return clean_text(str(v)) if v else ""474849def _ts_date(ts) -> str | None:50    """Timestamp Unix → date ISO. La source encode ses dates à 12:00 UTC51    (placeholder « jour entier », vérifié sur tout l'index 2026-08-25) :52    AUCUNE heure réelle n'est publiée → on n'émet jamais d'heure."""53    try:54        ts = int(ts)55        if ts <= 0:56            return None57        return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")58    except (TypeError, ValueError, OSError):59        return None606162class QuebecCiteConnector(BaseConnector):63    source_id = "quebeccite"64    request_delay = 0.86566    def _post_json(self, payload: dict):67        self._throttle()68        resp = self.session.post(ALGOLIA_URL, json=payload,69                                 headers=ALGOLIA_HEADERS, timeout=self.timeout)70        self._last_request = time.time()71        resp.raise_for_status()72        return resp.json()7374    def _hits(self) -> list[dict]:75        since = int(time.time()) - 8640076        hits: list[dict] = []77        for page in range(QBC_PAGE_MAX):78            data = self._post_json({79                "params": f"hitsPerPage=1000&page={page}"80                          f"&numericFilters=event_date_end>={since}"})81            hits.extend(data.get("hits") or [])82            if page + 1 >= int(data.get("nbPages") or 1):83                break84        return hits8586    def fetch(self) -> list[Event]:87        events: list[Event] = []88        seen: set[str] = set()89        for h in self._hits():90            eid = str(h.get("nid") or "")91            title = clean_text(h.get("title") or h.get("index_title") or "")92            if not eid or eid in seen or not title:93                continue94            seen.add(eid)95            start_date = _ts_date(h.get("event_date_start"))96            end_date = _ts_date(h.get("event_date_end"))97            image = h.get("teaser_image") or ""98            if image.startswith("/"):99                image = BASE + image100            cats = [_txt(h.get("event_type")), _txt(h.get("subjects")),101                    _txt(h.get("themes"))]102            events.append(Event(103                source=self.source_id,104                external_id=eid,105                url=BASE + (h.get("url") or ""),106                title=title,107                description=_txt(h.get("summary")),108                raw_categories=[c for c in cats if c] + [title],109                venue=_txt(h.get("event_location")),110                city="",                       # non publié (lieu = salle)111                tourist_region="Région de Québec",112                lat=h.get("lat"), lng=h.get("lng"),113                start_date=start_date,114                end_date=end_date,115                is_free=True if h.get("event_free") is True else None,116                price_label=_txt(h.get("price")),117                website=(h.get("external_link") or "").strip(),118                image=image,119            ))120        return events121