Aller au contenu
mouaad.sek:~$
Tous les dossiers~/work/cf-03$
CF-03Produit · AutomatisationSTATUS: TOURNE CHAQUE HEURE

JobPilot

Un seul système local remplace une dizaine de sites d’emploi, d’alertes mail et de CV copiés-collés, avec plus de 2 100 tests automatisés pour garder le cap.

RÔLE
Seul, produit personnel
QUAND
2026
STACK
Python, Typer, FastAPI, SQLite, APScheduler, httpx, Playwright, sentence-transformers, Electron shell
LIEN
Repo privé, démo sur demande
01 / CONTEXTE

Le problème

Chercher un stage en France, c’est surveiller France Travail, Welcome to the Jungle, HelloWork, La Bonne Alternance, et les alertes LinkedIn et Indeed. La même offre apparaît quatre fois avec des formulations différentes, et chaque candidature demande son propre CV.

JobPilot en fait un pipeline : des sources interchangeables, un seul modèle de données, une file de revue unique, et des documents générés pour chaque offre.

02 / APPROCHE

De 8 types de sources à une seule file de revue

  1. 01Sources
  2. 02Normalisation
  3. 03Dédoublonnage
  4. 04Score
  5. 05File de revue
  6. 06CV adapté
03 / CHOIX TECHNIQUES

Choix techniques

CHOIXDOMAINEPOURQUOI
Ingestion idempotente sur hash du contenu et ID sourceDonnéesPas de doublons
Une seule fonction modifie les statuts, tracée en table d’auditÉtatTraçabilité
Alertes mail lues en IMAP, en lecture seuleSécuritéBoîte jamais modifiée
Les appels LLM ne journalisent que des métadonnéesSécuritéPas de fuite de secrets
Doublons détectés sur le texte des missions, pas le discours employeurMatchingVrai dédoublonnage
Chaque source implémente une même interface, testableArchitectureExtensible
04 / PREUVES

Directement depuis le code

Extraits du vrai dépôt et des livrables, raccourcis pour la lecture.

src/jobpilot/state.pypython
def transition(
    db: sqlite3.Connection,
    application_id: int,
    to_status: str,
    *,
    detail: dict | None = None,
) -> str:
    """Move an application to to_status if legal; log a status_change event."""
    frm = current_status(db, application_id)
    allowed = LEGAL.get(frm, set())
    if to_status not in allowed:
        raise IllegalTransition(
            f"application {application_id}: {frm} -> {to_status} not allowed "
            f"(legal: {sorted(allowed) or 'none, terminal'})"
        )

    now = _utc_now()
    db.execute(
        "UPDATE applications SET status = ?, last_event_at = ? WHERE id = ?",
        (to_status, now, application_id),
    )
    payload = {"from": frm, "to": to_status, **(detail or {})}
    log_event(db, application_id, "status_change", payload, commit=False)
    db.commit()
    return to_status

La seule fonction autorisée à modifier un statut : les transitions illégales lèvent une exception, et chaque changement est journalisé dans la même transaction.

05 / RÉSULTATS

Résultats

2 121tests automatisés
8types de connecteurs de sources
1hcycle de collecte, en tâche de fond
06 / RETOUR D’EXPÉRIENCE

Ce que je ferais autrement

Collecter des offres est facile ; savoir que deux annonces différentes sont le même poste, c’est le vrai problème. Prochaine étape : un tableau de bord web plus propre, et le moteur de matching en bibliothèque autonome.

DOSSIER SUIVANT · CF-04Site du restaurant L'Adress
Esc

↑↓ naviguer · Entrée ouvrir · Échap fermer