# ----------------------------------------------------------------------------- # Forma-Ka — Agrégateur de formations (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/crha.py : connecteur CRHA — Ordre des conseillers en ressources # humaines agréés (ordrecrha.org) — développement professionnel / Espace # Formation (~175 formations et événements RH). # Le catalogue public est servi par carrefourrh.org (portail de contenu de # l'Ordre) via une API JSON interne propre : # - /Formation/GetLaraFormations : toutes les formations (nom, description # courte, mots-clés, heures de formation continue par compétence, format, # prochaine date, lien vers la fiche Espace Formation, code d'événement) # - /Formation/GetFormats et /Formation/GetCategories : référentiels # (id chiffré -> libellé) des formats et des domaines de compétence. # Les fiches détail (formation.ordrecrha.org, LMS « Lära ») sont une SPA # fermée qui ne rend rien de plus publiquement — la liste JSON suffit. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html from ..schema import Formation, clean_text from .base import BaseConnector def _txt(raw: str | None) -> str: """Nettoie un champ texte de l'API (entités HTML «   » comprises).""" return clean_text(_html.unescape(raw or "")) BASE = "https://carrefourrh.org" LIST_URL = f"{BASE}/Formation/GetLaraFormations" FORMATS_URL = f"{BASE}/Formation/GetFormats" CATEGORIES_URL = f"{BASE}/Formation/GetCategories" # libellé de format Lära -> (type de formation, mode) canoniques Forma-Ka _FORMAT_MAP = { "événements": ("Conférence", ""), "formation en classe virtuelle": ("Formation continue", "en ligne"), "formation en ligne": ("Cours en ligne", "asynchrone"), "formation en ligne - gratuite": ("Cours en ligne", "asynchrone"), "formation en salle": ("Formation continue", "présentiel"), "groupe de discussion ou de codéveloppement": ("Atelier", ""), } class CrhaConnector(BaseConnector): source_id = "crha" request_delay = 0.5 def fetch(self) -> list[Formation]: # 1) référentiels : formats et domaines de compétence (id -> nom) formats = {o["Id"]: o["Name"] for o in self.get(FORMATS_URL).json()} categories = {o["Id"]: o["Name"] for o in self.get(CATEGORIES_URL).json()} # 2) catalogue complet rows = self.get(LIST_URL).json() out: list[Formation] = [] for row in rows: if not isinstance(row, dict) or not row.get("Name"): continue code = (row.get("CustomFields") or {}).get("CodeEvent", "") external_id = code or row.get("Id", "") url = row.get("CatalogLink") or row.get("ExternalLink") or "" if not external_id or not url: continue f = Formation( source=self.source_id, external_id=str(external_id), url=url, title=_txt(row["Name"]), language="fr", code=code, ) # format -> type + mode ; les « en ligne » autoportants sont # asynchrones, la classe virtuelle est synchrone à distance fmt_name = formats.get(row.get("Type", ""), "") ttype, mode = _FORMAT_MAP.get(fmt_name.lower(), ("Formation continue", "")) f.training_type = ttype f.mode = mode if fmt_name: f.details["format"] = fmt_name if fmt_name.lower() == "formation en ligne - gratuite": f.price = 0.0 f.is_free = True f.price_label = "Gratuit" # description : courte (la fiche LMS n'expose rien de plus) f.description = _txt(row.get("Description", "")) \ or _txt(row.get("ShortDescription", "")) # heures de formation continue par domaine de compétence hours = 0.0 names = [] for cat in row.get("Categories") or []: name = categories.get(cat.get("Id", ""), "") if name: names.append(name) hours += float(cat.get("Credits") or 0) if names: f.category = names[0] f.tags.extend(names) if hours > 0: f.duration_hours = hours f.duration = f"{hours:g} h" f.credits = (f"{hours:g} h de formation continue" .replace(".", ",")) f.credential = "Heures de formation continue CRHA" # prochaine occurrence : « 12 AOÛT 2026 » (parse via finalize) occ = _txt(row.get("OccurenceDate", "")) if occ: f.schedule_label = occ # mots-clés éditoriaux keywords = [_txt(k) for k in (row.get("Keywords") or "").split(",")] f.tags.extend(k for k in keywords if k) f.tags.append("Ressources humaines") out.append(f) return out