spb/forma-ka Public
Python 65.1%
TypeScript 17.9%
CSS 16.4%
HTML 0.5%
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