HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/connectors/teamtailor.py6# Rôle : Classe de plateforme Teamtailor (<org>.teamtailor.com) — sitemap7# public + JSON-LD JobPosting des pages détail /jobs/<id>-<slug>8# Créé : 2026-08-25 Modifié : 2026-08-259# =============================================================================10"""Plateforme Teamtailor (sites carrières <org>.teamtailor.com).1112- liste : GET https://<org>.teamtailor.com/sitemap.xml13 -> URLs /jobs/<id>-<slug> (la page /jobs est rendue en JS)14- détail : GET /jobs/<id>-<slug> -> JSON-LD JobPosting (lieu, dates,15 description, salaire). Cache BD + budget ; filtre Québec au16 détail, offre différée si le budget est épuisé sans cache.17"""18from __future__ import annotations1920import os21import re2223from ..schema import JobPosting, is_quebec_location24from . import _jsonld25from .base import BaseConnector2627MAX_DETAILS = int(os.environ.get("JOBKA_TEAMTAILOR_DETAIL_LIMIT", "60"))2829_JOB_URL_RE = re.compile(30 r"<loc>\s*(https?://[a-z0-9-]+\.teamtailor\.com/jobs/(\d+)-([^<\s]+))\s*"31 r"</loc>", re.I)323334class TeamtailorConnector(BaseConnector):35 """Base Teamtailor — sous-classes : définir source_id, EMPLOYER, ORG."""3637 ats = "teamtailor"38 request_delay = 1.03940 EMPLOYER = ""41 ORG = "" # <ORG>.teamtailor.com42 quebec_only = True4344 def _fetch_detail(self, url: str) -> dict:45 html = self.get(url).text46 node = _jsonld.extract_jobposting(html)47 return _jsonld.jobposting_fields(node) if node else {}4849 def fetch(self) -> list[JobPosting]:50 xml = self.get(51 f"https://{self.ORG}.teamtailor.com/sitemap.xml").text52 out: list[JobPosting] = []53 details_used = 054 seen: set[str] = set()55 for url, eid, slug in _JOB_URL_RE.findall(xml):56 if eid in seen:57 continue58 seen.add(eid)59 if details_used >= MAX_DETAILS:60 d = self.stale_detail(eid)61 if not d:62 continue # différée : pas de détail frais ni en cache63 else:64 fresh = [False]6566 def _fn(u=url, fresh=fresh):67 fresh[0] = True68 return self._fetch_detail(u)6970 d = self.detail(eid, slug, _fn)71 if fresh[0]:72 details_used += 173 city = d.get("city") or ""74 region = (d.get("region_code") or "").upper()75 if self.quebec_only and not (76 region in ("QC", "QUÉBEC", "QUEBEC")77 or is_quebec_location(f"{city} {region}")):78 continue79 job = JobPosting(80 source=self.source_id, external_id=eid, url=url,81 employer=self.EMPLOYER,82 title=d.get("title") or slug.replace("-", " "),83 city=city, ats=self.ats,84 )85 _jsonld.apply_fields(job, d)86 out.append(job)87 return out88