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/breezy.py6# Rôle : Classe de plateforme Breezy HR (<org>.breezy.hr/json) — un7# employeur = une sous-classe de ~8 lignes (ORG, EMPLOYER)8# Créé : 2026-08-17 Modifié : 2026-08-279# =============================================================================10"""Plateforme Breezy HR.1112API publique : GET https://<org>.breezy.hr/json?verbose=true13-> liste de {id, friendly_id, name(titre), url(lien direct),14 type:{id:"fullTime", name:"Temps plein"}, department,15 location:{country:{id}, state:{id:"QC", name:"Quebec"}, city},16 published_date(ISO), salary, description(HTML — seulement avec verbose)}.1718Le filtre le plus fiable est location.state.id == "QC". Une seule requête.19"""20from __future__ import annotations2122from ..schema import JobPosting, clean_html, is_quebec_location23from .base import BaseConnector2425_TYPE = {"fulltime": "temps_plein", "parttime": "temps_partiel",26 "contract": "contractuel", "temporary": "contractuel",27 "internship": "stage", "seasonal": "saisonnier"}282930class BreezyConnector(BaseConnector):31 """Base Breezy — sous-classes : définir source_id, EMPLOYER, ORG."""3233 ats = "breezy"34 request_delay = 1.03536 EMPLOYER = ""37 ORG = ""38 quebec_only = True3940 def _keep(self, item: dict) -> bool:41 if not self.quebec_only:42 return True43 loc = item.get("location") or {}44 # Breezy expose le pays de façon autoritaire : un posting hors Canada45 # n'est jamais québécois, même si sa ville est homonyme d'une ville QC46 # (« East Granby, Connecticut » matchait Granby — bug x4th_day_trucking47 # 2026-08-27, la variante nom d'État COMPLET échappe à _NON_QC_REGION_RE48 # qui ne couvre que les codes « CT »). Pays absent : heuristique texte.49 country = ((loc.get("country") or {}).get("id") or "").upper()50 if country and country != "CA":51 return False52 state = (loc.get("state") or {})53 if (state.get("id") or "").upper() == "QC":54 return True55 return is_quebec_location(56 f"{loc.get('city') or ''}, {state.get('name') or ''}")5758 def fetch(self) -> list[JobPosting]:59 items = self.get(f"https://{self.ORG}.breezy.hr/json",60 params={"verbose": "true"}).json()61 out: list[JobPosting] = []62 for item in items or []:63 if not self._keep(item):64 continue65 loc = item.get("location") or {}66 typ = item.get("type") or {}67 sal = item.get("salary")68 salary_label = ""69 if isinstance(sal, str):70 salary_label = sal71 elif isinstance(sal, dict):72 salary_label = sal.get("text") or ""73 # adresse complète géocodable (composantes Google) si publiée74 street = loc.get("streetAddress")75 address = ""76 if isinstance(street, dict):77 address = street.get("location") or ""78 elif isinstance(street, str):79 address = street80 job = JobPosting(81 source=self.source_id, external_id=str(item.get("id") or ""),82 url=item.get("url") or "",83 employer=self.EMPLOYER or (item.get("company") or {}).get("name", ""),84 title=item.get("name") or "",85 description=clean_html(item.get("description") or ""),86 address=address,87 city=loc.get("city") or "",88 location_label=f"{loc.get('city') or ''}, "89 f"{(loc.get('state') or {}).get('name') or ''}".strip(", "),90 work_mode="teletravail" if loc.get("is_remote") else None,91 date_posted=item.get("published_date"),92 salary_label=salary_label,93 company_logo=(item.get("company") or {}).get("logo_url") or "",94 ats=self.ats,95 )96 job.details["employment_label"] = typ.get("name") or typ.get("id") or ""97 if item.get("department"):98 job.details["team"] = item["department"]99 out.append(job)100 return out101