# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/icims.py # Rôle : Classe de plateforme iCIMS (portail carrières .icims.com) # — liste iframe paginée + JSON-LD des pages détail # Créé : 2026-08-18 Modifié : 2026-08-18 # ============================================================================= """Plateforme iCIMS. - liste : GET https://.icims.com/jobs/search?ss=1&in_iframe=1&pr= (HTML léger, liens /jobs///job) — pagination pr=0,1,2… - détail : GET https://.icims.com/jobs///job?in_iframe=1 -> JSON-LD schema.org/JobPosting complet (description, lieu, dates) Le lieu n'apparaissant pas toujours sur la liste, le filtre Québec est appliqué après lecture du détail (avec cache BD : chaque offre n'est visitée qu'une fois). """ from __future__ import annotations import os import re from ..schema import JobPosting, is_quebec_location from . import _jsonld from .base import BaseConnector MAX_DETAILS = int(os.environ.get("JOBKA_ICIMS_DETAIL_LIMIT", "150")) _LINK_RE = re.compile(r'href="https?://[^"]*?/jobs/(\d+)/([^/"]+)/job[^"]*"') class ICIMSConnector(BaseConnector): """Base iCIMS — sous-classes : définir source_id, EMPLOYER, SUB (sous-domaine complet, ex. « careers-capreit »).""" ats = "icims" request_delay = 1.0 EMPLOYER = "" SUB = "" # .icims.com quebec_only = True max_pages = 15 @property def _base(self) -> str: return f"https://{self.SUB}.icims.com" def _fetch_detail(self, jid: str, slug: str) -> dict: html = self.get(f"{self._base}/jobs/{jid}/{slug}/job", params={"in_iframe": "1"}).text node = _jsonld.extract_jobposting(html) return _jsonld.jobposting_fields(node) if node else {} def fetch(self) -> list[JobPosting]: seen: dict[str, str] = {} for page in range(self.max_pages): html = self.get(f"{self._base}/jobs/search", params={"ss": "1", "in_iframe": "1", "pr": str(page)}).text links = _LINK_RE.findall(html) new = 0 for jid, slug in links: if jid not in seen: seen[jid] = slug new += 1 if not links or new == 0: break out: list[JobPosting] = [] details_used = 0 for jid, slug in seen.items(): if details_used >= MAX_DETAILS: # budget épuisé : ne servir que le cache (même périmé) — # l'offre sera reprise à la prochaine synchronisation fields = self.stale_detail(jid) if not fields: continue else: fresh = [False] def _fn(j=jid, s=slug, fresh=fresh): fresh[0] = True return self._fetch_detail(j, s) # clé « v2| » : re-visite progressive (logo employeur, # industrie, scolarité, compétences du JSON-LD — 2026-08-19) fields = self.detail(jid, f"v2|{jid}", _fn) if fresh[0]: details_used += 1 if not fields: continue place = " ".join(str(fields.get(k) or "") for k in ("city", "region_code", "postal_code")) if self.quebec_only and not ( (fields.get("region_code") or "").upper() == "QC" or is_quebec_location(place)): continue job = JobPosting( source=self.source_id, external_id=jid, url=f"{self._base}/jobs/{jid}/{slug}/job", employer=self.EMPLOYER, title="", ats=self.ats, ) _jsonld.apply_fields(job, fields) if not job.title: job.title = slug.replace("-", " ").strip().capitalize() out.append(job) return out