SPB Git

Repository Public

gen_connectors.py 8.0 KB 211 lines python

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-1710# =============================================================================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}7071CAREERS_URL = {72    "workday": "https://{tenant}.{host}.myworkdayjobs.com/{site}",73    "lever": "https://jobs.lever.co/{org}",74    "greenhouse": "https://job-boards.greenhouse.io/{org}",75    "smartrecruiters": "https://jobs.smartrecruiters.com/company/{org}",76    "ashby": "https://jobs.ashbyhq.com/{org}",77    "workable": "https://apply.workable.com/{org}",78    "recruitee": "https://{org}.recruitee.com",79    "breezy": "https://{org}.breezy.hr",80    "bamboohr": "https://{org}.bamboohr.com/careers",81    "taleo": "https://{host}.taleo.net/careersection/{section}/jobsearch.ftl?lang=fr",82    "njoyn": "{base}/{cl}/xweb/xweb.asp?clid={clid}&page=joblisting&lang=2",83    "ultipro": "https://recruiting.ultipro.com/{org}/JobBoard/{board}",84    "icims": "https://{sub}.icims.com/jobs/search?ss=1",85    "successfactors": "{base}",86    "adp": ("https://workforcenow.adp.com/mascsr/default/mdf/recruitment/"87            "recruitment.html?cid={cid}&ccId={ccid}&lang=fr_CA"),88    "digitalrecruiters": "{base}",89    "workland": "{list_url}",90}9192PLATFORMS.update({93    "taleo": ("taleo", "TaleoConnector"),94    "njoyn": ("njoyn", "NjoynConnector"),95    "ultipro": ("ultipro", "UltiProConnector"),96    "icims": ("icims", "ICIMSConnector"),97    "successfactors": ("successfactors", "SuccessFactorsConnector"),98    "adp": ("adp", "ADPWorkforceNowConnector"),99    "digitalrecruiters": ("digitalrecruiters", "DigitalRecruitersConnector"),100    "workland": ("workland", "WorklandConnector"),101})102103104def slugify(s: str) -> str:105    s = unicodedata.normalize("NFD", s)106    s = "".join(c for c in s if unicodedata.category(c) != "Mn")107    s = re.sub(r"[^A-Za-z0-9]+", "_", s.lower()).strip("_")108    s = re.sub(r"_+", "_", s)109    # un module/identifiant Python ne peut pas commencer par un chiffre110    return f"x{s}" if s and s[0].isdigit() else s111112113def camel(s: str) -> str:114    return "".join(p.capitalize() for p in s.split("_"))115116117HEADER = """\118# =============================================================================119# Job·Ka — Groupe KA120# Auteur  : Simon-Pierre Boucher121# Contact : contact@spboucher.ai122# Fichier : jobka/connectors/{fname}123# Rôle    : Connecteur {employer} — {ats} ({detail})124#           [généré par scripts/gen_connectors.py, flux vérifié le {today}]125# Créé    : {today}   Modifié : {today}126# =============================================================================127"""128129130def gen_one(feed: dict) -> tuple[str, str] | None:131    ats = feed["ats"]132    module, base = PLATFORMS[ats]133    sid = feed.get("source_id") or slugify(feed.get("org")134                                           or feed.get("tenant")135                                           or feed["employer"])136    fname = f"{sid}.py"137    path = CONN_DIR / fname138    if path.exists():139        return None    # idempotent : ne pas écraser un connecteur existant140    cls = camel(sid) + "Connector"141142    if ats == "workday":143        detail = f"tenant {feed['tenant']}.{feed['host']}, site {feed['site']}"144        body = (f"    TENANT = {feed['tenant']!r}\n"145                f"    HOST = {feed['host']!r}\n"146                f"    SITE = {feed['site']!r}\n")147    elif ats in EXTRA_ATTRS:148        pairs = [(k, a) for k, a in EXTRA_ATTRS[ats]149                 if feed.get(k) not in (None, "")]150        detail = ", ".join(f"{a.lower()}={feed[k]!r}" for k, a in pairs[:2])151        body = "".join(f"    {a} = {feed[k]!r}\n" for k, a in pairs)152    else:153        attr = ORG_ATTR[ats]154        detail = f"org « {feed['org']} »"155        body = f"    {attr} = {feed['org']!r}\n"156157    src = (HEADER.format(fname=fname, employer=feed["employer"], ats=ats,158                         detail=detail, today=TODAY)159           + f"from .{module} import {base}\n\n\n"160           + f"class {cls}({base}):\n"161           + f"    source_id = {sid!r}\n"162           + f"    EMPLOYER = {feed['employer']!r}\n"163           + body)164    path.write_text(src, encoding="utf-8")165    return sid, fname166167168def careers_url(feed: dict) -> str:169    return CAREERS_URL[feed["ats"]].format(**feed)170171172def main() -> None:173    feeds = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))174    reg = json.loads(SOURCES.read_text(encoding="utf-8"))175    known = {s["id"] for s in reg["sources"]}176177    created = skipped = 0178    for feed in feeds:179        sid = feed.get("source_id") or slugify(feed.get("org")180                                               or feed.get("tenant")181                                               or feed["employer"])182        feed["source_id"] = sid183        res = gen_one(feed)184        if res:185            created += 1186            print(f"  + {res[1]}")187        else:188            skipped += 1189        if sid not in known:190            reg["sources"].append({191                "id": sid,192                "name": feed["employer"],193                "url": feed.get("url", ""),194                "careers_url": careers_url(feed),195                "sectors": feed.get("sectors", []),196                "connector": feed["ats"],197                "status": "actif",198                "region": ", ".join((feed.get("cities") or [])[:3]),199            })200            known.add(sid)201202    reg["_meta"]["updated"] = TODAY203    SOURCES.write_text(json.dumps(reg, ensure_ascii=False, indent=2) + "\n",204                       encoding="utf-8")205    print(f"[gen] {created} connecteurs créés, {skipped} déjà présents, "206          f"{len(reg['sources'])} sources au registre")207208209if __name__ == "__main__":210    main()211
Files 89f9713f83129aec72fa1f63191f394d0fd83d08
Menu

Theme