# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/lever.py # Rôle : Classe de plateforme Lever (api.lever.co/v0/postings) — un # employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER) # Créé : 2026-08-17 Modifié : 2026-08-17 # ============================================================================= """Plateforme Lever. API publique : GET https://api.lever.co/v0/postings/?mode=json -> liste de {id, text, hostedUrl, createdAt(epoch ms), country, workplaceType(remote|hybrid|on-site|unspecified), descriptionBodyPlain, categories:{location, allLocations, commitment, team, department}, salaryRange:{min, max, currency, interval} quand publié}. Une seule requête liste TOUT (descriptions incluses) : pas de page détail. """ from __future__ import annotations from ..schema import JobPosting, is_quebec_location from .base import BaseConnector _WORKPLACE = {"remote": "teletravail", "hybrid": "hybride", "on-site": "presentiel", "onsite": "presentiel"} _INTERVAL = {"per-hour-salary": "hour", "per-year-salary": "year", "per-month-salary": "month", "per-week-salary": "week", "hourly": "hour", "yearly": "year", "annual": "year", "monthly": "month"} class LeverConnector(BaseConnector): """Base Lever — sous-classes : définir source_id, EMPLOYER, ORG.""" ats = "lever" request_delay = 1.0 EMPLOYER = "" ORG = "" quebec_only = True def _keep(self, item: dict) -> bool: if not self.quebec_only: return True cats = item.get("categories") or {} locs = cats.get("allLocations") or [cats.get("location") or ""] return any(is_quebec_location(l or "") for l in locs) def fetch(self) -> list[JobPosting]: items = self.get(f"https://api.lever.co/v0/postings/{self.ORG}", params={"mode": "json"}).json() out: list[JobPosting] = [] for item in items: if not self._keep(item): continue cats = item.get("categories") or {} sal = item.get("salaryRange") or {} desc = (item.get("descriptionBodyPlain") or item.get("descriptionPlain") or "") # bloc d'introduction (openingPlain) : AVANT le corps opening = item.get("openingPlain") or "" if opening and opening.strip() not in desc: desc = f"{opening.strip()}\n\n{desc}" # les puces (exigences, avantages) sont dans lists[] for lst in item.get("lists") or []: titre = lst.get("text") or "" contenu = lst.get("content") or "" if contenu: from ..normalize import clean_html desc += f"\n\n{titre}\n{clean_html(contenu)}" # bloc de conclusion (additionalPlain) : APRÈS les puces additional = item.get("additionalPlain") or "" if additional and additional.strip() not in desc: desc += f"\n\n{additional.strip()}" job = JobPosting( source=self.source_id, external_id=str(item.get("id") or ""), url=item.get("hostedUrl") or "", employer=self.EMPLOYER, title=item.get("text") or "", description=desc, location_label=cats.get("location") or "", work_mode=_WORKPLACE.get(item.get("workplaceType") or ""), date_posted=item.get("createdAt"), salary_min=sal.get("min"), salary_max=sal.get("max"), salary_unit=_INTERVAL.get((sal.get("interval") or "").lower()), apply_url=item.get("applyUrl") or "", ats=self.ats, ) job.details["employment_label"] = cats.get("commitment") or "" if cats.get("team"): job.details["team"] = cats["team"] all_locs = [l for l in (cats.get("allLocations") or []) if l] if len(all_locs) > 1: job.details["all_locations"] = all_locs out.append(job) return out