# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/ashby.py # Rôle : Classe de plateforme Ashby (api.ashbyhq.com/posting-api) — un # employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER) # Créé : 2026-08-17 Modifié : 2026-08-17 # ============================================================================= """Plateforme Ashby. API publique : GET https://api.ashbyhq.com/posting-api/job-board/ ?includeCompensation=true -> {jobs:[{id, title, location, secondaryLocations, department, team, employmentType(FullTime|PartTime|Intern|Contract|Temporary), isRemote, workplaceType, publishedAt, jobUrl, applyUrl, descriptionHtml, descriptionPlain, compensation:{compensationTierSummary, scrapeableCompensationSalarySummary, ...}}]}. Une seule requête liste TOUT (descriptions + salaires inclus) — l'ATS le plus transparent sur les salaires. """ from __future__ import annotations from ..schema import JobPosting, is_quebec_location from .base import BaseConnector _EMPLOYMENT = {"fulltime": "temps_plein", "parttime": "temps_partiel", "intern": "stage", "contract": "contractuel", "temporary": "contractuel"} class AshbyConnector(BaseConnector): """Base Ashby — sous-classes : définir source_id, EMPLOYER, ORG.""" ats = "ashby" request_delay = 1.0 EMPLOYER = "" ORG = "" quebec_only = True @staticmethod def _locations(item: dict) -> list[str]: locs = [item.get("location") or ""] locs += [s.get("location") or "" for s in item.get("secondaryLocations") or []] addr = ((item.get("address") or {}).get("postalAddress") or {}) if addr.get("addressLocality"): locs.append(f"{addr['addressLocality']}, {addr.get('addressRegion') or ''}") return [l for l in locs if l] def _keep(self, item: dict) -> bool: if not self.quebec_only: return True return any(is_quebec_location(l) for l in self._locations(item)) def fetch(self) -> list[JobPosting]: data = self.get( f"https://api.ashbyhq.com/posting-api/job-board/{self.ORG}", params={"includeCompensation": "true"}).json() out: list[JobPosting] = [] for item in data.get("jobs") or []: if not item.get("isListed", True) or not self._keep(item): continue comp = item.get("compensation") or {} salary_label = (comp.get("scrapeableCompensationSalarySummary") or comp.get("compensationTierSummary") or "") # salaires structurés (summaryComponents) : plus fiables que le libellé s_min = s_max = s_unit = None extra_comp: list[dict] = [] for c in comp.get("summaryComponents") or []: ctype = c.get("compensationType") or "" if ctype == "Salary" and s_min is None: s_min, s_max = c.get("minValue"), c.get("maxValue") interval = (c.get("interval") or "").upper() s_unit = ("hour" if "HOUR" in interval else "month" if "MONTH" in interval else "year" if "YEAR" in interval else None) elif ctype and ctype != "Salary": # composantes Bonus / Equity / Commission (montants si publiés) entry = {"type": ctype} if c.get("minValue") is not None: entry["min"] = c["minValue"] if c.get("maxValue") is not None: entry["max"] = c["maxValue"] if c.get("interval") and (c.get("interval") or "").upper() != "NONE": entry["interval"] = c["interval"] extra_comp.append(entry) workplace = (item.get("workplaceType") or "").lower() work_mode = ("hybride" if workplace == "hybrid" else "teletravail" if item.get("isRemote") else "presentiel" if workplace == "onsite" else None) job = JobPosting( source=self.source_id, external_id=str(item.get("id") or ""), url=item.get("jobUrl") or "", employer=self.EMPLOYER, title=item.get("title") or "", description=item.get("descriptionPlain") or "", location_label=self._locations(item)[0] if self._locations(item) else "", work_mode=work_mode, employment_type=_EMPLOYMENT.get( (item.get("employmentType") or "").lower()), date_posted=item.get("publishedAt"), salary_min=s_min, salary_max=s_max, salary_unit=s_unit if s_min is not None else None, salary_label=salary_label, apply_url=item.get("applyUrl") or "", ats=self.ats, ) if item.get("team"): job.details["team"] = item["team"] if extra_comp: job.details["compensation_components"] = extra_comp out.append(job) return out