[ka6] fix connecteur groupemaurice: 502 Bad Gateway transitoire du frontal Dayforce avortait tout le sync (stale 5,3 h, un seul POST search en échec). Garde-fou dans DayforceConnector: le POST de recherche retente une fois après pause sur 5xx, puis lève (sync en échec, jamais un faux 0) — même pattern que njoyn 33dba44 / digitalrecruiters 81cf8e2. Sync isolé re-testé: 108 offres trouvées (= médiane 108).
1 changed file +29 −7
modified
jobka/connectors/dayforce.py
+29 −7
@@ -5,7 +5,7 @@ | |||||
| 5 | # Fichier : jobka/connectors/dayforce.py | 5 | # Fichier : jobka/connectors/dayforce.py | ||
| 6 | # Rôle : Classe de plateforme Dayforce HCM (jobs.dayforcehcm.com) — API JSON | 6 | # Rôle : Classe de plateforme Dayforce HCM (jobs.dayforcehcm.com) — API JSON | ||
| 7 | # publique du frontal Next.js, un employeur = une sous-classe | 7 | # publique du frontal Next.js, un employeur = une sous-classe | ||
| 8 | −# Créé : 2026-08-22 Modifié : 2026-08-22 | 8 | +# Créé : 2026-08-22 Modifié : 2026-08-26 | ||
| 9 | # ============================================================================= | 9 | # ============================================================================= | ||
| 10 | """Plateforme Dayforce HCM (ex-Ceridian) — jobs.dayforcehcm.com. | 10 | """Plateforme Dayforce HCM (ex-Ceridian) — jobs.dayforcehcm.com. | ||
| 11 | 11 | ||||
@@ -33,6 +33,9 @@ from __future__ import annotations | |||||
| 33 | 33 | ||||
| 34 | import os | 34 | import os | ||
| 35 | import re | 35 | import re | ||
| 36 | +import time | ||||
| 37 | + | ||||
| 38 | +import requests | ||||
| 36 | 39 | ||||
| 37 | from ..schema import JobPosting, clean_html, is_quebec_location | 40 | from ..schema import JobPosting, clean_html, is_quebec_location | ||
| 38 | from .base import BaseConnector | 41 | from .base import BaseConnector | ||
@@ -89,6 +92,30 @@ class DayforceConnector(BaseConnector): | |||||
| 89 | "jobDescriptionFooter")) | 92 | "jobDescriptionFooter")) | ||
| 90 | return {"attributes": attrs, "description": clean_html(desc)} | 93 | return {"attributes": attrs, "description": clean_html(desc)} | ||
| 91 | 94 | ||||
| 95 | + # -- recherche (garde-fou 5xx transitoire) ------------------------------ | ||||
| 96 | + def _search_page(self, search_url: str, csrf: str, start: int) -> dict: | ||||
| 97 | + """POST de recherche avec garde-fou : le frontal Dayforce renvoie | ||||
| 98 | + parfois un 5xx passager (502 Bad Gateway observé le 2026-08-26 sur | ||||
| 99 | + groupemaurice) qui avortait tout le sync — on retente une fois après | ||||
| 100 | + pause, puis on lève (sync en échec, jamais un faux 0).""" | ||||
| 101 | + for attempt in range(2): | ||||
| 102 | + try: | ||||
| 103 | + return self.post( | ||||
| 104 | + search_url, | ||||
| 105 | + json={"clientNamespace": self.NS, | ||||
| 106 | + "jobBoardCode": self.BOARD, | ||||
| 107 | + "cultureCode": self.CULTURE, | ||||
| 108 | + "paginationStart": start}, | ||||
| 109 | + headers={"X-CSRF-TOKEN": csrf, | ||||
| 110 | + "Accept": "application/json"}).json() | ||||
| 111 | + except requests.HTTPError as e: | ||||
| 112 | + status = getattr(e.response, "status_code", 0) or 0 | ||||
| 113 | + if attempt == 0 and status >= 500: | ||||
| 114 | + time.sleep(10) | ||||
| 115 | + continue | ||||
| 116 | + raise | ||||
| 117 | + raise RuntimeError("unreachable") | ||||
| 118 | + | ||||
| 92 | # -- contrat ------------------------------------------------------------ | 119 | # -- contrat ------------------------------------------------------------ | ||
| 93 | def fetch(self) -> list[JobPosting]: | 120 | def fetch(self) -> list[JobPosting]: | ||
| 94 | csrf = (self.get(f"{BASE}/api/auth/csrf").json() or {}).get( | 121 | csrf = (self.get(f"{BASE}/api/auth/csrf").json() or {}).get( | ||
@@ -99,12 +126,7 @@ class DayforceConnector(BaseConnector): | |||||
| 99 | details_used = 0 | 126 | details_used = 0 | ||
| 100 | start = 0 | 127 | start = 0 | ||
| 101 | for _ in range(self.max_pages): | 128 | for _ in range(self.max_pages): | ||
| 102 | − data = self.post( | 129 | + data = self._search_page(search_url, csrf, start) | ||
| 103 | − search_url, | ||||
| 104 | − json={"clientNamespace": self.NS, "jobBoardCode": self.BOARD, | ||||
| 105 | − "cultureCode": self.CULTURE, "paginationStart": start}, | ||||
| 106 | − headers={"X-CSRF-TOKEN": csrf, | ||||
| 107 | − "Accept": "application/json"}).json() | ||||
| 108 | postings = data.get("jobPostings") or [] | 130 | postings = data.get("jobPostings") or [] | ||
| 109 | if not postings: | 131 | if not postings: | ||
| 110 | break | 132 | break | ||
| 111 | 133 | ||||