SPB Git forge

spb/job-ka

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

[ka6] snapshot pré-mission osgoode_properties

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

1 changed file +116 −4

modified jobka/connectors/niobec.py +116 −4
@@ -2,11 +2,41 @@
2 2 # Job·Ka — Groupe KA
3 3 # Auteur : Simon-Pierre Boucher
4 4 # Contact : contact@spboucher.ai
5 −# Fichier : jobka/connectors/jobka/connectors/niobec.py
6 −# Rôle : Connecteur Niobec (Magris Performance Materials) — mine de niobium, Saint-Honoré (Saguenay) — ADP WFN
7 −# Créé : 2026-08-25 Modifié : 2026-08-25
5 +# Fichier : jobka/connectors/niobec.py
6 +# Rôle : Connecteur Niobec (Magris Performance Materials) — mine de niobium,
7 +# Saint-Honoré (Saguenay) — ADP WFN + repli « détail seulement »
8 +# quand l'API liste échoue (maintenances nocturnes ADP)
9 +# Créé : 2026-08-25 Modifié : 2026-08-30
8 10 # =============================================================================
9 −from .adp import ADPWorkforceNowConnector
11 +"""Niobec (Magris Performance Materials) — ADP Workforce Now, avec repli.
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:52 (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 / mitsubishi_hc_capital.py /
23 +msi_gestion.py / ciena.py) : si la pagination liste échoue en 5xx ou en
24 +non-JSON, on revalide une à une les offres actives connues en base via l'API
25 +détail job-requisitions/<itemID> (qui renvoie la réquisition complète :
26 +titre, date, lieux, description). Aucune donnée inventée — tout vient de la
27 +réponse live. Une offre en 404 est considérée retirée. Si AUCUNE offre n'est
28 +validable (maintenance totale : le détail redirige aussi vers
29 +sorry.adp.com), l'erreur d'origine est propagée pour que le sync reste un
30 +échec (inventaire préservé). Limite assumée : le repli ne découvre pas les
31 +NOUVELLES offres — le chemin liste normal reprend seul au cycle suivant la
32 +fin de la maintenance.
33 +"""
34 +from __future__ import annotations
35 +
36 +import requests
37 +
38 +from ..schema import JobPosting, clean_html
39 +from .adp import _API, ADPWorkforceNowConnector
10 40
11 41
12 42 class NiobecConnector(ADPWorkforceNowConnector):
@@ -15,3 +45,85 @@ class NiobecConnector(ADPWorkforceNowConnector):
15 45 CID = "099e24b7-1868-4283-a5d0-994ec5a80995"
16 46 CCID = "19000101_000001"
17 47 FORCE_CITY = "Saint-Honoré" # mine mono-site Saint-Honoré (Saguenay) : l'API n'expose aucune localisation
48 +
49 + def fetch(self) -> list[JobPosting]:
50 + try:
51 + return super().fetch()
52 + except (requests.RequestException, ValueError) as exc:
53 + resp = getattr(exc, "response", None)
54 + if resp is not None and resp.status_code < 500:
55 + raise # 4xx franc : pas une panne liste, ne pas masquer
56 + jobs = self._fetch_known_jobs()
57 + if jobs is None:
58 + raise # API détail cassée aussi : vraie panne totale
59 + return jobs
60 +
61 + def _known_ids(self) -> list[str]:
62 + """itemID (external_id) des offres actives de la source en base."""
63 + from .. import db
64 + con = db.connect()
65 + try:
66 + rows = con.execute(
67 + "SELECT external_id FROM jobs WHERE source=? AND active=1",
68 + (self.source_id,)).fetchall()
69 + finally:
70 + con.close()
71 + return [str(eid) for (eid,) in rows if eid]
72 +
73 + def _fetch_known_jobs(self) -> list[JobPosting] | None:
74 + """Repli sans liste : chaque offre connue est revalidée via l'API
75 + détail (la réquisition complète, source de vérité de son existence).
76 + Aucune donnée inventée — tout vient de la réponse du moment."""
77 + out: list[JobPosting] = []
78 + errors = 0
79 + for iid in self._known_ids():
80 + try:
81 + r = self.get(f"{_API}/{iid}", params=self._params(),
82 + headers={"Accept": "application/json"}).json()
83 + except requests.HTTPError as exc:
84 + h = getattr(exc, "response", None)
85 + if h is not None and h.status_code == 404:
86 + continue # offre retirée chez l'employeur
87 + errors += 1
88 + continue
89 + except (requests.RequestException, ValueError):
90 + errors += 1 # non-JSON (maintenance) ou réseau
91 + continue
92 + if not (r or {}).get("requisitionTitle"):
93 + errors += 1 # réponse inattendue : ne pas conclure au retrait
94 + continue
95 + locations = self._locations(r)
96 + if not self._keep(locations):
97 + continue
98 + qc = next((l for l in locations
99 + if l["prov"].upper() == "QC"),
100 + locations[0] if locations else
101 + {"city": self.FORCE_CITY, "postal": "",
102 + "label": self.FORCE_CITY})
103 + job = JobPosting(
104 + source=self.source_id, external_id=iid,
105 + url=("https://workforcenow.adp.com/mascsr/default/mdf/"
106 + f"recruitment/recruitment.html?cid={self.CID}"
107 + f"&ccId={self.CCID}&lang={self.LANG}&jobId={iid}"),
108 + employer=self.EMPLOYER,
109 + title=r.get("requisitionTitle") or "",
110 + city=qc["city"], postal_code=qc["postal"],
111 + location_label=qc["label"],
112 + date_posted=r.get("postDate") or None,
113 + ats=self.ats,
114 + )
115 + pay = r.get("payGradeRange") or {}
116 + lo = (pay.get("minimumRate") or {}).get("amountValue")
117 + hi = (pay.get("maximumRate") or {}).get("amountValue")
118 + if lo:
119 + job.salary_min = float(lo)
120 + job.salary_max = float(hi) if hi else None
121 + job.salary_unit = "year" if float(lo) > 5000 else "hour"
122 + wl = (r.get("workLevelCode") or {}).get("shortName")
123 + if wl:
124 + job.details["employment_label"] = wl
125 + job.description = clean_html(r.get("requisitionDescription") or "")
126 + out.append(job)
127 + if not out and errors:
128 + return None # rien de validable : laisser le sync échouer
129 + return out
18 130