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/smartrecruiters.py6# Rôle : Classe de plateforme SmartRecruiters (api.smartrecruiters.com) —7# un employeur = une sous-classe de ~8 lignes (COMPANY, EMPLOYER)8# Créé : 2026-08-17 Modifié : 2026-08-179# =============================================================================10"""Plateforme SmartRecruiters.1112API publique :13- liste : GET https://api.smartrecruiters.com/v1/companies/<company>/postings14 ?limit=100&offset=N -> {totalFound, content:[{id, name, releasedDate,15 location:{city, region, country, remote, latitude, longitude},16 typeOfEmployment:{label}, experienceLevel, ref}]}17- détail : GET .../postings/<id> -> {jobAd:{sections:{jobDescription,18 qualifications, additionalInformation:{title, text(HTML)}}}}1920Le lieu inclut lat/lng : on les passe directement (validés par finalize()).21Page détail avec cache BD (clé = releasedDate) + budget de requêtes.22"""23from __future__ import annotations2425import os2627from ..schema import JobPosting, clean_html28from .base import BaseConnector2930PAGE_SIZE = 10031MAX_DETAILS = int(os.environ.get("JOBKA_SR_DETAIL_LIMIT", "300"))323334class SmartRecruitersConnector(BaseConnector):35 """Base SmartRecruiters — sous-classes : source_id, EMPLOYER, COMPANY."""3637 ats = "smartrecruiters"38 request_delay = 0.73940 EMPLOYER = ""41 COMPANY = ""42 max_pages = 2043 quebec_only = True4445 @property46 def _base(self) -> str:47 return f"https://api.smartrecruiters.com/v1/companies/{self.COMPANY}/postings"4849 def _keep(self, item: dict) -> bool:50 if not self.quebec_only:51 return True52 loc = item.get("location") or {}53 if (loc.get("country") or "").lower() != "ca":54 return False55 from ..schema import is_quebec_location56 return is_quebec_location(57 f"{loc.get('city') or ''}, {loc.get('region') or ''}")5859 def _fetch_detail(self, posting_id: str) -> dict:60 data = self.get(f"{self._base}/{posting_id}").json()61 sections = ((data.get("jobAd") or {}).get("sections") or {})62 parts = []63 for name in ("companyDescription", "jobDescription", "qualifications",64 "additionalInformation"):65 sec = sections.get(name) or {}66 if sec.get("text"):67 parts.append(f"{sec.get('title') or ''}\n{sec['text']}")68 return {"description_html": "\n\n".join(parts)}6970 def fetch(self) -> list[JobPosting]:71 out: list[JobPosting] = []72 details_used = 073 offset = 074 for _ in range(self.max_pages):75 data = self.get(self._base, params={"limit": PAGE_SIZE,76 "offset": offset}).json()77 items = data.get("content") or []78 if not items:79 break80 for item in items:81 if not self._keep(item):82 continue83 loc = item.get("location") or {}84 eid = str(item.get("id") or "")85 if not eid:86 continue87 try:88 lat = float(loc.get("latitude"))89 lng = float(loc.get("longitude"))90 except (TypeError, ValueError):91 lat = lng = None92 job = JobPosting(93 source=self.source_id, external_id=eid,94 url=f"https://jobs.smartrecruiters.com/{self.COMPANY}/{eid}",95 employer=self.EMPLOYER or (item.get("company") or {}).get("name", ""),96 title=item.get("name") or "",97 city=(loc.get("city") or ""),98 location_label=loc.get("fullLocation")99 or f"{loc.get('city') or ''}, {loc.get('region') or ''}",100 work_mode="teletravail" if loc.get("remote")101 else ("hybride" if loc.get("hybrid") else None),102 date_posted=item.get("releasedDate"),103 lat=lat, lng=lng,104 ats=self.ats,105 )106 job.details["employment_label"] = \107 (item.get("typeOfEmployment") or {}).get("label") or ""108 # champs structurés présents sur CHAQUE ligne de liste109 # (zéro requête supplémentaire) — enrichissement 2026-08-19110 job.language = (item.get("language") or {}).get("code") or ""111 exp = (item.get("experienceLevel") or {}).get("label") or ""112 if exp:113 job.requirements["experience"] = exp114 for src_key, dst_key in (("function", "function"),115 ("industry", "industry")):116 label = (item.get(src_key) or {}).get("label") or ""117 if label:118 job.details[dst_key] = label119 dept = (item.get("department") or {}).get("label") or ""120 if dept:121 job.details.setdefault("team", dept)122 if item.get("refNumber"):123 job.details["requisition"] = str(item["refNumber"])124 key = str(item.get("releasedDate") or "")[:19]125 if details_used < MAX_DETAILS:126 fresh = [False]127128 def _fn(pid=eid, fresh=fresh):129 fresh[0] = True130 return self._fetch_detail(pid)131132 d = self.detail(eid, key, _fn)133 if fresh[0]:134 details_used += 1135 else:136 # budget épuisé : détail périmé plutôt que fiche vidée137 d = self.stale_detail(eid)138 job.description = clean_html(d.get("description_html", ""))139 out.append(job)140 offset += PAGE_SIZE141 if offset >= int(data.get("totalFound") or 0):142 break143 return out144