# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/breezy.py # Rôle : Classe de plateforme Breezy HR (.breezy.hr/json) — un # employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER) # Créé : 2026-08-17 Modifié : 2026-08-27 # ============================================================================= """Plateforme Breezy HR. API publique : GET https://.breezy.hr/json?verbose=true -> liste de {id, friendly_id, name(titre), url(lien direct), type:{id:"fullTime", name:"Temps plein"}, department, location:{country:{id}, state:{id:"QC", name:"Quebec"}, city}, published_date(ISO), salary, description(HTML — seulement avec verbose)}. Le filtre le plus fiable est location.state.id == "QC". Une seule requête. """ from __future__ import annotations from ..schema import JobPosting, clean_html, is_quebec_location from .base import BaseConnector _TYPE = {"fulltime": "temps_plein", "parttime": "temps_partiel", "contract": "contractuel", "temporary": "contractuel", "internship": "stage", "seasonal": "saisonnier"} class BreezyConnector(BaseConnector): """Base Breezy — sous-classes : définir source_id, EMPLOYER, ORG.""" ats = "breezy" request_delay = 1.0 EMPLOYER = "" ORG = "" quebec_only = True def _keep(self, item: dict) -> bool: if not self.quebec_only: return True loc = item.get("location") or {} # Breezy expose le pays de façon autoritaire : un posting hors Canada # n'est jamais québécois, même si sa ville est homonyme d'une ville QC # (« East Granby, Connecticut » matchait Granby — bug x4th_day_trucking # 2026-08-27, la variante nom d'État COMPLET échappe à _NON_QC_REGION_RE # qui ne couvre que les codes « CT »). Pays absent : heuristique texte. country = ((loc.get("country") or {}).get("id") or "").upper() if country and country != "CA": return False state = (loc.get("state") or {}) if (state.get("id") or "").upper() == "QC": return True return is_quebec_location( f"{loc.get('city') or ''}, {state.get('name') or ''}") def fetch(self) -> list[JobPosting]: items = self.get(f"https://{self.ORG}.breezy.hr/json", params={"verbose": "true"}).json() out: list[JobPosting] = [] for item in items or []: if not self._keep(item): continue loc = item.get("location") or {} typ = item.get("type") or {} sal = item.get("salary") salary_label = "" if isinstance(sal, str): salary_label = sal elif isinstance(sal, dict): salary_label = sal.get("text") or "" # adresse complète géocodable (composantes Google) si publiée street = loc.get("streetAddress") address = "" if isinstance(street, dict): address = street.get("location") or "" elif isinstance(street, str): address = street job = JobPosting( source=self.source_id, external_id=str(item.get("id") or ""), url=item.get("url") or "", employer=self.EMPLOYER or (item.get("company") or {}).get("name", ""), title=item.get("name") or "", description=clean_html(item.get("description") or ""), address=address, city=loc.get("city") or "", location_label=f"{loc.get('city') or ''}, " f"{(loc.get('state') or {}).get('name') or ''}".strip(", "), work_mode="teletravail" if loc.get("is_remote") else None, date_posted=item.get("published_date"), salary_label=salary_label, company_logo=(item.get("company") or {}).get("logo_url") or "", ats=self.ats, ) job.details["employment_label"] = typ.get("name") or typ.get("id") or "" if item.get("department"): job.details["team"] = item["department"] out.append(job) return out