SPB Git

spb/job-ka

Public
11.2 KB

# Job·Ka

# Tous les emplois des employeurs québécois. Un seul endroit.

www.job-ka.com

Job·Ka collecte les offres directement sur les pages carrières des employeurs du Québec — pas sur les job boards. Chaque offre est traçable à sa source, les salaires sont normalisés en $/h et $/an, et la candidature se fait chez l'employeur. Zéro boîte noire, zéro intermédiaire.


# Sommaire


# Pourquoi Job·Ka ?

Les grands sites d'emploi mélangent annonces payantes, doublons repostés et offres mortes. L'offre originale — celle de la page carrière de l'employeur — est la seule source fiable : à jour, complète, sans frais de mise en avant.

Job·Ka se branche directement sur les systèmes de recrutement (ATS) des employeurs : Workday, Lever, Greenhouse, SmartRecruiters, ou leur site maison. Un connecteur = un employeur. Le tout resynchronisé jour et nuit.

Ce n'est pas un job board qui vend de la visibilité, c'est un index fidèle de ce que les employeurs québécois affichent réellement — avec les salaires en clair quand ils existent (normalisés en $/h et $/an), parce que la transparence salariale est le différenciateur clé.

# L'architecture en 30 secondes

flowchart LR
  subgraph Sources
    W["Workday"] --> C
    L["Lever"] --> C
    G["Greenhouse"] --> C
    S["SmartRecruiters"] --> C
    X["Sites maison<br/>(HTML / Scrapfly)"] --> C
  end
  C["Connecteurs<br/>(auto-découverts)"] --> N["finalize()<br/>normalisation"]
  N --> D[("SQLite<br/>data/jobka.db")]
  W2["⏱ Watcher horaire<br/>(PM2)"] -.-> C
  D --> A["FastAPI<br/>/api/*"]
  A --> F["React + Vite<br/>frontend/dist"]
  D --> GEO["Géocodage<br/>Nominatim + Adresses Québec"]
  GEO --> D
Couche Rôle Fichiers
Connecteurs 1 employeur = 1 module ; plateformes ATS partagées jobka/connectors/
Schéma JobPosting + finalize() (normalisation commune) jobka/schema.py, jobka/normalize.py
Persistance diff par content_hash, cycle de vie, anti-dérive jobka/db.py
Pipeline ingestion, boucle watch, géocodage, dédup jobka/ingest.py, geocode.py, dedup.py
API + Web FastAPI + frontend Vite servi en statique jobka/web.py, frontend/
Tests fixtures HTTP enregistrées, rejeu hors ligne jobka/fixtures.py, tests/

# Le pipeline en détail

# Connecteurs auto-découverts

Toute sous-classe de BaseConnector avec un source_id est enregistrée par pkgutil — aucun registre à maintenir. Les plateformes ATS (Workday, Lever, Greenhouse, SmartRecruiters) sont des classes de base : brancher un nouvel employeur = ~10 lignes. Politesse : throttling par source (request_delay), User-Agent identifiable JobKaBot/1.0, budgets de requêtes, cache BD des pages détail.

# Diff engine avec garde-fou anti-dérive

Chaque sync compare le content_hash de chaque offre : ajout, mise à jour, ou disparition. Une offre absente 2 syncs consécutifs passe active=0 (jamais de DELETE). Si une source retourne ≤ 25 % de sa médiane historique, alerte et retraits suspendus — un connecteur cassé ne vide pas la base. Les offres dont la date_limite est dépassée sont retirées même si encore affichées.

# Salaires normalisés — jamais inventés

parse_salary() exige un signe monétaire ou une unité explicite, applique des bornes de plausibilité par unité, et convertit en $/h et $/an (40 h × 52 sem.). Une offre sans salaire reste sans salaire. L'historique des changements est conservé (salary_log).

# Géocodage sans invention

Nominatim (1 req/1,1 s) + repli officiel Adresses Québec (MERN, lots de 200), cache persistant par lieu normalisé, validation par bounding box provinciale. Sans adresse : centroïde de ville. Introuvable : geocode_failed=1, jamais de coordonnées bidon.

# Déduplication inter-sources

Blocage par clé exacte titre normalisé | employeur | ville + union-find avec exigence d'un second signal concordant (type, salaire ±4 %, date ±7 j). Le canonique est la source la plus autoritaire (page carrière avant agrégat) ; les doublons sont masqués (dup_of), jamais supprimés.

# Structure du dépôt

text
job-ka/
├── run.py                     # point d'entrée : sync | watch | serve | geocode | record
├── requirements.txt           # fastapi, uvicorn, requests, beautifulsoup4
├── jobka/
│   ├── schema.py              # dataclass JobPosting + finalize()
│   ├── normalize.py           # salaires, dates, mode/type, lieux, catégories
│   ├── db.py                  # SQLite : diff, cycle de vie, anti-dérive
│   ├── ingest.py              # run() / watch() — orchestration
│   ├── dedup.py               # déduplication inter-sources (union-find)
│   ├── geocode.py             # Nominatim + Adresses Québec + cache
│   ├── web.py                 # API FastAPI + /health + frontend statique
│   ├── fixtures.py            # enregistrement/rejeu HTTP pour les tests
│   └── connectors/
│       ├── base.py            # BaseConnector (throttling, Scrapfly, cache détail)
│       ├── workday.py         # plateforme Workday (facette Québec auto-découverte)
│       ├── lever.py           # plateforme Lever
│       ├── greenhouse.py      # plateforme Greenhouse
│       ├── smartrecruiters.py # plateforme SmartRecruiters
│       └── <employeur>.py     # 1 fichier = 1 employeur (sous-classe)
├── data/sources.json          # registre descriptif des employeurs
├── frontend/                  # React 18 + Vite + TS (carte MapLibre, sans jeton)
├── tests/                     # pytest : normalisation + rejeu des connecteurs
├── scripts/check_headers.py   # hook pre-commit : en-tête d'auteur obligatoire
└── docs/                      # audit Lou·Ka, déploiement

