# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/bamboohr.py # Rôle : Classe de plateforme BambooHR (.bamboohr.com/careers) — un # employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER) # Créé : 2026-08-17 Modifié : 2026-08-17 # ============================================================================= """Plateforme BambooHR. API publique : - liste : GET https://.bamboohr.com/careers/list -> {result:[{id, jobOpeningName, departmentLabel, employmentStatusLabel, location:{city,state}, atsLocation:{country,state,city}, isRemote, locationType}]} ⚠ Pour les postes Remote/Hybrid, location.city/state est souvent null — le lieu réel est dans atsLocation : tester les DEUX pour le filtre Québec. - détail : GET https://.bamboohr.com/careers//detail -> {result:{jobOpening:{description(HTML), compensation, datePosted, minimumExperience, jobOpeningShareUrl}}} Page détail avec cache BD (clé = hash de la ligne de liste) + budget. """ from __future__ import annotations import hashlib import json import os from ..schema import JobPosting, clean_html, is_quebec_location from .base import BaseConnector MAX_DETAILS = int(os.environ.get("JOBKA_BHR_DETAIL_LIMIT", "150")) class BambooHRConnector(BaseConnector): """Base BambooHR — sous-classes : définir source_id, EMPLOYER, ORG.""" ats = "bamboohr" request_delay = 0.8 EMPLOYER = "" ORG = "" quebec_only = True @staticmethod def _loc_label(item: dict) -> str: for loc in (item.get("location") or {}, item.get("atsLocation") or {}): city = loc.get("city") or "" state = loc.get("state") or loc.get("province") or "" if city or state: return f"{city}, {state}".strip(", ") return "" def _keep(self, item: dict) -> bool: if not self.quebec_only: return True for loc in (item.get("location") or {}, item.get("atsLocation") or {}): state = (loc.get("state") or loc.get("province") or "") if state.strip().lower() in ("quebec", "québec", "qc"): return True if is_quebec_location(f"{loc.get('city') or ''}, {state}"): return True return False def fetch(self) -> list[JobPosting]: data = self.get(f"https://{self.ORG}.bamboohr.com/careers/list").json() out: list[JobPosting] = [] details_used = 0 for item in data.get("result") or []: if not self._keep(item): continue eid = str(item.get("id") or "") if not eid: continue # locationType (sur chaque ligne) : « 0 » présentiel, « 1 » # hybride, « 2 » télétravail — plus fiable que le seul isRemote work_mode = {"0": "presentiel", "1": "hybride", "2": "teletravail"}.get(str(item.get("locationType") or "")) if work_mode is None and item.get("isRemote"): work_mode = "teletravail" job = JobPosting( source=self.source_id, external_id=eid, url=f"https://{self.ORG}.bamboohr.com/careers/{eid}", employer=self.EMPLOYER, title=item.get("jobOpeningName") or "", location_label=self._loc_label(item), work_mode=work_mode, ats=self.ats, ) job.details["employment_label"] = item.get("employmentStatusLabel") or "" if item.get("departmentLabel"): job.details["team"] = item["departmentLabel"] key = hashlib.sha1(json.dumps(item, sort_keys=True, default=str).encode()).hexdigest()[:12] if details_used < MAX_DETAILS: fresh = [False] def _fn(e=eid, fresh=fresh): fresh[0] = True d = self.get(f"https://{self.ORG}.bamboohr.com/careers/" f"{e}/detail").json() jo = ((d.get("result") or {}).get("jobOpening") or {}) return { "description_html": jo.get("description") or "", "compensation": jo.get("compensation") or "", "date_posted": jo.get("datePosted") or "", "experience": jo.get("minimumExperience") or "", } d = self.detail(eid, key, _fn) if fresh[0]: details_used += 1 job.description = clean_html(d.get("description_html", "")) job.salary_label = d.get("compensation", "") job.date_posted = d.get("date_posted") or None if d.get("experience"): job.requirements["experience"] = d["experience"] out.append(job) return out