SPB Git forge

spb/job-ka

Public
227commits 1branches 0releases
37.7 MBsize
maindefault branch
28 min agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%

[ka6] fix connecteur eq3_ltd: retrait immédiat des offres CONFIRMÉES fermées — dans la nuit du 15 au 16/09, EQ3 a réellement fermé ses 12 postes (« Position Closed » vérifié live sur chacune des 12 pages /p/, plus aucun formulaire APPLY), étape suivante du délisting du board traité en 6012d1c ; le repli « board délisté » a fonctionné exactement comme conçu (re-vérification des 12 offres, rejet des pages fermées → found=0, volume désormais légitime), mais la garde anti-dérive du moteur (found=0 vs médiane 12 → retraits suspendus) maintenait les 12 offres fermées EN LIGNE sur le site (active=1, miss_count=0) pendant que la fenêtre de médiane (DRIFT_HISTORY=5) se vidait — correct pour un connecteur cassé, injustifié quand la fermeture est PROUVÉE page par page. Correctif tenant-spécifique dans eq3_ltd.py seul (db.py intact) : le repli collecte les uid des offres dont la page /p/ affiche « Position Closed » et les désactive directement (_deactivate_closed, UPDATE active=0), fermeture vérifiée à la source — les offres injoignables ou hors QC continuent de suivre le chemin normal miss_count. Vérifié live : la page carrières officielle eq3.com pointe toujours vers eq3-ltd.breezy.hr (pas de migration d'ATS), /json toujours [], validThrough des 12 offres tous dans le passé. Test unitaire adapté (offre « Position Closed » → retrait direct capturé, vivante conservée, hors-QC simplement lâchée). Sync réel rejoué : 12 offre(s) confirmée(s) fermée(s) — retirées, 0 offre active restante en base, found=0 légitime (le tenant n'a plus aucun poste ouvert ; dès republication le chemin normal /json reprend seul). 105 tests pytest verts. pm2 restart job-ka-sync OK, site 200.

Simon-Pierre Boucher committed 8 days ago (Sep 16, 2026) parent c7adc0c

2 changed files +52 −10

