SPB Git

spb/forma-ka Public

Python 65.1% TypeScript 17.9% CSS 16.4% HTML 0.5%
5.1 KB · 125 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Forma-Ka — Agrégateur de formations (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/crha.py : connecteur CRHA — Ordre des conseillers en ressources5#   humaines agréés (ordrecrha.org) — développement professionnel / Espace6#   Formation (~175 formations et événements RH).7#   Le catalogue public est servi par carrefourrh.org (portail de contenu de8#   l'Ordre) via une API JSON interne propre :9#   - /Formation/GetLaraFormations : toutes les formations (nom, description10#     courte, mots-clés, heures de formation continue par compétence, format,11#     prochaine date, lien vers la fiche Espace Formation, code d'événement)12#   - /Formation/GetFormats et /Formation/GetCategories : référentiels13#     (id chiffré -> libellé) des formats et des domaines de compétence.14#   Les fiches détail (formation.ordrecrha.org, LMS « Lära ») sont une SPA15#   fermée qui ne rend rien de plus publiquement — la liste JSON suffit.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import html as _html2021from ..schema import Formation, clean_text22from .base import BaseConnector232425def _txt(raw: str | None) -> str:26    """Nettoie un champ texte de l'API (entités HTML «   » comprises)."""27    return clean_text(_html.unescape(raw or ""))2829BASE = "https://carrefourrh.org"30LIST_URL = f"{BASE}/Formation/GetLaraFormations"31FORMATS_URL = f"{BASE}/Formation/GetFormats"32CATEGORIES_URL = f"{BASE}/Formation/GetCategories"3334# libellé de format Lära -> (type de formation, mode) canoniques Forma-Ka35_FORMAT_MAP = {36    "événements": ("Conférence", ""),37    "formation en classe virtuelle": ("Formation continue", "en ligne"),38    "formation en ligne": ("Cours en ligne", "asynchrone"),39    "formation en ligne - gratuite": ("Cours en ligne", "asynchrone"),40    "formation en salle": ("Formation continue", "présentiel"),41    "groupe de discussion ou de codéveloppement": ("Atelier", ""),42}434445class CrhaConnector(BaseConnector):46    source_id = "crha"47    request_delay = 0.54849    def fetch(self) -> list[Formation]:50        # 1) référentiels : formats et domaines de compétence (id -> nom)51        formats = {o["Id"]: o["Name"] for o in self.get(FORMATS_URL).json()}52        categories = {o["Id"]: o["Name"] for o in self.get(CATEGORIES_URL).json()}5354        # 2) catalogue complet55        rows = self.get(LIST_URL).json()5657        out: list[Formation] = []58        for row in rows:59            if not isinstance(row, dict) or not row.get("Name"):60                continue61            code = (row.get("CustomFields") or {}).get("CodeEvent", "")62            external_id = code or row.get("Id", "")63            url = row.get("CatalogLink") or row.get("ExternalLink") or ""64            if not external_id or not url:65                continue6667            f = Formation(68                source=self.source_id,69                external_id=str(external_id),70                url=url,71                title=_txt(row["Name"]),72                language="fr",73                code=code,74            )7576            # format -> type + mode ; les « en ligne » autoportants sont77            # asynchrones, la classe virtuelle est synchrone à distance78            fmt_name = formats.get(row.get("Type", ""), "")79            ttype, mode = _FORMAT_MAP.get(fmt_name.lower(),80                                          ("Formation continue", ""))81            f.training_type = ttype82            f.mode = mode83            if fmt_name:84                f.details["format"] = fmt_name85            if fmt_name.lower() == "formation en ligne - gratuite":86                f.price = 0.087                f.is_free = True88                f.price_label = "Gratuit"8990            # description : courte (la fiche LMS n'expose rien de plus)91            f.description = _txt(row.get("Description", "")) \92                or _txt(row.get("ShortDescription", ""))9394            # heures de formation continue par domaine de compétence95            hours = 0.096            names = []97            for cat in row.get("Categories") or []:98                name = categories.get(cat.get("Id", ""), "")99                if name:100                    names.append(name)101                hours += float(cat.get("Credits") or 0)102            if names:103                f.category = names[0]104                f.tags.extend(names)105            if hours > 0:106                f.duration_hours = hours107                f.duration = f"{hours:g} h"108                f.credits = (f"{hours:g} h de formation continue"109                             .replace(".", ","))110                f.credential = "Heures de formation continue CRHA"111112            # prochaine occurrence : « 12 AOÛT 2026 » (parse via finalize)113            occ = _txt(row.get("OccurenceDate", ""))114            if occ:115                f.schedule_label = occ116117            # mots-clés éditoriaux118            keywords = [_txt(k) for k in119                        (row.get("Keywords") or "").split(",")]120            f.tags.extend(k for k in keywords if k)121            f.tags.append("Ressources humaines")122123            out.append(f)124        return out125