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/greenhouse.py6# Rôle : Classe de plateforme Greenhouse (boards-api.greenhouse.io) — un7# employeur = une sous-classe de ~8 lignes (BOARD, EMPLOYER)8# Créé : 2026-08-17 Modifié : 2026-08-179# =============================================================================10"""Plateforme Greenhouse.1112API publique : GET https://boards-api.greenhouse.io/v1/boards/<board>/jobs13avec ?content=true -> {jobs:[{id, title, absolute_url, location:{name},14first_published, updated_at, application_deadline, content(HTML échappé),15departments, offices, metadata}]}.1617Une seule requête liste TOUT (contenu inclus) : pas de page détail.18`application_deadline` alimente date_deadline (retrait automatique).19"""20from __future__ import annotations2122from ..schema import JobPosting, clean_html, is_quebec_location23from .base import BaseConnector242526class GreenhouseConnector(BaseConnector):27 """Base Greenhouse — sous-classes : définir source_id, EMPLOYER, BOARD."""2829 ats = "greenhouse"30 request_delay = 1.03132 EMPLOYER = ""33 BOARD = ""34 quebec_only = True3536 def _keep(self, item: dict) -> bool:37 if not self.quebec_only:38 return True39 # strict sur le lieu du POSTE (les bureaux de l'entreprise sont trop40 # larges : un employeur québécois peut afficher un poste aux États-Unis)41 return is_quebec_location((item.get("location") or {}).get("name") or "")4243 def fetch(self) -> list[JobPosting]:44 data = self.get(45 f"https://boards-api.greenhouse.io/v1/boards/{self.BOARD}/jobs",46 params={"content": "true"}).json()47 out: list[JobPosting] = []48 for item in data.get("jobs") or []:49 if not self._keep(item):50 continue51 deadline = item.get("application_deadline") or None52 job = JobPosting(53 source=self.source_id, external_id=str(item.get("id") or ""),54 url=item.get("absolute_url") or "",55 employer=self.EMPLOYER or item.get("company_name") or "",56 title=item.get("title") or "",57 description=clean_html(item.get("content") or ""),58 location_label=(item.get("location") or {}).get("name") or "",59 date_posted=item.get("first_published") or item.get("updated_at"),60 date_deadline=deadline,61 language=(item.get("language") or "")[:2], # « fr » / « en »62 ats=self.ats,63 )64 deps = [d.get("name") for d in item.get("departments") or [] if d.get("name")]65 if deps:66 job.details["team"] = deps[0]67 if item.get("requisition_id"):68 job.details["requisition"] = str(item["requisition_id"])69 out.append(job)70 return out71