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/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