SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
4 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
4.1 KB · 112 lines python
Raw Blame History
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur  : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/connectors/icims.py6# Rôle    : Classe de plateforme iCIMS (portail carrières <org>.icims.com)7#           — liste iframe paginée + JSON-LD des pages détail8# Créé    : 2026-08-18   Modifié : 2026-08-189# =============================================================================10"""Plateforme iCIMS.1112- liste  : GET https://<sub>.icims.com/jobs/search?ss=1&in_iframe=1&pr=<page>13           (HTML léger, liens /jobs/<id>/<slug>/job) — pagination pr=0,1,2…14- détail : GET https://<sub>.icims.com/jobs/<id>/<slug>/job?in_iframe=115           -> JSON-LD schema.org/JobPosting complet (description, lieu, dates)1617Le lieu n'apparaissant pas toujours sur la liste, le filtre Québec est18appliqué après lecture du détail (avec cache BD : chaque offre n'est visitée19qu'une fois).20"""21from __future__ import annotations2223import os24import re2526from ..schema import JobPosting, is_quebec_location27from . import _jsonld28from .base import BaseConnector2930MAX_DETAILS = int(os.environ.get("JOBKA_ICIMS_DETAIL_LIMIT", "150"))3132_LINK_RE = re.compile(r'href="https?://[^"]*?/jobs/(\d+)/([^/"]+)/job[^"]*"')333435class ICIMSConnector(BaseConnector):36    """Base iCIMS — sous-classes : définir source_id, EMPLOYER, SUB37    (sous-domaine complet, ex. « careers-capreit »)."""3839    ats = "icims"40    request_delay = 1.04142    EMPLOYER = ""43    SUB = ""               # <SUB>.icims.com44    quebec_only = True45    max_pages = 154647    @property48    def _base(self) -> str:49        return f"https://{self.SUB}.icims.com"5051    def _fetch_detail(self, jid: str, slug: str) -> dict:52        html = self.get(f"{self._base}/jobs/{jid}/{slug}/job",53                        params={"in_iframe": "1"}).text54        node = _jsonld.extract_jobposting(html)55        return _jsonld.jobposting_fields(node) if node else {}5657    def fetch(self) -> list[JobPosting]:58        seen: dict[str, str] = {}59        for page in range(self.max_pages):60            html = self.get(f"{self._base}/jobs/search",61                            params={"ss": "1", "in_iframe": "1",62                                    "pr": str(page)}).text63            links = _LINK_RE.findall(html)64            new = 065            for jid, slug in links:66                if jid not in seen:67                    seen[jid] = slug68                    new += 169            if not links or new == 0:70                break7172        out: list[JobPosting] = []73        details_used = 074        for jid, slug in seen.items():75            if details_used >= MAX_DETAILS:76                # budget épuisé : ne servir que le cache (même périmé) —77                # l'offre sera reprise à la prochaine synchronisation78                fields = self.stale_detail(jid)79                if not fields:80                    continue81            else:82                fresh = [False]8384                def _fn(j=jid, s=slug, fresh=fresh):85                    fresh[0] = True86                    return self._fetch_detail(j, s)8788                # clé « v2| » : re-visite progressive (logo employeur,89                # industrie, scolarité, compétences du JSON-LD — 2026-08-19)90                fields = self.detail(jid, f"v2|{jid}", _fn)91                if fresh[0]:92                    details_used += 193            if not fields:94                continue95            place = " ".join(str(fields.get(k) or "") for k in96                             ("city", "region_code", "postal_code"))97            if self.quebec_only and not (98                    (fields.get("region_code") or "").upper() == "QC"99                    or is_quebec_location(place)):100                continue101            job = JobPosting(102                source=self.source_id, external_id=jid,103                url=f"{self._base}/jobs/{jid}/{slug}/job",104                employer=self.EMPLOYER,105                title="", ats=self.ats,106            )107            _jsonld.apply_fields(job, fields)108            if not job.title:109                job.title = slug.replace("-", " ").strip().capitalize()110            out.append(job)111        return out112