# =============================================================================
# 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