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/bamboohr.py6# Rôle : Classe de plateforme BambooHR (<org>.bamboohr.com/careers) — un7# employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER)8# Créé : 2026-08-17 Modifié : 2026-08-179# =============================================================================10"""Plateforme BambooHR.1112API publique :13- liste : GET https://<org>.bamboohr.com/careers/list14 -> {result:[{id, jobOpeningName, departmentLabel,15 employmentStatusLabel, location:{city,state},16 atsLocation:{country,state,city}, isRemote, locationType}]}17 ⚠ Pour les postes Remote/Hybrid, location.city/state est souvent null —18 le lieu réel est dans atsLocation : tester les DEUX pour le filtre Québec.19- détail : GET https://<org>.bamboohr.com/careers/<id>/detail20 -> {result:{jobOpening:{description(HTML), compensation,21 datePosted, minimumExperience, jobOpeningShareUrl}}}2223Page détail avec cache BD (clé = hash de la ligne de liste) + budget.24"""25from __future__ import annotations2627import hashlib28import json29import os3031from ..schema import JobPosting, clean_html, is_quebec_location32from .base import BaseConnector3334MAX_DETAILS = int(os.environ.get("JOBKA_BHR_DETAIL_LIMIT", "150"))353637class BambooHRConnector(BaseConnector):38 """Base BambooHR — sous-classes : définir source_id, EMPLOYER, ORG."""3940 ats = "bamboohr"41 request_delay = 0.84243 EMPLOYER = ""44 ORG = ""45 quebec_only = True4647 @staticmethod48 def _loc_label(item: dict) -> str:49 for loc in (item.get("location") or {}, item.get("atsLocation") or {}):50 city = loc.get("city") or ""51 state = loc.get("state") or loc.get("province") or ""52 if city or state:53 return f"{city}, {state}".strip(", ")54 return ""5556 def _keep(self, item: dict) -> bool:57 if not self.quebec_only:58 return True59 for loc in (item.get("location") or {}, item.get("atsLocation") or {}):60 state = (loc.get("state") or loc.get("province") or "")61 if state.strip().lower() in ("quebec", "québec", "qc"):62 return True63 if is_quebec_location(f"{loc.get('city') or ''}, {state}"):64 return True65 return False6667 def fetch(self) -> list[JobPosting]:68 data = self.get(f"https://{self.ORG}.bamboohr.com/careers/list").json()69 out: list[JobPosting] = []70 details_used = 071 for item in data.get("result") or []:72 if not self._keep(item):73 continue74 eid = str(item.get("id") or "")75 if not eid:76 continue77 # locationType (sur chaque ligne) : « 0 » présentiel, « 1 »78 # hybride, « 2 » télétravail — plus fiable que le seul isRemote79 work_mode = {"0": "presentiel", "1": "hybride",80 "2": "teletravail"}.get(str(item.get("locationType") or ""))81 if work_mode is None and item.get("isRemote"):82 work_mode = "teletravail"83 job = JobPosting(84 source=self.source_id, external_id=eid,85 url=f"https://{self.ORG}.bamboohr.com/careers/{eid}",86 employer=self.EMPLOYER,87 title=item.get("jobOpeningName") or "",88 location_label=self._loc_label(item),89 work_mode=work_mode,90 ats=self.ats,91 )92 job.details["employment_label"] = item.get("employmentStatusLabel") or ""93 if item.get("departmentLabel"):94 job.details["team"] = item["departmentLabel"]9596 key = hashlib.sha1(json.dumps(item, sort_keys=True,97 default=str).encode()).hexdigest()[:12]98 if details_used < MAX_DETAILS:99 fresh = [False]100101 def _fn(e=eid, fresh=fresh):102 fresh[0] = True103 d = self.get(f"https://{self.ORG}.bamboohr.com/careers/"104 f"{e}/detail").json()105 jo = ((d.get("result") or {}).get("jobOpening") or {})106 return {107 "description_html": jo.get("description") or "",108 "compensation": jo.get("compensation") or "",109 "date_posted": jo.get("datePosted") or "",110 "experience": jo.get("minimumExperience") or "",111 }112113 d = self.detail(eid, key, _fn)114 if fresh[0]:115 details_used += 1116 job.description = clean_html(d.get("description_html", ""))117 job.salary_label = d.get("compensation", "")118 job.date_posted = d.get("date_posted") or None119 if d.get("experience"):120 job.requirements["experience"] = d["experience"]121 out.append(job)122 return out123