SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
4 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
5.2 KB · 117 lines python
Raw Blame History
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur  : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/connectors/ciena.py6# Rôle    : Connecteur Ciena — workday (tenant ciena.wd5, site Careers)7#           [généré par scripts/gen_connectors.py, flux vérifié le 2026-08-23]8#           + repli « détail seulement » quand l'API liste du tenant est en9#           panne côté Workday (502 backend depuis le 2026-08-28)10# Créé    : 2026-08-23   Modifié : 2026-08-2911# =============================================================================12"""Ciena — cas particulier du tenant ciena.wd5.1314Depuis ~12:39 EDT le 2026-08-28, l'endpoint liste POST15/wday/cxs/ciena/Careers/jobs renvoie 502 (errorCaseId Workday) pour tout le16monde — vérifié depuis le nœud ET via proxy résidentiel canadien, ainsi qu'en17navigateur réel (le board React rend une page vide car son propre appel /jobs18reçoit le 502). L'index de recherche du tenant est cassé côté Workday, mais19l'API détail GET /wday/cxs/ciena/Careers/job/<externalPath> répond 200 avec20les données complètes (posted, startDate, description…), et le site officiel21ciena.com/careers pointe toujours vers ce board (pas de migration d'ATS).2223D'où le repli ci-dessous : si la pagination liste échoue en 5xx, on revalide24une à une les offres actives connues en base via l'API détail. Une offre qui25répond 404 ou `posted: false` est considérée retirée (elle disparaît du26résultat, la mécanique standard miss_count fait le reste). Si AUCUNE offre27n'est validable (API détail cassée elle aussi), on propage l'erreur d'origine28pour que le sync reste un échec (inventaire préservé). Limite assumée : le29repli ne découvre pas les NOUVELLES offres — dès que l'index Workday revient,30`super().fetch()` reprend le chemin normal au cycle suivant.31"""32from __future__ import annotations3334import requests3536from ..schema import JobPosting, clean_html37from .workday import WorkdayConnector383940class CienaConnector(WorkdayConnector):41    source_id = 'ciena'42    EMPLOYER = 'Ciena'43    TENANT = 'ciena'44    HOST = 'wd5'45    SITE = 'Careers'4647    def fetch(self) -> list[JobPosting]:48        try:49            return super().fetch()50        except (requests.RequestException, ValueError) as exc:51            resp = getattr(exc, "response", None)52            if resp is not None and resp.status_code < 500:53                raise            # 4xx : pas la panne d'index, ne pas masquer54            jobs = self._fetch_known_jobs()55            if jobs is None:56                raise            # API détail cassée aussi : vraie panne totale57            return jobs5859    def _known_paths(self) -> list[str]:60        """externalPath (« /job/… ») des offres actives de la source en base."""61        from .. import db62        con = db.connect()63        try:64            rows = con.execute(65                "SELECT url FROM jobs WHERE source=? AND active=1",66                (self.source_id,)).fetchall()67        finally:68            con.close()69        marker = f"/{self.SITE}/job/"70        paths = []71        for (url,) in rows:72            idx = (url or "").find(marker)73            if idx >= 0:74                paths.append(url[idx + len(marker) - len("/job/"):])75        return paths7677    def _fetch_known_jobs(self) -> list[JobPosting] | None:78        """Repli sans liste : chaque offre connue est revalidée via l'API79        détail (source de vérité de son existence). Aucune donnée inventée —80        tout vient de la réponse jobPostingInfo du moment."""81        out: list[JobPosting] = []82        errors = 083        for path in self._known_paths():84            try:85                data = self.get(86                    f"{self._base}/wday/cxs/{self.TENANT}/{self.SITE}{path}",87                    headers={"Accept": "application/json"}).json()88            except requests.HTTPError as exc:89                r = getattr(exc, "response", None)90                if r is not None and r.status_code == 404:91                    continue     # offre retirée chez l'employeur92                errors += 193                continue94            except (requests.RequestException, ValueError):95                errors += 196                continue97            info = (data or {}).get("jobPostingInfo") or {}98            if not info or not info.get("posted", True):99                continue         # dépubliée100            eid = info.get("jobReqId") or path.rsplit("_", 1)[-1]101            job = JobPosting(102                source=self.source_id, external_id=str(eid),103                url=f"{self._base}/{self.SITE}{path}",104                employer=self.EMPLOYER,105                title=(info.get("title") or "").strip(),106                location_label=info.get("location") or "",107                date_posted=info.get("startDate") or None,108                ats=self.ats,109            )110            job.description = clean_html(info.get("jobDescription") or "")111            job.details["employment_label"] = info.get("timeType") or ""112            job.apply_url = info.get("externalUrl") or ""113            out.append(job)114        if not out and errors:115            return None          # rien de validable : laisser le sync échouer116        return out117