HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
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