[ka6] fix connecteur bento_sushi: reprise sur réponses ADP 200 non-JSON (retry+backoff, erreur diagnosticable)
1 changed file +23 −6
modified
jobka/connectors/adp.py
+23 −6
@@ -5,7 +5,7 @@ | |||||
| 5 | # Fichier : jobka/connectors/adp.py | 5 | # Fichier : jobka/connectors/adp.py | ||
| 6 | # Rôle : Classe de plateforme ADP Workforce Now (centre de carrières) — | 6 | # Rôle : Classe de plateforme ADP Workforce Now (centre de carrières) — | ||
| 7 | # API JSON publique job-requisitions, un employeur = une sous-classe | 7 | # API JSON publique job-requisitions, un employeur = une sous-classe | ||
| 8 | −# Créé : 2026-08-18 Modifié : 2026-08-25 | 8 | +# Créé : 2026-08-18 Modifié : 2026-08-30 | ||
| 9 | # ============================================================================= | 9 | # ============================================================================= | ||
| 10 | """Plateforme ADP Workforce Now (workforcenow.adp.com). | 10 | """Plateforme ADP Workforce Now (workforcenow.adp.com). | ||
| 11 | 11 | ||||
@@ -21,6 +21,7 @@ API JSON publique du centre de carrières : | |||||
| 21 | from __future__ import annotations | 21 | from __future__ import annotations | ||
| 22 | 22 | ||||
| 23 | import os | 23 | import os | ||
| 24 | +import time | ||||
| 24 | 25 | ||||
| 25 | from ..schema import JobPosting, clean_html, is_quebec_location | 26 | from ..schema import JobPosting, clean_html, is_quebec_location | ||
| 26 | from .base import BaseConnector | 27 | from .base import BaseConnector | ||
@@ -50,6 +51,24 @@ class ADPWorkforceNowConnector(BaseConnector): | |||||
| 50 | FORCE_CITY = "" # employeurs mono-site QC dont le babillard n'expose | 51 | FORCE_CITY = "" # employeurs mono-site QC dont le babillard n'expose | ||
| 51 | # aucune requisitionLocation : ville imposée | 52 | # aucune requisitionLocation : ville imposée | ||
| 52 | 53 | ||||
| 54 | + def _get_json(self, url: str, params: dict) -> dict: | ||||
| 55 | + # ADP renvoie parfois 200 avec un corps vide/HTML (fenêtres de | ||||
| 56 | + # maintenance nocturnes constatées les 29-30 août 2026) : on retente | ||||
| 57 | + # avec backoff avant d'abandonner, et l'erreur finale nomme le statut | ||||
| 58 | + # HTTP + content-type pour que sync_log soit diagnostiquable. | ||||
| 59 | + last: ValueError | None = None | ||||
| 60 | + for attempt in range(3): | ||||
| 61 | + resp = self.get(url, params=params, | ||||
| 62 | + headers={"Accept": "application/json"}) | ||||
| 63 | + try: | ||||
| 64 | + return resp.json() | ||||
| 65 | + except ValueError as exc: | ||||
| 66 | + last = exc | ||||
| 67 | + time.sleep(10 * (attempt + 1)) | ||||
| 68 | + raise RuntimeError( | ||||
| 69 | + f"ADP non-JSON après 3 essais (HTTP {resp.status_code}, " | ||||
| 70 | + f"{resp.headers.get('content-type')}) : {last}") | ||||
| 71 | + | ||||
| 53 | def _params(self, extra: dict | None = None) -> dict: | 72 | def _params(self, extra: dict | None = None) -> dict: | ||
| 54 | p = {"cid": self.CID, "ccId": self.CCID, "lang": self.LANG, | 73 | p = {"cid": self.CID, "ccId": self.CCID, "lang": self.LANG, | ||
| 55 | "locale": self.LANG} | 74 | "locale": self.LANG} | ||
@@ -81,8 +100,7 @@ class ADPWorkforceNowConnector(BaseConnector): | |||||
| 81 | for l in locations) | 100 | for l in locations) | ||
| 82 | 101 | ||||
| 83 | def _fetch_detail(self, item_id: str) -> dict: | 102 | def _fetch_detail(self, item_id: str) -> dict: | ||
| 84 | − data = self.get(f"{_API}/{item_id}", params=self._params(), | 103 | + data = self._get_json(f"{_API}/{item_id}", params=self._params()) | ||
| 85 | − headers={"Accept": "application/json"}).json() | ||||
| 86 | return {"description": clean_html( | 104 | return {"description": clean_html( | ||
| 87 | data.get("requisitionDescription") or "")} | 105 | data.get("requisitionDescription") or "")} | ||
| 88 | 106 | ||||
@@ -91,9 +109,8 @@ class ADPWorkforceNowConnector(BaseConnector): | |||||
| 91 | details_used = 0 | 109 | details_used = 0 | ||
| 92 | skip = 0 | 110 | skip = 0 | ||
| 93 | for _ in range(self.max_pages): | 111 | for _ in range(self.max_pages): | ||
| 94 | − data = self.get(_API, params=self._params( | 112 | + data = self._get_json(_API, params=self._params( | ||
| 95 | − {"$top": str(PAGE_SIZE), "$skip": str(skip)}), | 113 | + {"$top": str(PAGE_SIZE), "$skip": str(skip)})) | ||
| 96 | − headers={"Accept": "application/json"}).json() | ||||
| 97 | reqs = data.get("jobRequisitions") or [] | 114 | reqs = data.get("jobRequisitions") or [] | ||
| 98 | if not reqs and skip == 0 and self.LANG != "en_US": | 115 | if not reqs and skip == 0 and self.LANG != "en_US": | ||
| 99 | # certains centres de carrières ne répondent qu'en anglais | 116 | # certains centres de carrières ne répondent qu'en anglais | ||
| 100 | 117 | ||||