SPB Git

spb/job-ka

Public

[ka6] fix connecteur msi_gestion: repli « détail seulement » pendant les maintenances nocturnes d'ADP WFN — cause de l'échec du 2026-08-29 22:51 : fenêtre de maintenance planifiée d'ADP Workforce Now du samedi soir (~22:00→02:00 ET, même événement systémique que cultures_genv a57f452, groupe_nordik a17b979, h2o_innovation 644a2d4 et mitsubishi_hc_capital 9d9e3f4), pendant laquelle workforcenow.adp.com redirige vers sorry.adp.com et la liste job-requisitions renvoie du HTML (« Expecting value: line 1 column 1 »). Un seul passage raté + cycle ~3 h suffit à dépasser le seuil stale de 4 h. Fix (pattern cultures_genv/groupe_nordik/h2o_innovation/mitsubishi_hc_capital/ciena) : si la liste échoue en 5xx/non-JSON, revalidation une à une des offres actives connues via l'API détail job-requisitions/<itemID> (réquisition complète live — aucune donnée inventée) ; 404 = offre retirée ; si RIEN n'est validable (maintenance totale) l'erreur d'origine est propagée (sync en échec, inventaire préservé). Le chemin liste normal reprend seul dès la fin de la maintenance. Tests réels (2026-08-30 00:50-00:51 ET, juste avant le retour de la fenêtre de maintenance) : run.py sync msi_gestion (chemin normal) → ok, found=5 (= médiane 5.0), 5 actives en base ; repli avec liste 502 simulée → 5 offres revalidées live (descriptions complètes). À 00:52 ET la maintenance totale a repris (liste ET détail 302 → sorry.adp.com) : le repli propage alors l'erreur comme conçu. pm2 restart job-ka-sync OK, site local 200, www.job-ka.com 200.

Simon-Pierre Boucher committed (Aug 30, 2026) parent 9d9e3f4 Browse files

1 changed file +113 −4