modified jobka/connectors/eq3_ltd.py +36 −3
@@ -16,7 +16,14 @@
16 16 # jamais inventées. Limite assumée : pas de découverte de NOUVELLES
17 17 # offres tant que le board est délisté ; dès que /json redevient
18 18 # non vide, le chemin normal reprend seul.
19 −# Créé : 2026-08-17 Modifié : 2026-09-15
19 +# + retrait immédiat des offres CONFIRMÉES fermées (2026-09-16) :
20 +# dans la nuit du 15 au 16, EQ3 a fermé ses 12 postes (« Position
21 +# Closed » vérifié live sur chaque page /p/). La garde anti-dérive
22 +# du moteur suspend les retraits quand found=0 vs médiane — correct
23 +# pour un connecteur cassé, mais ici la fermeture est PROUVÉE page
24 +# par page : ces offres sont donc désactivées directement, sans
25 +# attendre que la fenêtre de médiane se vide.
26 +# Créé : 2026-08-17 Modifié : 2026-09-16
20 27 # =============================================================================
21 28 from __future__ import annotations
22 29
@@ -51,7 +58,7 @@ class Eq3LtdConnector(BreezyConnector):
51 58 con = db.connect()
52 59 try:
53 60 rows = con.execute(
54 − "SELECT external_id, url, title, city, location_label,"
61 + "SELECT uid, external_id, url, title, city, location_label,"
55 62 " salary_label, date_posted, details, company_logo"
56 63 " FROM jobs WHERE source=? AND active=1 AND url<>''",
57 64 (self.source_id,)).fetchall()
@@ -59,6 +66,27 @@ class Eq3LtdConnector(BreezyConnector):
59 66 finally:
60 67 con.close()
61 68
69 + def _deactivate_closed(self, uids: list[str]) -> None:
70 + """Retire immédiatement les offres dont la page /p/ affiche
71 + « Position Closed » : fermeture VÉRIFIÉE à la source, pas une simple
72 + disparition de liste. Sans ce retrait direct, la garde anti-dérive du
73 + moteur (retraits suspendus quand found=0 vs médiane) maintiendrait en
74 + ligne des offres prouvées fermées pendant plusieurs cycles."""
75 + import time
76 +
77 + from .. import db
78 + con = db.connect()
79 + try:
80 + now = time.time()
81 + con.executemany(
82 + "UPDATE jobs SET active=0, updated_at=? WHERE uid=? AND active=1",
83 + [(now, uid) for uid in uids])
84 + con.commit()
85 + finally:
86 + con.close()
87 + print(f"[job-ka] eq3_ltd: {len(uids)} offre(s) confirmée(s) "
88 + f"fermée(s) sur leur page /p/ (« Position Closed ») — retirées")
89 +
62 90 def _fetch_board_delisted(self) -> list[JobPosting]:
63 91 rows = self._known_active_rows()
64 92 if not rows:
@@ -66,6 +94,7 @@ class Eq3LtdConnector(BreezyConnector):
66 94 print(f"[job-ka] eq3_ltd: board Breezy délisté (/json vide) — "
67 95 f"re-vérification live de {len(rows)} offre(s) connue(s)")
68 96 out: list[JobPosting] = []
97 + closed_uids: list[str] = []
69 98 for row in rows:
70 99 try:
71 100 html = self.get(row["url"]).text
@@ -74,7 +103,9 @@ class Eq3LtdConnector(BreezyConnector):
74 103 # le chemin normal miss_count -> retrait après délai de grâce
75 104 continue
76 105 if _CLOSED_RE.search(html):
77 − continue # réellement fermée : on la lâche
106 + # réellement fermée (constaté sur la page) : retrait direct
107 + closed_uids.append(row["uid"])
108 + continue
78 109 ld = None
79 110 for m in _LD_RE.finditer(html):
80 111 try:
@@ -116,4 +147,6 @@ class Eq3LtdConnector(BreezyConnector):
116 147 if details.get(key):
117 148 job.details[key] = details[key]
118 149 out.append(job)
150 + if closed_uids:
151 + self._deactivate_closed(closed_uids)
119 152 return out
modified tests/test_connectors.py +16 −7
@@ -5,7 +5,7 @@
5 5 # Fichier : tests/test_connectors.py
6 6 # Rôle : Tests de régression des connecteurs (rejeu des fixtures hors ligne)
7 7 # Ré-enregistrer après un changement de site : run.py record <source>
8 −# Créé : 2026-08-17 Modifié : 2026-09-15
8 +# Créé : 2026-08-17 Modifié : 2026-09-16
9 9 # =============================================================================
10 10 import json
11 11 import re
@@ -233,8 +233,9 @@ def test_eq3_ltd_delisted_board_fallback(monkeypatch):
233 233 vide) alors que les pages /p/ des offres restaient en ligne et ouvertes.
234 234 Quand la liste est vide, le connecteur re-vérifie chaque offre active
235 235 connue via sa page : une page vivante avec JSON-LD QC est conservée,
236 − une page « Position Closed » ou hors Québec est lâchée ; et une liste
237 − non vide ne déclenche jamais le repli."""
236 + une page « Position Closed » (fermeture vérifiée → retrait direct en
237 + base, malgré la garde anti-dérive) ou hors Québec est lâchée ; et une
238 + liste non vide ne déclenche jamais le repli."""
238 239 from jobka.connectors.eq3_ltd import Eq3LtdConnector
239 240
240 241 class _Resp:
@@ -261,16 +262,19 @@ def test_eq3_ltd_delisted_board_fallback(monkeypatch):
261 262 "addressRegion": region, "addressLocality": city}}}
262 263
263 264 rows = [
264 − {"external_id": "aaa111", "url": "https://x/p/aaa111-vivante",
265 + {"uid": "eq3_ltd:aaa111", "external_id": "aaa111",
266 + "url": "https://x/p/aaa111-vivante",
265 267 "title": "Associé(e) aux ventes", "city": "Brossard",
266 268 "location_label": "Brossard, Quebec", "salary_label": "18 $ à 25 $/h",
267 269 "date_posted": "2026-08-26", "details": '{"employment_label": '
268 270 '"Temps plein"}', "company_logo": ""},
269 − {"external_id": "bbb222", "url": "https://x/p/bbb222-fermee",
271 + {"uid": "eq3_ltd:bbb222", "external_id": "bbb222",
272 + "url": "https://x/p/bbb222-fermee",
270 273 "title": "Fermée", "city": "Laval", "location_label": "Laval",
271 274 "salary_label": "", "date_posted": None, "details": "{}",
272 275 "company_logo": ""},
273 − {"external_id": "ccc333", "url": "https://x/p/ccc333-denver",
276 + {"uid": "eq3_ltd:ccc333", "external_id": "ccc333",
277 + "url": "https://x/p/ccc333-denver",
274 278 "title": "Hors QC", "city": "Denver", "location_label": "Denver, CO",
275 279 "salary_label": "", "date_posted": None, "details": "{}",
276 280 "company_logo": ""},
@@ -284,10 +288,14 @@ def test_eq3_ltd_delisted_board_fallback(monkeypatch):
284 288 }
285 289
286 290 # 1. /json vide → repli : seule l'offre vivante et québécoise survit,
287 − # le libellé salarial connu est conservé
291 + # le libellé salarial connu est conservé ; l'offre « Position Closed »
292 + # est retirée directement en base (fermeture vérifiée à la source),
293 + # l'offre hors QC est simplement lâchée (pas de preuve de fermeture)
288 294 conn = Eq3LtdConnector()
289 295 conn.request_delay = 0
296 + deactivated = []
290 297 monkeypatch.setattr(conn, "_known_active_rows", lambda: rows)
298 + monkeypatch.setattr(conn, "_deactivate_closed", deactivated.extend)
291 299 monkeypatch.setattr(conn, "get", lambda url, **kw: _Resp(
292 300 pages.get(url, [])))
293 301 out = conn.fetch()
@@ -295,6 +303,7 @@ def test_eq3_ltd_delisted_board_fallback(monkeypatch):
295 303 assert out[0].title == "Associé(e) aux ventes"
296 304 assert out[0].salary_label == "18 $ à 25 $/h"
297 305 assert out[0].details["employment_label"] == "Temps plein"
306 + assert deactivated == ["eq3_ltd:bbb222"]
298 307
299 308 # 2. liste non vide → chemin normal, le repli n'est jamais sollicité
300 309 conn2 = Eq3LtdConnector()
301 310