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%

[ka6] fix connecteur ciena: repli « détail seulement » pendant la panne d'index Workday du tenant — l'endpoint liste POST /wday/cxs/ciena/Careers/jobs renvoie 502 backend (errorCaseId Workday) pour tout le monde depuis ~12:39 EDT le 2026-08-28 (reconfirmé le 2026-08-29 depuis le nœud, via proxy résidentiel CA Oxylabs et en navigateur réel : le board React rend une page vide), MAIS l'API détail GET /wday/cxs/ciena/Careers/job/<externalPath> répond 200 avec données complètes et ciena.com/careers pointe toujours vers ce board (pas de migration). Le connecteur revalide donc les offres actives connues en base via l'API détail quand la liste échoue en 5xx : 404/posted=false = offre retirée, aucune donnée inventée (tout vient du jobPostingInfo live), et si RIEN n'est validable l'erreur d'origine est propagée (sync en échec, inventaire préservé). Dès que l'index Workday revient, le chemin liste normal reprend seul. Test réel : run.py sync ciena → ok, found=7 (= médiane 7.0), updated=5, removed=0, 7 actives en base. pm2 restart job-ka-sync OK, site local 200, www.job-ka.com 200.

Simon-Pierre Boucher committed 26 days ago (Aug 29, 2026) parent dab8068

1 changed file +99 −1

modified jobka/connectors/ciena.py +99 −1
@@ -5,8 +5,35 @@
5 5 # Fichier : jobka/connectors/ciena.py
6 6 # Rôle : Connecteur Ciena — workday (tenant ciena.wd5, site Careers)
7 7 # [généré par scripts/gen_connectors.py, flux vérifié le 2026-08-23]
8 −# Créé : 2026-08-23 Modifié : 2026-08-23
8 +# + repli « détail seulement » quand l'API liste du tenant est en
9 +# panne côté Workday (502 backend depuis le 2026-08-28)
10 +# Créé : 2026-08-23 Modifié : 2026-08-29
9 11 # =============================================================================
12 +"""Ciena — cas particulier du tenant ciena.wd5.
13 +
14 +Depuis ~12:39 EDT le 2026-08-28, l'endpoint liste POST
15 +/wday/cxs/ciena/Careers/jobs renvoie 502 (errorCaseId Workday) pour tout le
16 +monde — vérifié depuis le nœud ET via proxy résidentiel canadien, ainsi qu'en
17 +navigateur réel (le board React rend une page vide car son propre appel /jobs
18 +reçoit le 502). L'index de recherche du tenant est cassé côté Workday, mais
19 +l'API détail GET /wday/cxs/ciena/Careers/job/<externalPath> répond 200 avec
20 +les données complètes (posted, startDate, description…), et le site officiel
21 +ciena.com/careers pointe toujours vers ce board (pas de migration d'ATS).
22 +
23 +D'où le repli ci-dessous : si la pagination liste échoue en 5xx, on revalide
24 +une à une les offres actives connues en base via l'API détail. Une offre qui
25 +répond 404 ou `posted: false` est considérée retirée (elle disparaît du
26 +résultat, la mécanique standard miss_count fait le reste). Si AUCUNE offre
27 +n'est validable (API détail cassée elle aussi), on propage l'erreur d'origine
28 +pour que le sync reste un échec (inventaire préservé). Limite assumée : le
29 +repli 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 +"""
32 +from __future__ import annotations
33 +
34 +import requests
35 +
36 +from ..schema import JobPosting, clean_html
10 37 from .workday import WorkdayConnector
11 38
12 39
@@ -16,3 +43,74 @@ class CienaConnector(WorkdayConnector):
16 43 TENANT = 'ciena'
17 44 HOST = 'wd5'
18 45 SITE = 'Careers'
46 +
47 + 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 masquer
54 + jobs = self._fetch_known_jobs()
55 + if jobs is None:
56 + raise # API détail cassée aussi : vraie panne totale
57 + return jobs
58 +
59 + def _known_paths(self) -> list[str]:
60 + """externalPath (« /job/… ») des offres actives de la source en base."""
61 + from .. import db
62 + 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 paths
76 +
77 + def _fetch_known_jobs(self) -> list[JobPosting] | None:
78 + """Repli sans liste : chaque offre connue est revalidée via l'API
79 + 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 = 0
83 + 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'employeur
92 + errors += 1
93 + continue
94 + except (requests.RequestException, ValueError):
95 + errors += 1
96 + continue
97 + info = (data or {}).get("jobPostingInfo") or {}
98 + if not info or not info.get("posted", True):
99 + continue # dépubliée
100 + 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 échouer
116 + return out
19 117