SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
1 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%

[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>
Simon-Pierre Boucher committed 24 days ago (Aug 31, 2026) parent 95df645

1 changed file +26 −5

modified jobka/connectors/workday.py +26 −5
@@ -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-17
8 +# Créé : 2026-08-17 Modifié : 2026-08-31
9 9 # =============================================================================
10 10 """Plateforme Workday.
11 11
@@ -28,6 +28,7 @@ from __future__ import annotations
28 28 import hashlib
29 29 import os
30 30 import re
31 +import time
31 32
32 33 from ..schema import JobPosting, clean_html, is_quebec_location
33 34 from .base import BaseConnector
@@ -54,6 +55,24 @@ class WorkdayConnector(BaseConnector):
54 55 def _base(self) -> str:
55 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 76 def _keep(self, item: dict) -> bool:
58 77 """Crochet de filtrage d'une ligne de liste (défaut : lieu québécois)."""
59 78 if not self.quebec_only:
@@ -121,9 +140,10 @@ class WorkdayConnector(BaseConnector):
121 140 total = 0
122 141 first = True
123 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 145 "limit": PAGE_SIZE,
126 − "offset": offset, "searchText": ""}).json()
146 + "offset": offset, "searchText": ""})
127 147 if first:
128 148 first = False
129 149 self._first_facets = data.get("facets") or []
@@ -169,8 +189,9 @@ class WorkdayConnector(BaseConnector):
169 189 continue # facette indisponible : on n'invente rien
170 190
171 191 def _fetch_detail(self, external_path: str) -> dict:
172 − data = self.get(f"{self._base}/wday/cxs/{self.TENANT}/{self.SITE}"
173 − f"{external_path}").json()
192 + data = self._req_json("get",
193 + f"{self._base}/wday/cxs/{self.TENANT}"
194 + f"/{self.SITE}{external_path}")
174 195 info = data.get("jobPostingInfo") or {}
175 196 return {
176 197 "description_html": info.get("jobDescription") or "",
177 198