SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
5 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
4.8 KB · 126 lines python
Raw Blame History
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur  : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/connectors/wpjobmanager.py6# Rôle    : Classe de plateforme WordPress + WP Job Manager — flux job_feed7#           (contenu complet) + JSON-LD des pages poste (lieu, date limite).8#           Utilisée par la plateforme « carrieresante.gouv.qc.ca » du réseau9#           de la santé (un site WordPress par établissement Santé Québec).10# Créé    : 2026-08-19   Modifié : 2026-08-1911# =============================================================================12"""Plateforme WordPress / WP Job Manager.1314- liste  : GET <BASE>/?feed=job_feed&posts_per_page=50015           -> RSS : <item> title / link (/poste/<slug>/) / pubDate /16              guid (?post_type=job_listing&p=<id>) / content:encoded (HTML17              complet de l'offre) — tout en une requête.18- détail : la page /poste/<slug>/ embarque un JSON-LD schema.org/JobPosting19           (validThrough, jobLocation, employmentType) — cache BD + budget,20           uniquement pour compléter lieu et date limite.21"""22from __future__ import annotations2324import html as _html25import os26import re2728from ..schema import JobPosting, clean_html29from . import _jsonld30from .base import BaseConnector3132MAX_DETAILS = int(os.environ.get("JOBKA_WPJM_DETAIL_LIMIT", "60"))3334_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+)")404142class 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."""4546    ats = "wpjobmanager"47    request_delay = 1.04849    EMPLOYER = ""50    BASE = ""51    DEFAULT_CITY = ""52    PER_PAGE = 5005354    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())6162    def _fetch_detail(self, url: str) -> dict:63        html = self.get(url).text64        node = _jsonld.extract_jobposting(html)65        return _jsonld.jobposting_fields(node) if node else {}6667    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)}).text71        out: list[JobPosting] = []72        details_used = 073        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                continue84            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                continue89            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]103104                def _fn(u=link, fresh=fresh):105                    fresh[0] = True106                    return self._fetch_detail(u)107108                fields = self.detail(eid, key, _fn)109                if fresh[0]:110                    details_used += 1111            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 out126