HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1#!/usr/bin/env python32# =============================================================================3# Job·Ka — Groupe KA4# Auteur : Simon-Pierre Boucher5# Contact : contact@spboucher.ai6# Fichier : scripts/gen_connectors.py7# Rôle : Génération en série des connecteurs employeurs à partir d'un8# fichier de flux vérifiés (data/feeds.json) + MAJ de sources.json9# Créé : 2026-08-17 Modifié : 2026-08-2210# =============================================================================11"""Génère un connecteur (~15 lignes) par flux ATS vérifié.1213Usage :14 python3 scripts/gen_connectors.py data/feeds.json1516Format d'entrée (liste JSON) — champs selon l'ATS :17 {"ats":"workday","tenant":"x","host":"wd3","site":"Site","employer":"Nom",18 "qc":N, "cities":[...]}19 {"ats":"lever"|"greenhouse"|"smartrecruiters"|"ashby"|"workable"|20 "recruitee"|"breezy"|"bamboohr","org":"slug","employer":"Nom", ...}2122Idempotent : un connecteur existant n'est PAS réécrit (préserve les23ajustements manuels) ; sources.json est fusionné par id.24"""25from __future__ import annotations2627import datetime28import json29import re30import sys31import unicodedata32from pathlib import Path3334ROOT = Path(__file__).resolve().parent.parent35CONN_DIR = ROOT / "jobka" / "connectors"36SOURCES = ROOT / "data" / "sources.json"3738TODAY = datetime.date.today().isoformat()3940# ats -> (module de plateforme, classe de base, attribut(s) d'org)41PLATFORMS = {42 "workday": ("workday", "WorkdayConnector"),43 "lever": ("lever", "LeverConnector"),44 "greenhouse": ("greenhouse", "GreenhouseConnector"),45 "smartrecruiters": ("smartrecruiters", "SmartRecruitersConnector"),46 "ashby": ("ashby", "AshbyConnector"),47 "workable": ("workable", "WorkableConnector"),48 "recruitee": ("recruitee", "RecruiteeConnector"),49 "breezy": ("breezy", "BreezyConnector"),50 "bamboohr": ("bamboohr", "BambooHRConnector"),51}5253ORG_ATTR = {"lever": "ORG", "greenhouse": "BOARD",54 "smartrecruiters": "COMPANY", "ashby": "ORG", "workable": "ORG",55 "recruitee": "ORG", "breezy": "ORG", "bamboohr": "ORG"}5657# ATS à attributs multiples (secteur public/parapublic et grands employeurs) :58# feed[clé] -> attribut de classe. Voir data/feeds-public.json.59EXTRA_ATTRS = {60 "taleo": [("host", "HOST"), ("section", "SECTION"), ("portal", "PORTAL")],61 "njoyn": [("base", "BASE"), ("cl", "CL"), ("clid", "CLID"),62 ("use_scrapfly", "USE_SCRAPFLY")],63 "ultipro": [("org", "ORG"), ("board", "BOARD")],64 "icims": [("sub", "SUB")],65 "successfactors": [("base", "BASE")],66 "adp": [("cid", "CID"), ("ccid", "CCID")],67 "digitalrecruiters": [("base", "BASE")],68 "workland": [("list_url", "LIST_URL")],69 "dayforce": [("ns", "NS"), ("board", "BOARD")],70}7172CAREERS_URL = {73 "workday": "https://{tenant}.{host}.myworkdayjobs.com/{site}",74 "lever": "https://jobs.lever.co/{org}",75 "greenhouse": "https://job-boards.greenhouse.io/{org}",76 "smartrecruiters": "https://jobs.smartrecruiters.com/company/{org}",77 "ashby": "https://jobs.ashbyhq.com/{org}",78 "workable": "https://apply.workable.com/{org}",79 "recruitee": "https://{org}.recruitee.com",80 "breezy": "https://{org}.breezy.hr",81 "bamboohr": "https://{org}.bamboohr.com/careers",82 "taleo": "https://{host}.taleo.net/careersection/{section}/jobsearch.ftl?lang=fr",83 "njoyn": "{base}/{cl}/xweb/xweb.asp?clid={clid}&page=joblisting&lang=2",84 "ultipro": "https://recruiting.ultipro.com/{org}/JobBoard/{board}",85 "icims": "https://{sub}.icims.com/jobs/search?ss=1",86 "successfactors": "{base}",87 "adp": ("https://workforcenow.adp.com/mascsr/default/mdf/recruitment/"88 "recruitment.html?cid={cid}&ccId={ccid}&lang=fr_CA"),89 "digitalrecruiters": "{base}",90 "workland": "{list_url}",91 "dayforce": "https://jobs.dayforcehcm.com/fr-CA/{ns}/{board}",92}9394PLATFORMS.update({95 "taleo": ("taleo", "TaleoConnector"),96 "njoyn": ("njoyn", "NjoynConnector"),97 "ultipro": ("ultipro", "UltiProConnector"),98 "icims": ("icims", "ICIMSConnector"),99 "successfactors": ("successfactors", "SuccessFactorsConnector"),100 "adp": ("adp", "ADPWorkforceNowConnector"),101 "digitalrecruiters": ("digitalrecruiters", "DigitalRecruitersConnector"),102 "workland": ("workland", "WorklandConnector"),103 "dayforce": ("dayforce", "DayforceConnector"),104})105106107def slugify(s: str) -> str:108 s = unicodedata.normalize("NFD", s)109 s = "".join(c for c in s if unicodedata.category(c) != "Mn")110 s = re.sub(r"[^A-Za-z0-9]+", "_", s.lower()).strip("_")111 s = re.sub(r"_+", "_", s)112 # un module/identifiant Python ne peut pas commencer par un chiffre113 return f"x{s}" if s and s[0].isdigit() else s114115116def camel(s: str) -> str:117 return "".join(p.capitalize() for p in s.split("_"))118119120HEADER = """\121# =============================================================================122# Job·Ka — Groupe KA123# Auteur : Simon-Pierre Boucher124# Contact : contact@spboucher.ai125# Fichier : jobka/connectors/{fname}126# Rôle : Connecteur {employer} — {ats} ({detail})127# [généré par scripts/gen_connectors.py, flux vérifié le {today}]128# Créé : {today} Modifié : {today}129# =============================================================================130"""131132133def gen_one(feed: dict) -> tuple[str, str] | None:134 ats = feed["ats"]135 module, base = PLATFORMS[ats]136 sid = feed.get("source_id") or slugify(feed.get("org")137 or feed.get("tenant")138 or feed["employer"])139 fname = f"{sid}.py"140 path = CONN_DIR / fname141 if path.exists():142 return None # idempotent : ne pas écraser un connecteur existant143 cls = camel(sid) + "Connector"144145 if ats == "workday":146 detail = f"tenant {feed['tenant']}.{feed['host']}, site {feed['site']}"147 body = (f" TENANT = {feed['tenant']!r}\n"148 f" HOST = {feed['host']!r}\n"149 f" SITE = {feed['site']!r}\n")150 elif ats in EXTRA_ATTRS:151 pairs = [(k, a) for k, a in EXTRA_ATTRS[ats]152 if feed.get(k) not in (None, "")]153 detail = ", ".join(f"{a.lower()}={feed[k]!r}" for k, a in pairs[:2])154 body = "".join(f" {a} = {feed[k]!r}\n" for k, a in pairs)155 else:156 attr = ORG_ATTR[ats]157 detail = f"org « {feed['org']} »"158 body = f" {attr} = {feed['org']!r}\n"159160 src = (HEADER.format(fname=fname, employer=feed["employer"], ats=ats,161 detail=detail, today=TODAY)162 + f"from .{module} import {base}\n\n\n"163 + f"class {cls}({base}):\n"164 + f" source_id = {sid!r}\n"165 + f" EMPLOYER = {feed['employer']!r}\n"166 + body)167 path.write_text(src, encoding="utf-8")168 return sid, fname169170171def careers_url(feed: dict) -> str:172 return CAREERS_URL[feed["ats"]].format(**feed)173174175def main() -> None:176 feeds = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))177 reg = json.loads(SOURCES.read_text(encoding="utf-8"))178 known = {s["id"] for s in reg["sources"]}179180 created = skipped = 0181 for feed in feeds:182 sid = feed.get("source_id") or slugify(feed.get("org")183 or feed.get("tenant")184 or feed["employer"])185 feed["source_id"] = sid186 res = gen_one(feed)187 if res:188 created += 1189 print(f" + {res[1]}")190 else:191 skipped += 1192 if sid not in known:193 reg["sources"].append({194 "id": sid,195 "name": feed["employer"],196 "url": feed.get("url", ""),197 "careers_url": careers_url(feed),198 "sectors": feed.get("sectors", []),199 "connector": feed["ats"],200 "status": "actif",201 "region": ", ".join((feed.get("cities") or [])[:3]),202 })203 known.add(sid)204205 reg["_meta"]["updated"] = TODAY206 SOURCES.write_text(json.dumps(reg, ensure_ascii=False, indent=2) + "\n",207 encoding="utf-8")208 print(f"[gen] {created} connecteurs créés, {skipped} déjà présents, "209 f"{len(reg['sources'])} sources au registre")210211212if __name__ == "__main__":213 main()214