SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
5 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
4.3 KB · 101 lines python
Raw Blame History
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