[ka6] connecteur shrinerschildrens: maintenance tenant Workday (401 'system account', 2026-09-04 ~02h EDT, portail redirigé vers community.workday.com/maintenance-page) — retry+backoff du 401 de maintenance dans _req_json de la plateforme workday et erreur explicite dans sync_log; autres tenants wd12 non touchés (cabotcorp 4 offres vérifié)
1 changed file +38 −8
modified
jobka/connectors/workday.py
+38 −8
@@ -5,7 +5,7 @@ | ||
| 5 | 5 | # Fichier : jobka/connectors/workday.py |
| 6 | 6 | # Rôle : Classe de plateforme Workday (API JSON publique wday/cxs) — un |
| 7 | 7 | # employeur = une sous-classe de ~10 lignes (TENANT, HOST, SITE) |
| 8 | −# Créé : 2026-08-17 Modifié : 2026-08-31 | |
| 8 | +# Créé : 2026-08-17 Modifié : 2026-09-04 | |
| 9 | 9 | # ============================================================================= |
| 10 | 10 | """Plateforme Workday. |
| 11 | 11 | |
@@ -30,6 +30,8 @@ import os | ||
| 30 | 30 | import re |
| 31 | 31 | import time |
| 32 | 32 | |
| 33 | +import requests | |
| 34 | + | |
| 33 | 35 | from ..schema import JobPosting, clean_html, is_quebec_location |
| 34 | 36 | from .base import BaseConnector |
| 35 | 37 | |
@@ -55,20 +57,48 @@ class WorkdayConnector(BaseConnector): | ||
| 55 | 57 | def _base(self) -> str: |
| 56 | 58 | return f"https://{self.TENANT}.{self.HOST}.myworkdayjobs.com" |
| 57 | 59 | |
| 60 | + @staticmethod | |
| 61 | + def _is_maintenance_401(exc: requests.exceptions.HTTPError) -> bool: | |
| 62 | + """401 « Unable to verify credentials for system account » : signature | |
| 63 | + d'un tenant en maintenance Workday (le portail carrières redirige alors | |
| 64 | + vers community.workday.com/maintenance-page). Constaté le 2026-09-04 | |
| 65 | + sur shrinerschildrens.wd12 — les autres tenants du même host restaient | |
| 66 | + servis normalement.""" | |
| 67 | + resp = exc.response | |
| 68 | + if resp is None or resp.status_code != 401: | |
| 69 | + return False | |
| 70 | + try: | |
| 71 | + return "system account" in resp.text.lower() | |
| 72 | + except Exception: | |
| 73 | + return False | |
| 74 | + | |
| 58 | 75 | 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 | |
| 76 | + # Workday renvoie parfois 200 avec un corps vide/HTML, ou 401 | |
| 77 | + # « system account » (fenêtres de maintenance nocturnes ~02-03 h EDT, | |
| 78 | + # constatées les 29/31 août et 4 sept. 2026) : on retente avec backoff | |
| 79 | + # avant d'abandonner, et l'erreur finale est explicite pour que | |
| 80 | + # sync_log soit diagnostiquable. | |
| 81 | + last: Exception | None = None | |
| 82 | + maintenance = False | |
| 65 | 83 | for attempt in range(3): |
| 66 | − resp = (self.post if method == "post" else self.get)(url, **kwargs) | |
| 84 | + try: | |
| 85 | + resp = (self.post if method == "post" else self.get)(url, | |
| 86 | + **kwargs) | |
| 87 | + except requests.exceptions.HTTPError as exc: | |
| 88 | + if not self._is_maintenance_401(exc): | |
| 89 | + raise | |
| 90 | + last, maintenance = exc, True | |
| 91 | + time.sleep(10 * (attempt + 1)) | |
| 92 | + continue | |
| 67 | 93 | try: |
| 68 | 94 | return resp.json() |
| 69 | 95 | except ValueError as exc: |
| 70 | 96 | last = exc |
| 71 | 97 | time.sleep(10 * (attempt + 1)) |
| 98 | + if maintenance: | |
| 99 | + raise RuntimeError( | |
| 100 | + "Workday : tenant en maintenance (401 system account) après " | |
| 101 | + f"3 essais — fenêtre temporaire côté Workday : {last}") | |
| 72 | 102 | raise RuntimeError( |
| 73 | 103 | f"Workday non-JSON après 3 essais (HTTP {resp.status_code}, " |
| 74 | 104 | f"{resp.headers.get('content-type')}) : {last}") |
| 75 | 105 | |