Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
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