# Démarrage rapide

bash
git clone <repo> job-ka && cd job-ka

# Backend
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

# Frontend
cd frontend && npm install && npm run build && cd ..

# Secrets (Scrapfly pour les sites anti-bot — facultatif au départ)
cp .env.example .env   # puis remplir SCRAPFLY_API_KEY

# Amorçage : première synchronisation + géocodage
.venv/bin/python run.py sync
.venv/bin/python run.py geocode

# Service (API + frontend) — http://localhost:8096
.venv/bin/python run.py serve 8096

En dev frontend : npm run dev dans frontend/ (proxy /api → :8096).

# CLI run.py

Commande Rôle
run.py sync [source ...] synchronise les offres (toutes les sources ou celles données)
run.py watch [minutes] boucle : sync + géocodage + dédup (défaut 60 min)
run.py serve [port] API + frontend (défaut 8096)
run.py geocode [n] géocode les offres sans coordonnées
run.py record <source ...> enregistre les fixtures de test d'une source

# Variables d'environnement

Variable Rôle
SCRAPFLY_API_KEY rendu JS / anti-bot pour les pages carrières maison
JOBKA_WORKDAY_DETAIL_LIMIT plafond de pages détail Workday par sync (défaut 300)
JOBKA_SR_DETAIL_LIMIT plafond de pages détail SmartRecruiters (défaut 300)

# API

Route Rôle
GET /health healthcheck (BD + fraîcheur des syncs)
GET /api/jobs recherche : q, city, category, work_mode, employment_type, salary_min, with_salary, sort, limit, offset
GET /api/jobs.geojson offres géolocalisées (carte), bbox + mêmes filtres
GET /api/jobs/{uid} fiche complète + historique salarial
GET /api/facets valeurs distinctes pour les filtres
GET /api/sources registre des employeurs + fraîcheur par source
GET /api/stats totaux, catégories, villes, dernières syncs
POST /api/sync déclenche une synchronisation (webhook)

# Ajouter un employeur (≈ 10 lignes)

python
# jobka/connectors/mon_employeur.py  (+ en-tête d'auteur, règle nº 1)
from .workday import WorkdayConnector

class MonEmployeurConnector(WorkdayConnector):
    source_id = "mon_employeur"
    EMPLOYER = "Mon Employeur inc."
    TENANT = "monemployeur"     # monemployeur.wd3.myworkdayjobs.com
    HOST = "wd3"
    SITE = "Careers"

Puis : ajouter l'entrée dans data/sources.json, run.py sync mon_employeur pour valider, run.py record mon_employeur pour figer les fixtures de test.

# Tests

bash
.venv/bin/python -m pytest            # tout
.venv/bin/python -m pytest tests/test_normalize.py   # normalisation seule
  • test_normalize.py — salaires, dates, modes, types, lieux, catégories ;
  • test_connectors.py — rejeu hors ligne des fixtures : instantané exact + invariants (uid uniques, URLs absolues, salaires plausibles, valeurs jamais inventées) + finalize() sûr.

# Production

text
job-ka-web    .venv/bin/python run.py serve 8096     # API + frontend
job-ka-sync   .venv/bin/python run.py watch 60       # resync horaire, jour et nuit
job-ka-ngrok  ngrok http --url=www.job-ka.com 8096   # tunnel public

Philosophie d'exploitation : on ne pousse que le code — le serveur maintient ses données lui-même. data/*.db, frontend/dist/ et .env ne sont jamais versionnés. Détails : docs/deploiement.md.

# Principes

  1. Politesse — throttling par source, robots.txt respecté, User-Agent identifiable, budgets de requêtes, caches.
  2. Fidélité — les champs reflètent la source ; une donnée absente reste absente, jamais de valeur inventée (salaire, date, coordonnées).
  3. Traçabilité — chaque offre garde son URL originale ; la candidature se fait chez l'employeur. Zéro boîte noire.
  4. Robustesse — une source cassée ne bloque pas les autres et ne vide pas la base (délai de grâce + anti-dérive).
  5. Transparence salariale — salaires numériques normalisés en $/h et $/an ; filtre « salaire affiché » de premier ordre.

# Écosystème Groupe KA

Job·Ka partage la philosophie et l'architecture des plateformes sœurs : Lou·Ka (logements), Immo·Ka, Vrai-Prix, ValoPlex, Auto·Ka, Fabri·Ka, Food·Ka.


# Auteur

Simon-Pierre Boucher — contact@spboucher.ai

© 2026 Groupe KA — Simon-Pierre Boucher. Tous droits réservés.

Files · f6e757145f695629fd1254157f0277ba68e1996b
Menu

Theme