# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/ciena.py # Rôle : Connecteur Ciena — workday (tenant ciena.wd5, site Careers) # [généré par scripts/gen_connectors.py, flux vérifié le 2026-08-23] # + repli « détail seulement » quand l'API liste du tenant est en # panne côté Workday (502 backend depuis le 2026-08-28) # Créé : 2026-08-23 Modifié : 2026-08-29 # ============================================================================= """Ciena — cas particulier du tenant ciena.wd5. Depuis ~12:39 EDT le 2026-08-28, l'endpoint liste POST /wday/cxs/ciena/Careers/jobs renvoie 502 (errorCaseId Workday) pour tout le monde — vérifié depuis le nœud ET via proxy résidentiel canadien, ainsi qu'en navigateur réel (le board React rend une page vide car son propre appel /jobs reçoit le 502). L'index de recherche du tenant est cassé côté Workday, mais l'API détail GET /wday/cxs/ciena/Careers/job/ répond 200 avec les données complètes (posted, startDate, description…), et le site officiel ciena.com/careers pointe toujours vers ce board (pas de migration d'ATS). D'où le repli ci-dessous : si la pagination liste échoue en 5xx, on revalide une à une les offres actives connues en base via l'API détail. Une offre qui répond 404 ou `posted: false` est considérée retirée (elle disparaît du résultat, la mécanique standard miss_count fait le reste). Si AUCUNE offre n'est validable (API détail cassée elle aussi), on propage l'erreur d'origine pour que le sync reste un échec (inventaire préservé). Limite assumée : le repli ne découvre pas les NOUVELLES offres — dès que l'index Workday revient, `super().fetch()` reprend le chemin normal au cycle suivant. """ from __future__ import annotations import requests from ..schema import JobPosting, clean_html from .workday import WorkdayConnector class CienaConnector(WorkdayConnector): source_id = 'ciena' EMPLOYER = 'Ciena' TENANT = 'ciena' HOST = 'wd5' SITE = 'Careers' def fetch(self) -> list[JobPosting]: try: return super().fetch() except (requests.RequestException, ValueError) as exc: resp = getattr(exc, "response", None) if resp is not None and resp.status_code < 500: raise # 4xx : pas la panne d'index, ne pas masquer jobs = self._fetch_known_jobs() if jobs is None: raise # API détail cassée aussi : vraie panne totale return jobs def _known_paths(self) -> list[str]: """externalPath (« /job/… ») des offres actives de la source en base.""" from .. import db con = db.connect() try: rows = con.execute( "SELECT url FROM jobs WHERE source=? AND active=1", (self.source_id,)).fetchall() finally: con.close() marker = f"/{self.SITE}/job/" paths = [] for (url,) in rows: idx = (url or "").find(marker) if idx >= 0: paths.append(url[idx + len(marker) - len("/job/"):]) return paths def _fetch_known_jobs(self) -> list[JobPosting] | None: """Repli sans liste : chaque offre connue est revalidée via l'API détail (source de vérité de son existence). Aucune donnée inventée — tout vient de la réponse jobPostingInfo du moment.""" out: list[JobPosting] = [] errors = 0 for path in self._known_paths(): try: data = self.get( f"{self._base}/wday/cxs/{self.TENANT}/{self.SITE}{path}", headers={"Accept": "application/json"}).json() except requests.HTTPError as exc: r = getattr(exc, "response", None) if r is not None and r.status_code == 404: continue # offre retirée chez l'employeur errors += 1 continue except (requests.RequestException, ValueError): errors += 1 continue info = (data or {}).get("jobPostingInfo") or {} if not info or not info.get("posted", True): continue # dépubliée eid = info.get("jobReqId") or path.rsplit("_", 1)[-1] job = JobPosting( source=self.source_id, external_id=str(eid), url=f"{self._base}/{self.SITE}{path}", employer=self.EMPLOYER, title=(info.get("title") or "").strip(), location_label=info.get("location") or "", date_posted=info.get("startDate") or None, ats=self.ats, ) job.description = clean_html(info.get("jobDescription") or "") job.details["employment_label"] = info.get("timeType") or "" job.apply_url = info.get("externalUrl") or "" out.append(job) if not out and errors: return None # rien de validable : laisser le sync échouer return out