SPB Git forge

spb/job-ka

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