SPB Git forge

spb/job-ka

Public
226commits 1branches 0releases
37.5 MBsize
maindefault branch
9 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
8.2 KB · 214 lines python
Raw Blame History
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