# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/connectors/wpjobmanager.py # Rôle : Classe de plateforme WordPress + WP Job Manager — flux job_feed # (contenu complet) + JSON-LD des pages poste (lieu, date limite). # Utilisée par la plateforme « carrieresante.gouv.qc.ca » du réseau # de la santé (un site WordPress par établissement Santé Québec). # Créé : 2026-08-19 Modifié : 2026-08-19 # ============================================================================= """Plateforme WordPress / WP Job Manager. - liste : GET /?feed=job_feed&posts_per_page=500 -> RSS : title / link (/poste//) / pubDate / guid (?post_type=job_listing&p=) / content:encoded (HTML complet de l'offre) — tout en une requête. - détail : la page /poste// embarque un JSON-LD schema.org/JobPosting (validThrough, jobLocation, employmentType) — cache BD + budget, uniquement pour compléter lieu et date limite. """ from __future__ import annotations import html as _html import os import re from ..schema import JobPosting, clean_html from . import _jsonld from .base import BaseConnector MAX_DETAILS = int(os.environ.get("JOBKA_WPJM_DETAIL_LIMIT", "60")) _ITEM_RE = re.compile(r"(.*?)", re.S | re.I) _TAG_RE = {t: re.compile(rf"<{t}>(.*?)", re.S | re.I) for t in ("title", "link", "pubDate", "guid")} _CONTENT_RE = re.compile( r"", re.S | re.I) _PID_RE = re.compile(r"[?&](?:amp;)?p=(\d+)") class WPJobManagerConnector(BaseConnector): """Base WP Job Manager — sous-classes : définir source_id, EMPLOYER, BASE (racine du site WordPress) et DEFAULT_CITY au besoin.""" ats = "wpjobmanager" request_delay = 1.0 EMPLOYER = "" BASE = "" DEFAULT_CITY = "" PER_PAGE = 500 def _cdata(self, s: str) -> str: s = (s or "").strip() if s.startswith(""): s = s[:-3] return _html.unescape(s.strip()) def _fetch_detail(self, url: str) -> dict: html = self.get(url).text node = _jsonld.extract_jobposting(html) return _jsonld.jobposting_fields(node) if node else {} def fetch(self) -> list[JobPosting]: xml = self.get(f"{self.BASE}/", params={"feed": "job_feed", "posts_per_page": str(self.PER_PAGE)}).text out: list[JobPosting] = [] details_used = 0 seen: set[str] = set() for raw in _ITEM_RE.findall(xml): def g(tag: str, raw=raw) -> str: m = _TAG_RE[tag].search(raw) return self._cdata(m.group(1)) if m else "" link, guid = g("link"), g("guid") m_id = _PID_RE.search(_html.unescape(guid)) eid = m_id.group(1) if m_id else "" title = g("title") if not link or not title: continue if not eid: m_slug = re.search(r"/poste/([^/]+)/?$", link) eid = m_slug.group(1) if m_slug else "" if not eid or eid in seen: continue seen.add(eid) m_c = _CONTENT_RE.search(raw) job = JobPosting( source=self.source_id, external_id=eid, url=link, employer=self.EMPLOYER, title=title, description=clean_html(_html.unescape(m_c.group(1))) if m_c else "", city=self.DEFAULT_CITY, date_posted=g("pubDate") or None, ats=self.ats, ) key = f"{title}|{job.date_posted or ''}" if details_used < MAX_DETAILS: fresh = [False] def _fn(u=link, fresh=fresh): fresh[0] = True return self._fetch_detail(u) fields = self.detail(eid, key, _fn) if fresh[0]: details_used += 1 else: fields = self.stale_detail(eid) if fields: if fields.get("date_deadline"): job.date_deadline = fields["date_deadline"] if fields.get("city"): job.city = fields["city"] if fields.get("postal_code"): job.postal_code = fields["postal_code"] if fields.get("employment_label"): job.details["employment_label"] = fields["employment_label"] if fields.get("lat") is not None: job.lat, job.lng = fields["lat"], fields["lng"] out.append(job) return out