# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/recruitee.py # Rôle : Classe de plateforme Recruitee (.recruitee.com/api/offers) — # un employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER) # Créé : 2026-08-17 Modifié : 2026-08-17 # ============================================================================= """Plateforme Recruitee. API publique : GET https://.recruitee.com/api/offers/ -> {offers:[{id, title, city, state_code, state_name, country_code, postal_code, remote, hybrid, on_site, employment_type_code, description(HTML, dans la liste), requirements(HTML), salary:{min, max, period, currency}, careers_url, published_at ("2026-08-11 19:27:47 UTC"), close_at, company_name}]}. Une seule requête liste TOUT (descriptions + salaires structurés). """ from __future__ import annotations from ..schema import JobPosting, clean_html, is_quebec_location from .base import BaseConnector _EMPLOYMENT = { "fulltime": "temps_plein", "fulltime_permanent": "temps_plein", "parttime": "temps_partiel", "parttime_permanent": "temps_partiel", "internship": "stage", "traineeship": "stage", "temporary": "contractuel", "contract": "contractuel", "freelance": "contractuel", "seasonal": "saisonnier", } _PERIOD = {"hour": "hour", "hourly": "hour", "day": "day", "week": "week", "month": "month", "monthly": "month", "year": "year", "yearly": "year", "annual": "year"} class RecruiteeConnector(BaseConnector): """Base Recruitee — sous-classes : définir source_id, EMPLOYER, ORG.""" ats = "recruitee" request_delay = 1.0 EMPLOYER = "" ORG = "" quebec_only = True def _keep(self, item: dict) -> bool: if not self.quebec_only: return True if (item.get("country_code") or "").upper() not in ("CA", ""): return False if (item.get("state_code") or "").upper() == "QC": return True return is_quebec_location( f"{item.get('city') or ''}, {item.get('state_name') or ''}") def fetch(self) -> list[JobPosting]: data = self.get(f"https://{self.ORG}.recruitee.com/api/offers/").json() out: list[JobPosting] = [] for item in data.get("offers") or []: if (item.get("status") or "published") != "published": continue if not self._keep(item): continue # traductions : quand une version FRANÇAISE existe, c'est elle # qu'on affiche (public québécois) — langue fr/bilingue déduite translations = item.get("translations") or {} langs = sorted(k for k, v in translations.items() if isinstance(v, dict) and (v.get("title") or v.get("description"))) fr = translations.get("fr") if isinstance( translations.get("fr"), dict) else None title = item.get("title") or "" desc_html = item.get("description") or "" reqs_html = item.get("requirements") or "" if fr: title = fr.get("title") or title desc_html = fr.get("description") or desc_html reqs_html = fr.get("requirements") or reqs_html language = "" if "fr" in langs: language = "bilingue" if "en" in langs else "fr" elif langs == ["en"]: language = "en" desc = clean_html(desc_html) reqs = clean_html(reqs_html) if reqs: desc = f"{desc}\n\nExigences\n{reqs}" if desc else reqs sal = item.get("salary") or {} def _num(v): try: return float(v) if v not in (None, "") else None except (TypeError, ValueError): return None work_mode = ("teletravail" if item.get("remote") else "hybride" if item.get("hybrid") else "presentiel" if item.get("on_site") else None) job = JobPosting( source=self.source_id, external_id=str(item.get("id") or ""), url=item.get("careers_url") or "", employer=self.EMPLOYER or item.get("company_name") or "", title=title, description=desc, city=item.get("city") or "", postal_code=item.get("postal_code") or "", location_label=f"{item.get('city') or ''}, " f"{item.get('state_name') or ''}".strip(", "), work_mode=work_mode, employment_type=_EMPLOYMENT.get( (item.get("employment_type_code") or "").lower()), salary_min=_num(sal.get("min")), salary_max=_num(sal.get("max")), salary_unit=_PERIOD.get((sal.get("period") or "").lower()), date_posted=item.get("published_at") or item.get("created_at"), date_deadline=item.get("close_at"), language=language, apply_url=item.get("careers_apply_url") or "", ats=self.ats, ) if item.get("department"): job.details["team"] = item["department"] # niveau d'expérience / scolarité structurés (codes Recruitee) if item.get("experience_code"): job.requirements.setdefault( "experience", str(item["experience_code"]).replace("_", " ")) if item.get("education_code"): job.requirements.setdefault( "education", str(item["education_code"]).replace("_", " ")) hours_min = item.get("min_hours_per_week") or item.get("min_hours") hours_max = item.get("max_hours_per_week") or item.get("max_hours") if hours_min or hours_max: job.details["hours_per_week"] = ( f"{hours_min or hours_max}" if not (hours_min and hours_max and hours_min != hours_max) else f"{hours_min}-{hours_max}") out.append(job) return out