# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/rimouski.py : connecteur Ville de Rimouski — événements officiels # Source : https://rimouski.ca/loisirs-culture/activites/evenements # Extraction: flux RSS ÉTENDU du calendrier (découvert sur la page) : # /rss/loisirs-culture/activites/evenements/ — ~240 items avec # balises maison (ISO), , # (« 13 h 30 »), , , # (« Salle (adresse civique) »), , . # 1 seule requête. Vérifié 2026-08-25. # Accès : flux RSS public du site municipal, robots.txt permissif # (sitemap seulement). GET direct, identification honnête. # Prix : non publié par le flux → jamais inventé. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..normalize import clean_text, parse_date_iso, parse_time from ..schema import Event from .base import BaseConnector FEED = "https://rimouski.ca/rss/loisirs-culture/activites/evenements/" _ITEM_RE = re.compile(r"(.*?)", re.S) # « Bibliothèque Lisette-Morin (110, rue de l'Évêché Est) » → salle + adresse _LOC_RE = re.compile(r"^(.*?)\s*(?:\(([^)]+)\))?\s*$", re.S) def _tag(block: str, name: str) -> str: m = re.search(rf"<{name}>(.*?)", block, re.S) return _html.unescape(m.group(1)).strip() if m else "" class RimouskiConnector(BaseConnector): source_id = "rimouski" def _parse_feed(self, xml: str) -> list[dict]: rows = [] for block in _ITEM_RE.findall(xml): link = _tag(block, "link") title = clean_text(_tag(block, "title")) start = parse_date_iso(_tag(block, "startDate")) if not link or not title or not start: continue venue = address = "" m = _LOC_RE.match(_tag(block, "location")) if m: venue = clean_text(m.group(1)) address = clean_text(m.group(2) or "") rows.append({ "link": link, "title": title, "start": start, "end": parse_date_iso(_tag(block, "endDate")), "time": parse_time(_tag(block, "eventTime")), "description": clean_text(_tag(block, "description")), "category": clean_text(_tag(block, "category")), "organizer": clean_text(_tag(block, "organizer")), "venue": venue, "address": address, "image": _tag(block, "image"), }) return rows def fetch(self) -> list[Event]: rows = self._parse_feed(self.get(FEED).text) events: list[Event] = [] seen: set[str] = set() for r in rows: slug = r["link"].rstrip("/").rsplit("/", 1)[-1] ext = f"{slug}-{r['start']}" if ext in seen: continue seen.add(ext) events.append(Event( source=self.source_id, external_id=ext, url=r["link"], title=r["title"], description=r["description"], raw_categories=[r["category"]] if r["category"] else [r["title"]], venue=r["venue"], address=r["address"], city="Rimouski", region="Bas-Saint-Laurent", start_date=r["start"], start_time=r["time"], end_date=r["end"] or r["start"], organizer=r["organizer"], image=r["image"], )) return events