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).
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.