[ka6] fix connecteur gsknch: reprise sur réponses Workday 200 non-JSON (fenêtres de maintenance nocturnes ~03h) — retry+backoff _req_json dans la plateforme workday, erreur diagnosticable, 11 offres retrouvées
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 changed file +26 −5
modified
jobka/connectors/workday.py
+26 −5
@@ -5,7 +5,7 @@ | |||||
| 5 | # Fichier : jobka/connectors/workday.py | 5 | # Fichier : jobka/connectors/workday.py | ||
| 6 | # Rôle : Classe de plateforme Workday (API JSON publique wday/cxs) — un | 6 | # Rôle : Classe de plateforme Workday (API JSON publique wday/cxs) — un | ||
| 7 | # employeur = une sous-classe de ~10 lignes (TENANT, HOST, SITE) | 7 | # employeur = une sous-classe de ~10 lignes (TENANT, HOST, SITE) | ||
| 8 | −# Créé : 2026-08-17 Modifié : 2026-08-17 | 8 | +# Créé : 2026-08-17 Modifié : 2026-08-31 | ||
| 9 | # ============================================================================= | 9 | # ============================================================================= | ||
| 10 | """Plateforme Workday. | 10 | """Plateforme Workday. | ||
| 11 | 11 | ||||
@@ -28,6 +28,7 @@ from __future__ import annotations | |||||
| 28 | import hashlib | 28 | import hashlib | ||
| 29 | import os | 29 | import os | ||
| 30 | import re | 30 | import re | ||
| 31 | +import time | ||||
| 31 | 32 | ||||
| 32 | from ..schema import JobPosting, clean_html, is_quebec_location | 33 | from ..schema import JobPosting, clean_html, is_quebec_location | ||
| 33 | from .base import BaseConnector | 34 | from .base import BaseConnector | ||
@@ -54,6 +55,24 @@ class WorkdayConnector(BaseConnector): | |||||
| 54 | def _base(self) -> str: | 55 | def _base(self) -> str: | ||
| 55 | return f"https://{self.TENANT}.{self.HOST}.myworkdayjobs.com" | 56 | return f"https://{self.TENANT}.{self.HOST}.myworkdayjobs.com" | ||
| 56 | 57 | ||||
| 58 | + def _req_json(self, method: str, url: str, **kwargs) -> dict: | ||||
| 59 | + # Workday renvoie parfois 200 avec un corps vide/HTML (fenêtres de | ||||
| 60 | + # maintenance nocturnes ~03 h EDT, constatées les 29 et 31 août 2026 | ||||
| 61 | + # sur de nombreux tenants) : on retente avec backoff avant | ||||
| 62 | + # d'abandonner, et l'erreur finale nomme le statut HTTP + | ||||
| 63 | + # content-type pour que sync_log soit diagnostiquable. | ||||
| 64 | + last: ValueError | None = None | ||||
| 65 | + for attempt in range(3): | ||||
| 66 | + resp = (self.post if method == "post" else self.get)(url, **kwargs) | ||||
| 67 | + try: | ||||
| 68 | + return resp.json() | ||||
| 69 | + except ValueError as exc: | ||||
| 70 | + last = exc | ||||
| 71 | + time.sleep(10 * (attempt + 1)) | ||||
| 72 | + raise RuntimeError( | ||||
| 73 | + f"Workday non-JSON après 3 essais (HTTP {resp.status_code}, " | ||||
| 74 | + f"{resp.headers.get('content-type')}) : {last}") | ||||
| 75 | + | ||||
| 57 | def _keep(self, item: dict) -> bool: | 76 | def _keep(self, item: dict) -> bool: | ||
| 58 | """Crochet de filtrage d'une ligne de liste (défaut : lieu québécois).""" | 77 | """Crochet de filtrage d'une ligne de liste (défaut : lieu québécois).""" | ||
| 59 | if not self.quebec_only: | 78 | if not self.quebec_only: | ||
@@ -121,9 +140,10 @@ class WorkdayConnector(BaseConnector): | |||||
| 121 | total = 0 | 140 | total = 0 | ||
| 122 | first = True | 141 | first = True | ||
| 123 | for _ in range(self.max_pages): | 142 | for _ in range(self.max_pages): | ||
| 124 | − data = self.post(url, json={"appliedFacets": applied, | 143 | + data = self._req_json("post", url, | ||
| 144 | + json={"appliedFacets": applied, | ||||
| 125 | "limit": PAGE_SIZE, | 145 | "limit": PAGE_SIZE, | ||
| 126 | − "offset": offset, "searchText": ""}).json() | 146 | + "offset": offset, "searchText": ""}) | ||
| 127 | if first: | 147 | if first: | ||
| 128 | first = False | 148 | first = False | ||
| 129 | self._first_facets = data.get("facets") or [] | 149 | self._first_facets = data.get("facets") or [] | ||
@@ -169,8 +189,9 @@ class WorkdayConnector(BaseConnector): | |||||
| 169 | continue # facette indisponible : on n'invente rien | 189 | continue # facette indisponible : on n'invente rien | ||
| 170 | 190 | ||||
| 171 | def _fetch_detail(self, external_path: str) -> dict: | 191 | def _fetch_detail(self, external_path: str) -> dict: | ||
| 172 | − data = self.get(f"{self._base}/wday/cxs/{self.TENANT}/{self.SITE}" | 192 | + data = self._req_json("get", | ||
| 173 | − f"{external_path}").json() | 193 | + f"{self._base}/wday/cxs/{self.TENANT}" | ||
| 194 | + f"/{self.SITE}{external_path}") | ||||
| 174 | info = data.get("jobPostingInfo") or {} | 195 | info = data.get("jobPostingInfo") or {} | ||
| 175 | return { | 196 | return { | ||
| 176 | "description_html": info.get("jobDescription") or "", | 197 | "description_html": info.get("jobDescription") or "", | ||
| 177 | 198 | ||||