[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 | 5 | # Fichier : jobka/connectors/adp.py |
| 6 | 6 | # Rôle : Classe de plateforme ADP Workforce Now (centre de carrières) — |
| 7 | 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 | 10 | """Plateforme ADP Workforce Now (workforcenow.adp.com). |
| 11 | 11 | |
@@ -21,6 +21,7 @@ API JSON publique du centre de carrières : | ||
| 21 | 21 | from __future__ import annotations |
| 22 | 22 | |
| 23 | 23 | import os |
| 24 | +import time | |
| 24 | 25 | |
| 25 | 26 | from ..schema import JobPosting, clean_html, is_quebec_location |
| 26 | 27 | from .base import BaseConnector |
@@ -50,6 +51,24 @@ class ADPWorkforceNowConnector(BaseConnector): | ||
| 50 | 51 | FORCE_CITY = "" # employeurs mono-site QC dont le babillard n'expose |
| 51 | 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 | 72 | def _params(self, extra: dict | None = None) -> dict: |
| 54 | 73 | p = {"cid": self.CID, "ccId": self.CCID, "lang": self.LANG, |
| 55 | 74 | "locale": self.LANG} |
@@ -81,8 +100,7 @@ class ADPWorkforceNowConnector(BaseConnector): | ||
| 81 | 100 | for l in locations) |
| 82 | 101 | |
| 83 | 102 | def _fetch_detail(self, item_id: str) -> dict: |
| 84 | − data = self.get(f"{_API}/{item_id}", params=self._params(), | |
| 85 | − headers={"Accept": "application/json"}).json() | |
| 103 | + data = self._get_json(f"{_API}/{item_id}", params=self._params()) | |
| 86 | 104 | return {"description": clean_html( |
| 87 | 105 | data.get("requisitionDescription") or "")} |
| 88 | 106 | |
@@ -91,9 +109,8 @@ class ADPWorkforceNowConnector(BaseConnector): | ||
| 91 | 109 | details_used = 0 |
| 92 | 110 | skip = 0 |
| 93 | 111 | for _ in range(self.max_pages): |
| 94 | − data = self.get(_API, params=self._params( | |
| 95 | − {"$top": str(PAGE_SIZE), "$skip": str(skip)}), | |
| 96 | − headers={"Accept": "application/json"}).json() | |
| 112 | + data = self._get_json(_API, params=self._params( | |
| 113 | + {"$top": str(PAGE_SIZE), "$skip": str(skip)})) | |
| 97 | 114 | reqs = data.get("jobRequisitions") or [] |
| 98 | 115 | if not reqs and skip == 0 and self.LANG != "en_US": |
| 99 | 116 | # certains centres de carrières ne répondent qu'en anglais |
| 100 | 117 | |