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.9 KB · 137 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/victoriaville.py : connecteur Ville de Victoriaville — « Quoi5#   faire à Victo » (calendrier municipal officiel).6#   Source    : https://victoriaville.ca/calendriers-cours-et-ateliers/7#               quoi-faire-a-victo (Craft CMS, rendu serveur).8#   Extraction: la page liste UNE SEMAINE de séances, jour par jour9#               (sections id="day-YYYY-MM-DD", ~115 <article> par semaine :10#               titre, lieu, heures « De 9 h à 17 h », image CloudFront,11#               lien fiche ?date=YYYY-MM-DD). Navigation par12#               ?dateDebut=YYYY-MM-DD → on couvre VIC_WEEKS semaines13#               (défaut 4) = 1 GET/semaine. Vérifié 2026-08-25.14#   Accès     : page municipale publique, robots.txt permissif (Craft :15#               /cpresources/ et /vendor/ seulement). GET directs throttlés,16#               identification honnête.17#   Prix      : non publié dans la liste → jamais inventé.18# -----------------------------------------------------------------------------19from __future__ import annotations2021import os22import re23from datetime import date, timedelta2425from ..normalize import clean_text, parse_time26from ..schema import Event27from .base import BaseConnector2829LIST_URL = ("https://victoriaville.ca/calendriers-cours-et-ateliers/"30            "quoi-faire-a-victo")31WEEKS = int(os.environ.get("VIC_WEEKS", "4"))3233_DAY_RE = re.compile(r'id="day-(\d{4}-\d{2}-\d{2})"')34_ARTICLE_RE = re.compile(35    r'<article class="thumbnails-listing-item">(.*?)</article>', re.S)36_LINK_RE = re.compile(37    r'href="(https://victoriaville\.ca/[^"?]+)\?date=(\d{4}-\d{2}-\d{2})"')38_TITLE_RE = re.compile(39    r'thumbnails-listing-item-title[^>]*>\s*(.*?)\s*</h3>', re.S)40_LIEU_RE = re.compile(41    r'thumbnails-listing-item-lieu[^>]*>\s*(.*?)\s*</p>', re.S)42_HOURS_RE = re.compile(43    r'thumbnails-listing-item-hours[^>]*>(.*?)</p>', re.S)44_IMG_RE = re.compile(r'<img[^>]+src="([^"]+)"')45# « De 9 h à 17 h », « 19 h 30 » — début puis fin éventuelle46_RANGE_RE = re.compile(47    r"(\d{1,2}\s*h(?:\s*\d{2})?)\s*(?:à|-|–)\s*(\d{1,2}\s*h(?:\s*\d{2})?)", re.I)484950class VictoriavilleConnector(BaseConnector):51    source_id = "victoriaville"52    request_delay = 1.05354    def _parse_week(self, html: str) -> list[dict]:55        """Découpe la page en sections-jour puis en cartes-séance."""56        rows: list[dict] = []57        marks = [(m.start(), m.group(1)) for m in _DAY_RE.finditer(html)]58        for i, (pos, day) in enumerate(marks):59            end = marks[i + 1][0] if i + 1 < len(marks) else len(html)60            for block in _ARTICLE_RE.findall(html[pos:end]):61                link = _LINK_RE.search(block)62                title = _TITLE_RE.search(block)63                if not link or not title:64                    continue65                hours_txt = ""66                m = _HOURS_RE.search(block)67                if m:68                    hours_txt = clean_text(re.sub(r"<[^>]+>", " ", m.group(1)))69                start_time = end_time = None70                rng = _RANGE_RE.search(hours_txt)71                if rng:72                    start_time = parse_time(rng.group(1))73                    end_time = parse_time(rng.group(2))74                else:75                    start_time = parse_time(hours_txt)76                # « 00h00 » = placeholder « toute la journée » (vérifié live77                # 2026-08-25 : 28 cartes/semaine) → heure jamais inventée78                if start_time == "00:00":79                    start_time = None80                if end_time == "00:00":81                    end_time = None82                lieu = _LIEU_RE.search(block)83                img = _IMG_RE.search(block)84                rows.append({85                    "url": link.group(1),86                    "date": day,87                    "title": clean_text(re.sub(r"<[^>]+>", "", title.group(1))),88                    "venue": clean_text(re.sub(89                        r"<[^>]+>", "", lieu.group(1))) if lieu else "",90                    "start_time": start_time,91                    "end_time": end_time,92                    "image": img.group(1) if img else "",93                })94        return rows9596    def fetch(self) -> list[Event]:97        rows: list[dict] = []98        today = date.today()99        for week in range(WEEKS):100            url = LIST_URL101            params = None102            if week:103                params = {"dateDebut": (today + timedelta(days=7 * week))104                          .isoformat(), "recherche": "",105                          "programmationSpeciale": ""}106            try:107                rows += self._parse_week(self.get(url, params=params).text)108            except Exception as exc:    # une semaine cassée ≠ source cassée109                if week == 0:110                    raise111                print(f"[sorti-ka] victoriaville : semaine +{week} ignorée : {exc}")112        events: list[Event] = []113        seen: set[str] = set()114        for r in rows:115            slug = r["url"].rstrip("/").rsplit("/", 1)[-1]116            ext = f"{slug}-{r['date']}"117            if ext in seen or not r["title"]:118                continue119            seen.add(ext)120            events.append(Event(121                source=self.source_id,122                external_id=ext,123                url=f"{r['url']}?date={r['date']}",124                title=r["title"],125                raw_categories=[r["title"]],126                venue=r["venue"],127                city="Victoriaville",128                region="Centre-du-Québec",129                start_date=r["date"],130                start_time=r["start_time"],131                end_date=r["date"],132                end_time=r["end_time"],133                organizer="Ville de Victoriaville",134                image=r["image"],135            ))136        return events137