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.