# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/digitalrecruiters.py # Rôle : Classe de plateforme Cegid Digital Recruiters (sites carrières) — # sitemap + JSON-LD des pages annonce (ex. Santé Québec) # Créé : 2026-08-18 Modifié : 2026-08-26 # ============================================================================= """Plateforme Cegid Digital Recruiters. Les sites carrières Digital Recruiters (ex. emplois.sante.quebec) sont rendus côté serveur : - liste : GET //sitemap.xml -> URLs /annonce/- - détail : chaque page annonce embarque un JSON-LD schema.org/JobPosting complet (description, lieu, salaire, dates) — cache BD + budget. """ from __future__ import annotations import os import re import time from ..schema import JobPosting, is_quebec_location from . import _jsonld from .base import BaseConnector MAX_DETAILS = int(os.environ.get("JOBKA_DR_DETAIL_LIMIT", "120")) _AD_RE = re.compile(r"([^<]*/annonce/(\d+)[^<]*)", re.I) class DigitalRecruitersConnector(BaseConnector): """Base Digital Recruiters — sous-classes : définir source_id, EMPLOYER, BASE (ex. https://emplois.sante.quebec) et LOCALE.""" ats = "digitalrecruiters" request_delay = 0.8 EMPLOYER = "" BASE = "" LOCALE = "fr_CA" quebec_only = True def _fetch_detail(self, url: str) -> dict: html = self.get(url).text node = _jsonld.extract_jobposting(html) return _jsonld.jobposting_fields(node) if node else {} def _fetch_sitemap(self) -> str: """Sitemap de la liste, avec garde-fou : une réponse vide ou anti-bot (challenge en 200, escalade en échec) ne doit JAMAIS passer pour un babillard à 0 offre — cause des syncs « 0 trouvé » intermittents de cssdd. Tout sitemap DR légitime, même sans annonce, contient — sinon on retente une fois puis on lève (sync en échec, pas 0).""" url = f"{self.BASE}/{self.LOCALE}/sitemap.xml" for attempt in range(2): xml = self.get(url).text if _AD_RE.search(xml) or re.search(r" list[JobPosting]: xml = self._fetch_sitemap() out: list[JobPosting] = [] details_used = 0 seen: set[str] = set() for url, eid in _AD_RE.findall(xml): if eid in seen: continue seen.add(eid) job = JobPosting(source=self.source_id, external_id=eid, url=url, employer=self.EMPLOYER, title="", ats=self.ats) fields: dict = {} if details_used < MAX_DETAILS: fresh = [False] def _fn(u=url, fresh=fresh): fresh[0] = True return self._fetch_detail(u) fields = self.detail(eid, eid, _fn) if fresh[0]: details_used += 1 if not fields: continue _jsonld.apply_fields(job, fields) if self.quebec_only and job.city and not is_quebec_location( f"{job.city} {fields.get('region_code', '')}"): if (fields.get("region_code") or "").upper() != "QC": continue if not job.title: continue out.append(job) return out