Unified Split
modified jobka/connectors/msi_gestion.py +113 −4
@@ -3,11 +3,38 @@
3 # Auteur : Simon-Pierre Boucher 3 # Auteur : Simon-Pierre Boucher
4 # Contact : contact@spboucher.ai 4 # Contact : contact@spboucher.ai
5 # Fichier : jobka/connectors/msi_gestion.py 5 # Fichier : jobka/connectors/msi_gestion.py
6−# Rôle : Connecteur MSI Gestion immobilière — adp (cid='47d4e792-26e5-471c-92ac-73c60131bab8', ccid='19000101_000001') 6+# Rôle : Connecteur MSI Gestion immobilière — ADP WFN + repli « détail
7−# [généré par scripts/gen_connectors.py, flux vérifié le 2026-08-18] 7+# seulement » quand l'API liste échoue (maintenances nocturnes ADP)
8−# Créé : 2026-08-18 Modifié : 2026-08-18 8+# Créé : 2026-08-18 Modifié : 2026-08-30
9 # ============================================================================= 9 # =============================================================================
10−from .adp import ADPWorkforceNowConnector 10+"""MSI Gestion immobilière — ADP Workforce Now, avec repli de revalidation.
11+
12+Contexte (2026-08-28/29) : ADP Workforce Now a des fenêtres de maintenance
13+nocturnes récurrentes (vendredi ET samedi soir ~22:00 → 02:00 ET) pendant
14+lesquelles TOUT workforcenow.adp.com redirige vers sorry.adp.com — la liste
15+job-requisitions renvoie alors du HTML, d'où le « Expecting value: line 1
16+column 1 » du sync de 22:51 (29/08). Comme le cycle complet met ~3 h entre
17+deux passages de la source, un seul passage raté pendant la fenêtre suffit à
18+dépasser le seuil stale de 4 h.
19+
20+D'où le repli ci-dessous (même pattern que cultures_genv.py /
21+groupe_nordik.py / h2o_innovation.py / mitsubishi_hc_capital.py / ciena.py) :
22+si la pagination liste échoue en 5xx ou en non-JSON, on revalide une à une
23+les offres actives connues en base via l'API détail
24+job-requisitions/<itemID> (qui renvoie la réquisition complète : titre, date,
25+lieux, description). Aucune donnée inventée — tout vient de la réponse live.
26+Une offre en 404 est considérée retirée. Si AUCUNE offre n'est validable
27+(maintenance totale : le détail redirige aussi vers sorry.adp.com), l'erreur
28+d'origine est propagée pour que le sync reste un échec (inventaire préservé).
29+Limite assumée : le repli ne découvre pas les NOUVELLES offres — le chemin
30+liste normal reprend seul au cycle suivant la fin de la maintenance.
31+"""
32+from __future__ import annotations
33+
34+import requests
35+
36+from ..schema import JobPosting, clean_html
37+from .adp import _API, ADPWorkforceNowConnector
11 38
12 39
13 class MsiGestionConnector(ADPWorkforceNowConnector): 40 class MsiGestionConnector(ADPWorkforceNowConnector):
@@ -15,3 +42,85 @@ class MsiGestionConnector(ADPWorkforceNowConnector):
15 EMPLOYER = 'MSI Gestion immobilière' 42 EMPLOYER = 'MSI Gestion immobilière'
16 CID = '47d4e792-26e5-471c-92ac-73c60131bab8' 43 CID = '47d4e792-26e5-471c-92ac-73c60131bab8'
17 CCID = '19000101_000001' 44 CCID = '19000101_000001'
45+
46+ def fetch(self) -> list[JobPosting]:
47+ try:
48+ return super().fetch()
49+ except (requests.RequestException, ValueError) as exc:
50+ resp = getattr(exc, "response", None)
51+ if resp is not None and resp.status_code < 500:
52+ raise # 4xx franc : pas une panne liste, ne pas masquer
53+ jobs = self._fetch_known_jobs()
54+ if jobs is None:
55+ raise # API détail cassée aussi : vraie panne totale
56+ return jobs
57+
58+ def _known_ids(self) -> list[str]:
59+ """itemID (external_id) des offres actives de la source en base."""
60+ from .. import db
61+ con = db.connect()
62+ try:
63+ rows = con.execute(
64+ "SELECT external_id FROM jobs WHERE source=? AND active=1",
65+ (self.source_id,)).fetchall()
66+ finally:
67+ con.close()
68+ return [str(eid) for (eid,) in rows if eid]
69+
70+ def _fetch_known_jobs(self) -> list[JobPosting] | None:
71+ """Repli sans liste : chaque offre connue est revalidée via l'API
72+ détail (la réquisition complète, source de vérité de son existence).
73+ Aucune donnée inventée — tout vient de la réponse du moment."""
74+ out: list[JobPosting] = []
75+ errors = 0
76+ for iid in self._known_ids():
77+ try:
78+ r = self.get(f"{_API}/{iid}", params=self._params(),
79+ headers={"Accept": "application/json"}).json()
80+ except requests.HTTPError as exc:
81+ h = getattr(exc, "response", None)
82+ if h is not None and h.status_code == 404:
83+ continue # offre retirée chez l'employeur
84+ errors += 1
85+ continue
86+ except (requests.RequestException, ValueError):
87+ errors += 1 # non-JSON (maintenance) ou réseau
88+ continue
89+ if not (r or {}).get("requisitionTitle"):
90+ errors += 1 # réponse inattendue : ne pas conclure au retrait
91+ continue
92+ locations = self._locations(r)
93+ if not self._keep(locations):
94+ continue
95+ qc = next((l for l in locations
96+ if l["prov"].upper() == "QC"),
97+ locations[0] if locations else
98+ {"city": self.FORCE_CITY, "postal": "",
99+ "label": self.FORCE_CITY})
100+ job = JobPosting(
101+ source=self.source_id, external_id=iid,
102+ url=("https://workforcenow.adp.com/mascsr/default/mdf/"
103+ f"recruitment/recruitment.html?cid={self.CID}"
104+ f"&ccId={self.CCID}&lang={self.LANG}&jobId={iid}"),
105+ employer=self.EMPLOYER,
106+ title=r.get("requisitionTitle") or "",
107+ city=qc["city"], postal_code=qc["postal"],
108+ location_label=qc["label"],
109+ date_posted=r.get("postDate") or None,
110+ ats=self.ats,
111+ )
112+ pay = r.get("payGradeRange") or {}
113+ lo = (pay.get("minimumRate") or {}).get("amountValue")
114+ hi = (pay.get("maximumRate") or {}).get("amountValue")
115+ if lo:
116+ job.salary_min = float(lo)
117+ job.salary_max = float(hi) if hi else None
118+ job.salary_unit = "year" if float(lo) > 5000 else "hour"
119+ wl = (r.get("workLevelCode") or {}).get("shortName")
120+ if wl:
121+ job.details["employment_label"] = wl
122+ job.description = clean_html(r.get("requisitionDescription") or "")
123+ out.append(job)
124+ if not out and errors:
125+ return None # rien de validable : laisser le sync échouer
126+ return out
18 127
Menu

Theme