# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/victoriaville.py : connecteur Ville de Victoriaville — « Quoi # faire à Victo » (calendrier municipal officiel). # Source : https://victoriaville.ca/calendriers-cours-et-ateliers/ # quoi-faire-a-victo (Craft CMS, rendu serveur). # Extraction: la page liste UNE SEMAINE de séances, jour par jour # (sections id="day-YYYY-MM-DD", ~115
par semaine : # titre, lieu, heures « De 9 h à 17 h », image CloudFront, # lien fiche ?date=YYYY-MM-DD). Navigation par # ?dateDebut=YYYY-MM-DD → on couvre VIC_WEEKS semaines # (défaut 4) = 1 GET/semaine. Vérifié 2026-08-25. # Accès : page municipale publique, robots.txt permissif (Craft : # /cpresources/ et /vendor/ seulement). GET directs throttlés, # identification honnête. # Prix : non publié dans la liste → jamais inventé. # ----------------------------------------------------------------------------- from __future__ import annotations import os import re from datetime import date, timedelta from ..normalize import clean_text, parse_time from ..schema import Event from .base import BaseConnector LIST_URL = ("https://victoriaville.ca/calendriers-cours-et-ateliers/" "quoi-faire-a-victo") WEEKS = int(os.environ.get("VIC_WEEKS", "4")) _DAY_RE = re.compile(r'id="day-(\d{4}-\d{2}-\d{2})"') _ARTICLE_RE = re.compile( r'
(.*?)
', re.S) _LINK_RE = re.compile( r'href="(https://victoriaville\.ca/[^"?]+)\?date=(\d{4}-\d{2}-\d{2})"') _TITLE_RE = re.compile( r'thumbnails-listing-item-title[^>]*>\s*(.*?)\s*', re.S) _LIEU_RE = re.compile( r'thumbnails-listing-item-lieu[^>]*>\s*(.*?)\s*

', re.S) _HOURS_RE = re.compile( r'thumbnails-listing-item-hours[^>]*>(.*?)

', re.S) _IMG_RE = re.compile(r']+src="([^"]+)"') # « De 9 h à 17 h », « 19 h 30 » — début puis fin éventuelle _RANGE_RE = re.compile( r"(\d{1,2}\s*h(?:\s*\d{2})?)\s*(?:à|-|–)\s*(\d{1,2}\s*h(?:\s*\d{2})?)", re.I) class VictoriavilleConnector(BaseConnector): source_id = "victoriaville" request_delay = 1.0 def _parse_week(self, html: str) -> list[dict]: """Découpe la page en sections-jour puis en cartes-séance.""" rows: list[dict] = [] marks = [(m.start(), m.group(1)) for m in _DAY_RE.finditer(html)] for i, (pos, day) in enumerate(marks): end = marks[i + 1][0] if i + 1 < len(marks) else len(html) for block in _ARTICLE_RE.findall(html[pos:end]): link = _LINK_RE.search(block) title = _TITLE_RE.search(block) if not link or not title: continue hours_txt = "" m = _HOURS_RE.search(block) if m: hours_txt = clean_text(re.sub(r"<[^>]+>", " ", m.group(1))) start_time = end_time = None rng = _RANGE_RE.search(hours_txt) if rng: start_time = parse_time(rng.group(1)) end_time = parse_time(rng.group(2)) else: start_time = parse_time(hours_txt) # « 00h00 » = placeholder « toute la journée » (vérifié live # 2026-08-25 : 28 cartes/semaine) → heure jamais inventée if start_time == "00:00": start_time = None if end_time == "00:00": end_time = None lieu = _LIEU_RE.search(block) img = _IMG_RE.search(block) rows.append({ "url": link.group(1), "date": day, "title": clean_text(re.sub(r"<[^>]+>", "", title.group(1))), "venue": clean_text(re.sub( r"<[^>]+>", "", lieu.group(1))) if lieu else "", "start_time": start_time, "end_time": end_time, "image": img.group(1) if img else "", }) return rows def fetch(self) -> list[Event]: rows: list[dict] = [] today = date.today() for week in range(WEEKS): url = LIST_URL params = None if week: params = {"dateDebut": (today + timedelta(days=7 * week)) .isoformat(), "recherche": "", "programmationSpeciale": ""} try: rows += self._parse_week(self.get(url, params=params).text) except Exception as exc: # une semaine cassée ≠ source cassée if week == 0: raise print(f"[sorti-ka] victoriaville : semaine +{week} ignorée : {exc}") events: list[Event] = [] seen: set[str] = set() for r in rows: slug = r["url"].rstrip("/").rsplit("/", 1)[-1] ext = f"{slug}-{r['date']}" if ext in seen or not r["title"]: continue seen.add(ext) events.append(Event( source=self.source_id, external_id=ext, url=f"{r['url']}?date={r['date']}", title=r["title"], raw_categories=[r["title"]], venue=r["venue"], city="Victoriaville", region="Centre-du-Québec", start_date=r["date"], start_time=r["start_time"], end_date=r["date"], end_time=r["end_time"], organizer="Ville de Victoriaville", image=r["image"], )) return events