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/lever.py6# Rôle : Classe de plateforme Lever (api.lever.co/v0/postings) — un7# employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER)8# Créé : 2026-08-17 Modifié : 2026-08-179# =============================================================================10"""Plateforme Lever.1112API publique : GET https://api.lever.co/v0/postings/<org>?mode=json13-> liste de {id, text, hostedUrl, createdAt(epoch ms), country,14 workplaceType(remote|hybrid|on-site|unspecified), descriptionBodyPlain,15 categories:{location, allLocations, commitment, team, department},16 salaryRange:{min, max, currency, interval} quand publié}.1718Une seule requête liste TOUT (descriptions incluses) : pas de page détail.19"""20from __future__ import annotations2122from ..schema import JobPosting, is_quebec_location23from .base import BaseConnector2425_WORKPLACE = {"remote": "teletravail", "hybrid": "hybride",26 "on-site": "presentiel", "onsite": "presentiel"}27_INTERVAL = {"per-hour-salary": "hour", "per-year-salary": "year",28 "per-month-salary": "month", "per-week-salary": "week",29 "hourly": "hour", "yearly": "year", "annual": "year",30 "monthly": "month"}313233class LeverConnector(BaseConnector):34 """Base Lever — sous-classes : définir source_id, EMPLOYER, ORG."""3536 ats = "lever"37 request_delay = 1.03839 EMPLOYER = ""40 ORG = ""41 quebec_only = True4243 def _keep(self, item: dict) -> bool:44 if not self.quebec_only:45 return True46 cats = item.get("categories") or {}47 locs = cats.get("allLocations") or [cats.get("location") or ""]48 return any(is_quebec_location(l or "") for l in locs)4950 def fetch(self) -> list[JobPosting]:51 items = self.get(f"https://api.lever.co/v0/postings/{self.ORG}",52 params={"mode": "json"}).json()53 out: list[JobPosting] = []54 for item in items:55 if not self._keep(item):56 continue57 cats = item.get("categories") or {}58 sal = item.get("salaryRange") or {}59 desc = (item.get("descriptionBodyPlain")60 or item.get("descriptionPlain") or "")61 # bloc d'introduction (openingPlain) : AVANT le corps62 opening = item.get("openingPlain") or ""63 if opening and opening.strip() not in desc:64 desc = f"{opening.strip()}\n\n{desc}"65 # les puces (exigences, avantages) sont dans lists[]66 for lst in item.get("lists") or []:67 titre = lst.get("text") or ""68 contenu = lst.get("content") or ""69 if contenu:70 from ..normalize import clean_html71 desc += f"\n\n{titre}\n{clean_html(contenu)}"72 # bloc de conclusion (additionalPlain) : APRÈS les puces73 additional = item.get("additionalPlain") or ""74 if additional and additional.strip() not in desc:75 desc += f"\n\n{additional.strip()}"76 job = JobPosting(77 source=self.source_id, external_id=str(item.get("id") or ""),78 url=item.get("hostedUrl") or "",79 employer=self.EMPLOYER,80 title=item.get("text") or "",81 description=desc,82 location_label=cats.get("location") or "",83 work_mode=_WORKPLACE.get(item.get("workplaceType") or ""),84 date_posted=item.get("createdAt"),85 salary_min=sal.get("min"), salary_max=sal.get("max"),86 salary_unit=_INTERVAL.get((sal.get("interval") or "").lower()),87 apply_url=item.get("applyUrl") or "",88 ats=self.ats,89 )90 job.details["employment_label"] = cats.get("commitment") or ""91 if cats.get("team"):92 job.details["team"] = cats["team"]93 all_locs = [l for l in (cats.get("allLocations") or []) if l]94 if len(all_locs) > 1:95 job.details["all_locations"] = all_locs96 out.append(job)97 return out98