SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
5 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
3.7 KB · 100 lines python
Raw Blame History
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur  : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/connectors/digitalrecruiters.py6# Rôle    : Classe de plateforme Cegid Digital Recruiters (sites carrières) —7#           sitemap + JSON-LD des pages annonce (ex. Santé Québec)8# Créé    : 2026-08-18   Modifié : 2026-08-269# =============================================================================10"""Plateforme Cegid Digital Recruiters.1112Les sites carrières Digital Recruiters (ex. emplois.sante.quebec) sont rendus13côté serveur :14- liste  : GET <BASE>/<locale>/sitemap.xml -> URLs /annonce/<id>-<slug>15- détail : chaque page annonce embarque un JSON-LD schema.org/JobPosting16           complet (description, lieu, salaire, dates) — cache BD + budget.17"""18from __future__ import annotations1920import os21import re22import time2324from ..schema import JobPosting, is_quebec_location25from . import _jsonld26from .base import BaseConnector2728MAX_DETAILS = int(os.environ.get("JOBKA_DR_DETAIL_LIMIT", "120"))2930_AD_RE = re.compile(r"<loc>([^<]*/annonce/(\d+)[^<]*)</loc>", re.I)313233class DigitalRecruitersConnector(BaseConnector):34    """Base Digital Recruiters — sous-classes : définir source_id, EMPLOYER,35    BASE (ex. https://emplois.sante.quebec) et LOCALE."""3637    ats = "digitalrecruiters"38    request_delay = 0.83940    EMPLOYER = ""41    BASE = ""42    LOCALE = "fr_CA"43    quebec_only = True4445    def _fetch_detail(self, url: str) -> dict:46        html = self.get(url).text47        node = _jsonld.extract_jobposting(html)48        return _jsonld.jobposting_fields(node) if node else {}4950    def _fetch_sitemap(self) -> str:51        """Sitemap de la liste, avec garde-fou : une réponse vide ou anti-bot52        (challenge en 200, escalade en échec) ne doit JAMAIS passer pour un53        babillard à 0 offre — cause des syncs « 0 trouvé » intermittents de54        cssdd. Tout sitemap DR légitime, même sans annonce, contient <urlset>55        — sinon on retente une fois puis on lève (sync en échec, pas 0)."""56        url = f"{self.BASE}/{self.LOCALE}/sitemap.xml"57        for attempt in range(2):58            xml = self.get(url).text59            if _AD_RE.search(xml) or re.search(r"<urlset", xml, re.I):60                return xml61            if attempt == 0:62                time.sleep(10)63        raise RuntimeError(64            f"digitalrecruiters {self.source_id}: sitemap illisible "65            f"(vide ou anti-bot, {len(xml)} octets) — sync avorté")6667    def fetch(self) -> list[JobPosting]:68        xml = self._fetch_sitemap()69        out: list[JobPosting] = []70        details_used = 071        seen: set[str] = set()72        for url, eid in _AD_RE.findall(xml):73            if eid in seen:74                continue75            seen.add(eid)76            job = JobPosting(source=self.source_id, external_id=eid, url=url,77                             employer=self.EMPLOYER, title="", ats=self.ats)78            fields: dict = {}79            if details_used < MAX_DETAILS:80                fresh = [False]8182                def _fn(u=url, fresh=fresh):83                    fresh[0] = True84                    return self._fetch_detail(u)8586                fields = self.detail(eid, eid, _fn)87                if fresh[0]:88                    details_used += 189            if not fields:90                continue91            _jsonld.apply_fields(job, fields)92            if self.quebec_only and job.city and not is_quebec_location(93                    f"{job.city} {fields.get('region_code', '')}"):94                if (fields.get("region_code") or "").upper() != "QC":95                    continue96            if not job.title:97                continue98            out.append(job)99        return out100