SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
4 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
5.0 KB · 123 lines python
Raw Blame History
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