SPB Git forge

spb/job-ka

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

[ka6] fix connecteur mitsubishi_hc_capital: repli « détail seulement » pendant les maintenances nocturnes d'ADP WFN — cause de l'échec du 2026-08-29 22:09 : 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 et h2o_innovation 644a2d4), 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/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 : repli avec liste 502 simulée → 5 offres revalidées live (descriptions complètes) ; run.py sync mitsubishi_hc_capital (chemin normal) → ok, found=5 (= médiane 5.0), 5 actives en base. pm2 restart job-ka-sync OK, site local 200, www.job-ka.com 200.

Simon-Pierre Boucher committed 25 days ago (Aug 30, 2026) parent 644a2d4

1 changed file +114 −4

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