SPB Git forge

spb/job-ka

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

taleo_tbe: secours og:title (retrait du JSON-LD par la plateforme) + fenêtre syncs_since_h

- Taleo TBE a retiré le JSON-LD JobPosting de requisition.jsp : les offres
  sans cache de détail étaient toutes différées par le filtre Québec →
  kruger publiait 0 offre depuis des jours (72 items RSS). Secours : lieu
  extrait du og:title (« … – Ville, RÉGION, PAYS »). Kruger retrouve 54
  offres QC ; sepaq/hec/exo tenaient encore grâce à leurs caches.
- base.detail() : ne met plus en cache les payloads vides ({} cadenassé par
  clé stable = connecteur gelé) ; 89 caches vides purgés de la base.
- /api/stats : paramètre optionnel syncs_since_h pour la supervision api-ka
  (fenêtre complète du sync_log au lieu des 20 dernières entrées).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 558d620

3 changed files +49 −7

modified jobka/connectors/base.py +7 −3
@@ -141,9 +141,13 @@ class BaseConnector:
141 141 if cached is not None:
142 142 return cached
143 143 payload = fetch_fn() or {}
144 − # détail « vide » (aucune description alors que le connecteur en
145 − # attendait une) : NE PAS le mettre en cache — il sera re-tenté au
146 − # prochain cycle au lieu de figer une fiche sans contenu
144 + # détail « vide » (payload sans aucun champ, ou aucune description
145 + # alors que le connecteur en attendait une) : NE PAS le mettre en
146 + # cache — il sera re-tenté au prochain cycle au lieu de figer une
147 + # fiche sans contenu (un {} cadenassé par clé stable avait gelé les
148 + # connecteurs Taleo TBE quand la plateforme a retiré son JSON-LD)
149 + if not payload:
150 + return payload
147 151 desc_keys = [k for k in ("description", "description_html")
148 152 if k in payload]
149 153 if desc_keys and not any((payload.get(k) or "").strip()
modified jobka/connectors/taleo_tbe.py +26 −1
@@ -42,6 +42,8 @@ _TAG_RE = {t: re.compile(rf"<{t}>(.*?)</{t}>", re.S | re.I)
42 42 for t in ("title", "link", "description")}
43 43 _RID_RE = re.compile(r"[?&]rid=(\d+)", re.I)
44 44 _QC_REGION = {"QC", "QUÉBEC", "QUEBEC"}
45 +_OG_TITLE_RE = re.compile(
46 + r'<meta\s+property="og:title"\s+content="([^"]*)"', re.I)
45 47
46 48
47 49 class TaleoTBEConnector(BaseConnector):
@@ -71,7 +73,30 @@ class TaleoTBEConnector(BaseConnector):
71 73 def _fetch_detail(self, rid: str) -> dict:
72 74 html = self.get(self._detail_url(rid)).text
73 75 node = _jsonld.extract_jobposting(html)
74 − return _jsonld.jobposting_fields(node) if node else {}
76 + if node:
77 + return _jsonld.jobposting_fields(node)
78 + return self._fields_from_og(html)
79 +
80 + @staticmethod
81 + def _fields_from_og(html: str) -> dict:
82 + """Secours quand le JSON-LD JobPosting est absent — retrait constaté
83 + sur toute la plateforme TBE le 2026-08-23 (les connecteurs TBE
84 + publiaient 0 offre, ex. Kruger : 72 items RSS, 0 retenu). Le lieu
85 + reste exposé dans og:title : « … – Ville, RÉGION, PAYS »."""
86 + m = _OG_TITLE_RE.search(html or "")
87 + if not m:
88 + return {}
89 + title = _html.unescape(m.group(1))
90 + if "–" not in title:
91 + return {}
92 + place = title.rsplit("–", 1)[1].strip()
93 + parts = [p.strip() for p in place.split(",") if p.strip()]
94 + if not parts:
95 + return {}
96 + fields: dict = {"city": parts[0]}
97 + if len(parts) >= 2:
98 + fields["region_code"] = parts[1].upper()
99 + return fields
75 100
76 101 def _is_qc(self, fields: dict) -> bool:
77 102 region = (fields.get("region_code") or "").upper()
modified jobka/web.py +16 −3
@@ -302,7 +302,7 @@ def sources():
302 302
303 303
304 304 @app.get("/api/stats")
305 −def stats():
305 +def stats(syncs_since_h: float | None = None):
306 306 con = db.connect()
307 307 row = con.execute(
308 308 """SELECT COUNT(*) total,
@@ -325,8 +325,21 @@ def stats():
325 325 """SELECT category, COUNT(*) n FROM jobs
326 326 WHERE active=1 AND dup_of IS NULL AND category<>''
327 327 GROUP BY category ORDER BY n DESC""")]
328 − log = [dict(r) for r in con.execute(
329 − "SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20")]
328 + # Fenêtre élargie pour la supervision (api-ka) : sans le paramètre, on
329 + # garde les 20 dernières entrées ; avec, toute l'activité de la fenêtre
330 + # (sinon le moniteur bi-horaire ne voit qu'une fraction de la rotation
331 + # des sources → fausses alertes « stale »).
332 + if syncs_since_h and syncs_since_h > 0:
333 + cutoff = time.time() - syncs_since_h * 3600
334 + log = [dict(r) for r in con.execute(
335 + "SELECT * FROM sync_log WHERE ts >= ? ORDER BY ts DESC LIMIT 4000",
336 + (cutoff,))]
337 + if len(log) < 20:
338 + log = [dict(r) for r in con.execute(
339 + "SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20")]
340 + else:
341 + log = [dict(r) for r in con.execute(
342 + "SELECT * FROM sync_log ORDER BY ts DESC LIMIT 20")]
330 343 # offres directes (pages carrières + dépôt direct) c. portails agrégateurs
331 344 agg = sorted(AGGREGATORS) or ["__aucun__"]
332 345 ph = ",".join("?" * len(agg))
333 346