SPB Git

spb/job-ka

Public

Nouvelle famille wpjobmanager (WordPress + WP Job Manager) : flux job_feed (contenu complet en une requête) + JSON-LD des pages poste (date limite, lieu). Ouvre la plateforme carrieresante.gouv.qc.ca de Santé Québec (un site par établissement, en déploiement) : CHU de Québec (60), CHUM (25), CISSS Abitibi-Témiscamingue (141), Chaudière-Appalaches (112), Laval (149), Lanaudière (124), Laurentides (130) + CISSS de l'Outaouais via viensvivretespassions.ca (101). Fixture chum. + connecteur cssdd (CSS des Découvreurs, famille DigitalRecruiters existante).

Simon-Pierre Boucher committed (Aug 19, 2026) parent 89f9713 Browse files

15 changed files +20,001 −0

added jobka/connectors/chu_quebec.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/chu_quebec.py
6 +# Rôle : Connecteur CHU de Québec — Université Laval (plateforme
7 +# carrieresante.gouv.qc.ca, WordPress + WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CHUQuebecConnector(WPJobManagerConnector):
14 + source_id = "chu_quebec"
15 + EMPLOYER = "CHU de Québec — Université Laval"
16 + BASE = "https://chudequebec.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Québec"
added jobka/connectors/chum.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/chum.py
6 +# Rôle : Connecteur CHUM (Centre hospitalier de l'Université de Montréal)
7 +# — plateforme carrieresante.gouv.qc.ca (WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CHUMConnector(WPJobManagerConnector):
14 + source_id = "chum"
15 + EMPLOYER = "CHUM"
16 + BASE = "https://chum.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Montréal"
added jobka/connectors/cisss_abitibi.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cisss_abitibi.py
6 +# Rôle : Connecteur CISSS de l'Abitibi-Témiscamingue — plateforme
7 +# carrieresante.gouv.qc.ca (WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CISSSAbitibiConnector(WPJobManagerConnector):
14 + source_id = "cisss_abitibi"
15 + EMPLOYER = "CISSS de l'Abitibi-Témiscamingue"
16 + BASE = "https://cisssat.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Rouyn-Noranda"
added jobka/connectors/cisss_chaudiere_appalaches.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cisss_chaudiere_appalaches.py
6 +# Rôle : Connecteur CISSS de Chaudière-Appalaches — plateforme
7 +# carrieresante.gouv.qc.ca (WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CISSSChaudiereAppalachesConnector(WPJobManagerConnector):
14 + source_id = "cisss_chaudiere_appalaches"
15 + EMPLOYER = "CISSS de Chaudière-Appalaches"
16 + BASE = "https://cisssca.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Lévis"
added jobka/connectors/cisss_lanaudiere.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cisss_lanaudiere.py
6 +# Rôle : Connecteur CISSS de Lanaudière — plateforme carrieresante.gouv.qc.ca
7 +# (WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CISSSLanaudiereConnector(WPJobManagerConnector):
14 + source_id = "cisss_lanaudiere"
15 + EMPLOYER = "CISSS de Lanaudière"
16 + BASE = "https://lanaudiere.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Joliette"
added jobka/connectors/cisss_laurentides.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cisss_laurentides.py
6 +# Rôle : Connecteur CISSS des Laurentides — plateforme
7 +# carrieresante.gouv.qc.ca (WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CISSSLaurentidesConnector(WPJobManagerConnector):
14 + source_id = "cisss_laurentides"
15 + EMPLOYER = "CISSS des Laurentides"
16 + BASE = "https://laurentides.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Saint-Jérôme"
added jobka/connectors/cisss_laval.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cisss_laval.py
6 +# Rôle : Connecteur CISSS de Laval — plateforme carrieresante.gouv.qc.ca
7 +# (WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CISSSLavalConnector(WPJobManagerConnector):
14 + source_id = "cisss_laval"
15 + EMPLOYER = "CISSS de Laval"
16 + BASE = "https://cissslaval.carrieresante.gouv.qc.ca"
17 + DEFAULT_CITY = "Laval"
added jobka/connectors/cisss_outaouais.py +17 −0
@@ -0,0 +1,17 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cisss_outaouais.py
6 +# Rôle : Connecteur CISSS de l'Outaouais — site de recrutement
7 +# viensvivretespassions.ca (WordPress + WP Job Manager)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .wpjobmanager import WPJobManagerConnector
11 +
12 +
13 +class CISSSOutaouaisConnector(WPJobManagerConnector):
14 + source_id = "cisss_outaouais"
15 + EMPLOYER = "CISSS de l'Outaouais"
16 + BASE = "https://viensvivretespassions.ca"
17 + DEFAULT_CITY = "Gatineau"
added jobka/connectors/cssdd.py +16 −0
@@ -0,0 +1,16 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/cssdd.py
6 +# Rôle : Connecteur Centre de services scolaire des Découvreurs (Québec)
7 +# — Cegid Digital Recruiters (emplois.cssdd.gouv.qc.ca)
8 +# Créé : 2026-08-19 Modifié : 2026-08-19
9 +# =============================================================================
10 +from .digitalrecruiters import DigitalRecruitersConnector
11 +
12 +
13 +class CSSDDConnector(DigitalRecruitersConnector):
14 + source_id = "cssdd"
15 + EMPLOYER = "Centre de services scolaire des Découvreurs"
16 + BASE = "https://emplois.cssdd.gouv.qc.ca"
added jobka/connectors/wpjobmanager.py +125 −0
@@ -0,0 +1,125 @@
1 +# =============================================================================
2 +# Job·Ka — Groupe KA
3 +# Auteur : Simon-Pierre Boucher
4 +# Contact : contact@spboucher.ai
5 +# Fichier : jobka/connectors/wpjobmanager.py
6 +# Rôle : Classe de plateforme WordPress + WP Job Manager — flux job_feed
7 +# (contenu complet) + JSON-LD des pages poste (lieu, date limite).
8 +# Utilisée par la plateforme « carrieresante.gouv.qc.ca » du réseau
9 +# de la santé (un site WordPress par établissement Santé Québec).
10 +# Créé : 2026-08-19 Modifié : 2026-08-19
11 +# =============================================================================
12 +"""Plateforme WordPress / WP Job Manager.
13 +
14 +- liste : GET <BASE>/?feed=job_feed&posts_per_page=500
15 + -> RSS : <item> title / link (/poste/<slug>/) / pubDate /
16 + guid (?post_type=job_listing&p=<id>) / content:encoded (HTML
17 + complet de l'offre) — tout en une requête.
18 +- détail : la page /poste/<slug>/ embarque un JSON-LD schema.org/JobPosting
19 + (validThrough, jobLocation, employmentType) — cache BD + budget,
20 + uniquement pour compléter lieu et date limite.
21 +"""
22 +from __future__ import annotations
23 +
24 +import html as _html
25 +import os
26 +import re
27 +
28 +from ..schema import JobPosting, clean_html
29 +from . import _jsonld
30 +from .base import BaseConnector
31 +
32 +MAX_DETAILS = int(os.environ.get("JOBKA_WPJM_DETAIL_LIMIT", "60"))
33 +
34 +_ITEM_RE = re.compile(r"<item>(.*?)</item>", re.S | re.I)
35 +_TAG_RE = {t: re.compile(rf"<{t}>(.*?)</{t}>", re.S | re.I)
36 + for t in ("title", "link", "pubDate", "guid")}
37 +_CONTENT_RE = re.compile(
38 + r"<content:encoded><!\[CDATA\[(.*?)\]\]></content:encoded>", re.S | re.I)
39 +_PID_RE = re.compile(r"[?&](?:amp;)?p=(\d+)")
40 +
41 +
42 +class WPJobManagerConnector(BaseConnector):
43 + """Base WP Job Manager — sous-classes : définir source_id, EMPLOYER,
44 + BASE (racine du site WordPress) et DEFAULT_CITY au besoin."""
45 +
46 + ats = "wpjobmanager"
47 + request_delay = 1.0
48 +
49 + EMPLOYER = ""
50 + BASE = ""
51 + DEFAULT_CITY = ""
52 + PER_PAGE = 500
53 +
54 + def _cdata(self, s: str) -> str:
55 + s = (s or "").strip()
56 + if s.startswith("<![CDATA["):
57 + s = s[9:]
58 + if s.endswith("]]>"):
59 + s = s[:-3]
60 + return _html.unescape(s.strip())
61 +
62 + def _fetch_detail(self, url: str) -> dict:
63 + html = self.get(url).text
64 + node = _jsonld.extract_jobposting(html)
65 + return _jsonld.jobposting_fields(node) if node else {}
66 +
67 + def fetch(self) -> list[JobPosting]:
68 + xml = self.get(f"{self.BASE}/",
69 + params={"feed": "job_feed",
70 + "posts_per_page": str(self.PER_PAGE)}).text
71 + out: list[JobPosting] = []
72 + details_used = 0
73 + seen: set[str] = set()
74 + for raw in _ITEM_RE.findall(xml):
75 + def g(tag: str, raw=raw) -> str:
76 + m = _TAG_RE[tag].search(raw)
77 + return self._cdata(m.group(1)) if m else ""
78 + link, guid = g("link"), g("guid")
79 + m_id = _PID_RE.search(_html.unescape(guid))
80 + eid = m_id.group(1) if m_id else ""
81 + title = g("title")
82 + if not link or not title:
83 + continue
84 + if not eid:
85 + m_slug = re.search(r"/poste/([^/]+)/?$", link)
86 + eid = m_slug.group(1) if m_slug else ""
87 + if not eid or eid in seen:
88 + continue
89 + seen.add(eid)
90 + m_c = _CONTENT_RE.search(raw)
91 + job = JobPosting(
92 + source=self.source_id, external_id=eid, url=link,
93 + employer=self.EMPLOYER, title=title,
94 + description=clean_html(_html.unescape(m_c.group(1)))
95 + if m_c else "",
96 + city=self.DEFAULT_CITY,
97 + date_posted=g("pubDate") or None,
98 + ats=self.ats,
99 + )
100 + key = f"{title}|{job.date_posted or ''}"
101 + if details_used < MAX_DETAILS:
102 + fresh = [False]
103 +
104 + def _fn(u=link, fresh=fresh):
105 + fresh[0] = True
106 + return self._fetch_detail(u)
107 +
108 + fields = self.detail(eid, key, _fn)
109 + if fresh[0]:
110 + details_used += 1
111 + else:
112 + fields = self.stale_detail(eid)
113 + if fields:
114 + if fields.get("date_deadline"):
115 + job.date_deadline = fields["date_deadline"]
116 + if fields.get("city"):
117 + job.city = fields["city"]
118 + if fields.get("postal_code"):
119 + job.postal_code = fields["postal_code"]
120 + if fields.get("employment_label"):
121 + job.details["employment_label"] = fields["employment_label"]
122 + if fields.get("lat") is not None:
123 + job.lat, job.lng = fields["lat"], fields["lng"]
124 + out.append(job)
125 + return out
added tests/fixtures/chum/03ada5dc2047d09bb50f.html +4162 −0
Large diff (4,162 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/chum/11c79adc26615ea860b9.html +4178 −0
Large diff (4,178 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/chum/171380ec67cdfd7bdb17.html +4135 −0
Large diff (4,135 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/chum/1f471636b44482e7722b.html +4137 −0
Large diff (4,137 lines) is not loaded by default to keep the page fast.Load diff
added tests/fixtures/chum/210bc7e9274ae521ae25.html +3112 −0
Large diff (3,112 lines) is not loaded by default to keep the page fast.Load diff

Diff truncated — file too large.

Menu

Theme