Ranger le moteur : ignore caches/logs, retire le backup local.

Prepare le depot pour Gitea (ms1reader-engine) sans artefacts de build.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 14:07:42 -04:00
parent 6300495929
commit 67c15bb0c0
32 changed files with 19 additions and 16896 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
__pycache__/
*.py[cod]
*.class
*.log
.venv/
venv/
.env
.env.*
!.env.example
.idea/
.vscode/
*.swp
Thumbs.db
Desktop.ini

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@ -1,74 +0,0 @@
"""
db.py
======
Module de gestion de la connexion MySQL pour le moteur de résultats.
Ce module est séparé pour éviter de dupliquer la logique de connexion
dans plusieurs fichiers Python. Si plus tard tu veux réutiliser cette
connexion dans d'autres scripts, tu n'auras qu'à importer get_connection().
"""
import mysql.connector
from mysql.connector import Error
from engine.logger import log_info, log_debug, log_warn, log_error
# ---------------------------------------------------------------------------
# CONFIGURATION DE LA BASE DE DONNÉES
# ---------------------------------------------------------------------------
# ⚠ IMPORTANT :
# - Adapte ces valeurs à TON environnement (dev / prod).
# - Idéalement, tu peux plus tard les lire à partir d'un fichier
# (ex: info_serveur.txt) pour éviter de hardcoder les mots de passe.
# ---------------------------------------------------------------------------
DB_CONFIG = {
"host": "serveur-prod.progiweb.net",
"user": "ms1reader_bd", # ajuste au besoin
"password": "X~=v]lg-l]4,T2FF", # ajuste au besoin
"database": "ms1reader_centrale",
"port": 3306
}
TABLE_NAME = "reader_lectures"
def get_connection():
"""
Crée et retourne une connexion MySQL.
Returns
-------
mysql.connector.connection_cext.CMySQLConnection
Connexion déjà ouverte vers la base configurée dans DB_CONFIG.
Raises
------
mysql.connector.Error
Si la connexion échoue (mauvais host, user, password, etc.).
"""
return mysql.connector.connect(**DB_CONFIG)
def test_connection():
"""
Petit utilitaire pour tester rapidement la connexion.
Tu peux exécuter ce fichier directement :
python -m engine.db
"""
try:
cnx = get_connection()
if cnx.is_connected():
log_info("[OK] Connexion MySQL réussie.")
else:
log_error("[ERREUR] Connexion MySQL NON établie.")
except Error as e:
log_error(f"[ERREUR] Mysql : {e}")
finally:
try:
if cnx and cnx.is_connected():
cnx.close()
except NameError:
# cnx n'existe pas si la connexion a échoué avant son affectation
pass
if __name__ == "__main__":
test_connection()

View File

@ -1,341 +0,0 @@
"""
Moteur de résultats Version TP + DETAIL brut
----------------------------------------------
Étapes couvertes :
1) Lire les courses actives (reader_config).
2) Charger en mémoire la configuration des timing points / contrôleurs
à partir de reader_config_timingpoint + reader_config_detail
(clef = 'clef_controleur').
3) Pour chaque course active :
- lire resultv2_<con_id>_lecture.last_processed_read_id
- lire les nouvelles lectures dans reader_lectures
- associer chaque lecture à un coureur (cla_list_tag) et à un TP
(via chrinfo_location → config_detail)
- écrire une ligne brute dans resultv2_<con_id>_detail
avec con_id, dossard, tp_id, tp_name, chrinfo_location, priorité, etc.
"""
from typing import Optional, Dict, List
from mysql.connector import Error
from engine.db import get_connection
BATCH_SIZE = 500
# ---------------------------------------------------------
# 1) COURSES ACTIVES
# ---------------------------------------------------------
def get_active_con_ids(cur) -> List[int]:
"""Retourne tous les con_id actifs."""
sql = "SELECT con_id FROM reader_config WHERE rea_active = 1"
cur.execute(sql)
rows = cur.fetchall()
return [int(r["con_id"]) for r in rows]
# ---------------------------------------------------------
# 2) CHARGER LA CONFIG TP / CONTRÔLEURS EN MÉMOIRE
# ---------------------------------------------------------
def load_tp_config(cur) -> Dict[str, List[Dict]]:
"""
Charge la configuration des timing points et contrôleurs.
On lit :
- reader_config_timingpoint (tp_id, con_id, tp_name)
- reader_config_detail (det_type=1, det_clef='clef_controleur')
On retourne un dict :
clé = chrinfo_location (ex: 'PRO5655')
valeur= liste de dicts { con_id, tp_id, tp_name, priority }
"""
sql = """
SELECT
tp.tp_id,
tp.con_id,
tp.tp_name,
d.det_value1 AS chr_location,
d.det_value2 AS priority
FROM reader_config_timingpoint tp
JOIN reader_config_detail d
ON d.det_type = 1
AND d.det_ref1_id = tp.tp_id
AND d.det_clef = 'clef_controleur'
"""
cur.execute(sql)
rows = cur.fetchall()
tp_map: Dict[str, List[Dict]] = {}
for row in rows:
loc = row["chr_location"]
if loc not in tp_map:
tp_map[loc] = []
# normaliser la priorité (det_value2) → int, défaut 1
try:
prio = int(row["priority"]) if row["priority"] is not None else 1
except ValueError:
prio = 1
tp_map[loc].append(
{
"con_id": int(row["con_id"]),
"tp_id": int(row["tp_id"]),
"tp_name": row["tp_name"],
"priority": prio,
}
)
# DEBUG : afficher ce qu'on a chargé
print("[DEBUG] TP config chargée :")
if not tp_map:
print(" (aucune ligne trouvée dans reader_config_detail avec det_clef='clef_controleur')")
else:
for loc, items in tp_map.items():
descs = [
f"con_id={it['con_id']} tp_id={it['tp_id']} "
f"tp_name={it['tp_name']} prio={it['priority']}"
for it in items
]
print(f" location={loc} -> " + " | ".join(descs))
return tp_map
def resolve_tp_for_lecture(tp_map: Dict[str, List[Dict]], con_id: int, chr_location: str) -> Optional[Dict]:
"""
À partir de chrinfo_location, trouve le TP applicable pour cette course.
- On cherche dans tp_map[chr_location]
- On filtre sur con_id
- S'il y a plusieurs TP possibles, on prend la meilleure priorité (plus petit chiffre).
"""
if chr_location not in tp_map:
return None
candidates = [tp for tp in tp_map[chr_location] if tp["con_id"] == con_id]
if not candidates:
return None
# Choisir la meilleure priorité
best = sorted(candidates, key=lambda t: t["priority"])[0]
return best
# ---------------------------------------------------------
# 3) LECTURE DE L'ÉTAT MOTEUR
# ---------------------------------------------------------
def get_last_processed_read_id(cur, engine_table: str) -> Optional[int]:
sql = f"SELECT last_processed_read_id FROM `{engine_table}` WHERE id = 1"
cur.execute(sql)
row = cur.fetchone()
return int(row["last_processed_read_id"]) if row else None
# ---------------------------------------------------------
# 4) LECTURES BRUTES
# ---------------------------------------------------------
def get_new_lectures(cur, last_processed: int):
"""Lit les nouvelles lectures à partir de rcourse_id."""
sql = """
SELECT rcourse_id, chrinfo_tag, chrinfo_time, chrinfo_location
FROM reader_lectures
WHERE rcourse_id > %s
ORDER BY rcourse_id ASC
LIMIT %s
"""
cur.execute(sql, (last_processed, BATCH_SIZE))
return cur.fetchall()
# ---------------------------------------------------------
# 5) MATCH TAG → COUREUR
# ---------------------------------------------------------
def get_classement_by_tag(cur, classement_table: str, tag: str) -> Optional[Dict]:
"""
Trouve le coureur correspondant à un tag RFID.
Retourne cla_info_dossard (ID officiel du coureur).
"""
pattern = f"%*{tag}*%"
sql = f"""
SELECT cla_info_dossard, con_id, cla_list_tag
FROM `{classement_table}`
WHERE cla_list_tag LIKE %s
LIMIT 1
"""
cur.execute(sql, (pattern,))
return cur.fetchone()
# ---------------------------------------------------------
# 6) ÉCRITURE DANS DETAIL
# ---------------------------------------------------------
def insert_detail_record(
cur,
cnx,
con_id: int,
dossard: str,
lecture: Dict,
tp_id: int,
tp_name: str,
controller_location: str,
priority: int,
):
"""
Insère une ligne brute dans resultv2_<con_id>_detail.
"""
detail_table = f"resultv2_{con_id}_detail"
sql = f"""
INSERT INTO `{detail_table}`
(con_id, tp_id, tp_name,
cla_info_dossard, rcourse_id,
timestamp_read, raw_tag,
reader_location, controller_priority,
source)
VALUES (%s, %s, %s,
%s, %s,
%s, %s,
%s, %s,
%s)
"""
data = (
con_id,
tp_id,
tp_name,
dossard,
lecture["rcourse_id"],
lecture["chrinfo_time"],
lecture["chrinfo_tag"],
controller_location,
priority,
"LIVE", # pour l'instant
)
cur.execute(sql, data)
cnx.commit()
# ---------------------------------------------------------
# 7) METTRE À JOUR LE POINTEUR
# ---------------------------------------------------------
def update_last_processed_read_id(cur, cnx, engine_table: str, new_value: int):
sql = f"""
UPDATE `{engine_table}`
SET last_processed_read_id = %s,
updated_at = NOW()
WHERE id = 1
"""
cur.execute(sql, (new_value,))
cnx.commit()
# ---------------------------------------------------------
# 8) TRAITER UNE COURSE
# ---------------------------------------------------------
def process_course(cur, cnx, con_id: int, tp_map: Dict[str, List[Dict]]):
"""Traite toutes les nouvelles lectures pour une course donnée."""
engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement"
last_processed = get_last_processed_read_id(cur, engine_table)
if last_processed is None:
print(f"[ERREUR] Pas de ligne id=1 dans {engine_table}.")
return
print(f"[INFO] CON_ID={con_id} last_processed_read_id={last_processed}")
lectures = get_new_lectures(cur, last_processed)
if not lectures:
print(f"[INFO] CON_ID={con_id} : aucune nouvelle lecture.")
return
max_rcourse_id = last_processed
for lecture in lectures:
rcourse_id = int(lecture["rcourse_id"])
tag = str(lecture["chrinfo_tag"])
chr_location = str(lecture["chrinfo_location"])
max_rcourse_id = max(max_rcourse_id, rcourse_id)
# 1) Trouver le coureur via TAG
coureur = get_classement_by_tag(cur, classement_table, tag)
if not coureur:
print(f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: tag {tag} introuvable dans classement")
continue
dossard = coureur["cla_info_dossard"]
# 2) Trouver le TP via chrinfo_location
tp_info = resolve_tp_for_lecture(tp_map, con_id, chr_location)
if not tp_info:
print(
f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: "
f"location {chr_location} introuvable dans config TP"
)
continue
tp_id = tp_info["tp_id"]
tp_name = tp_info["tp_name"]
priority = tp_info["priority"]
# DEBUG : affichage de ce qui est utilisé pour cette lecture
print(
f"[DEBUG] CON_ID={con_id} rcourse_id={rcourse_id} "
f"loc={chr_location} -> tp_id={tp_id} tp_name={tp_name} prio={priority}"
)
# 3) Écrire la ligne dans DETAIL
insert_detail_record(
cur,
cnx,
con_id,
dossard,
lecture,
tp_id,
tp_name,
chr_location,
priority,
)
print(
f"[OK] CON_ID={con_id} rcourse_id={rcourse_id} tag={tag} "
f"→ dossard={dossard}, tp={tp_name} (id={tp_id})"
)
update_last_processed_read_id(cur, cnx, engine_table, max_rcourse_id)
print(f"[OK] CON_ID={con_id} last_processed_read_id → {max_rcourse_id}")
# ---------------------------------------------------------
# 9) BOUCLE PRINCIPALE
# ---------------------------------------------------------
def run_once():
try:
cnx = get_connection()
cur = cnx.cursor(dictionary=True)
# Charger config TP/contrôleurs UNE FOIS
tp_map = load_tp_config(cur)
con_ids = get_active_con_ids(cur)
if not con_ids:
print("[INFO] Aucune course active.")
return
for con_id in con_ids:
process_course(cur, cnx, con_id, tp_map)
except Error as e:
print(f"[ERREUR MYSQL] {e}")
finally:
try:
cur.close()
cnx.close()
except:
pass
if __name__ == "__main__":
run_once()

View File

@ -1,330 +0,0 @@
"""
Moteur de résultats Version TP + DETAIL brut (patch final)
-----------------------------------------------------------
Étapes couvertes :
1) Lire les courses actives (reader_config).
2) Pour chaque course active :
- Charger la configuration TP / contrôleurs (seulement pour ce con_id !)
- Lire resultv2_<con_id>_lecture.last_processed_read_id
- Lire les nouvelles lectures dans reader_lectures
- Associer chaque lecture à un coureur (cla_list_tag)
- Associer chaque lecture à un TP (via chrinfo_location → config_detail)
- Écrire une ligne brute dans resultv2_<con_id>_detail
avec con_id, dossard, tp_id, tp_name, chrinfo_location, priorité,
controller_name, etc.
"""
from typing import Optional, Dict, List
from mysql.connector import Error
from engine.db import get_connection
BATCH_SIZE = 1000
# ---------------------------------------------------------
# 1) COURSES ACTIVES
# ---------------------------------------------------------
def get_active_con_ids(cur) -> List[int]:
"""Retourne tous les con_id actifs."""
sql = "SELECT con_id FROM reader_config WHERE rea_active = 1"
cur.execute(sql)
rows = cur.fetchall()
return [int(r["con_id"]) for r in rows]
# ---------------------------------------------------------
# 2) CHARGER LA CONFIG TP / CONTRÔLEURS (PAR COURSE)
# ---------------------------------------------------------
def load_tp_config_for_con(cur, con_id: int) -> Dict[str, List[Dict]]:
"""
Charge TOUTE la configuration TP pour UNE course (con_id).
Retourne un dict :
clé = chrinfo_location (ex: 'PRO5655')
valeur= liste de dicts { con_id, tp_id, tp_name, priority, controller_name }
"""
sql = """
SELECT
tp.tp_id,
tp.con_id,
tp.tp_name,
d.det_value1 AS chr_location,
d.det_value2 AS priority,
d.det_value3 AS controller_name
FROM reader_config_timingpoint tp
JOIN reader_config_detail d
ON d.det_type = 1
AND d.det_ref1_id = tp.tp_id
AND d.det_clef = 'clef_controleur'
WHERE tp.con_id = %s
"""
cur.execute(sql, (con_id,))
rows = cur.fetchall()
tp_map: Dict[str, List[Dict]] = {}
for row in rows:
loc = row["chr_location"]
if loc not in tp_map:
tp_map[loc] = []
try:
prio = int(row["priority"]) if row["priority"] is not None else 1
except ValueError:
prio = 1
tp_map[loc].append(
{
"con_id": int(row["con_id"]),
"tp_id": int(row["tp_id"]),
"tp_name": row["tp_name"],
"priority": prio,
"controller_name": row["controller_name"],
}
)
print(f"[DEBUG] TP config chargée pour CON_ID={con_id}:")
if not tp_map:
print(" (aucune ligne trouvée pour ce con_id)")
else:
for loc, items in tp_map.items():
parts = [
f"tp_id={tp['tp_id']} tp_name={tp['tp_name']} "
f"prio={tp['priority']} name={tp['controller_name']}"
for tp in items
]
print(f" location={loc} -> " + " | ".join(parts))
return tp_map
# ---------------------------------------------------------
# 3) RÉSOUDRE LE TP POUR UNE LECTURE
# ---------------------------------------------------------
def resolve_tp_for_lecture(tp_map: Dict[str, List[Dict]], con_id: int, chr_location: str) -> Optional[Dict]:
"""Trouve le TP applicable pour chrinfo_location avec ce con_id."""
if chr_location not in tp_map:
return None
candidates = [tp for tp in tp_map[chr_location] if tp["con_id"] == con_id]
if not candidates:
return None
return sorted(candidates, key=lambda t: t["priority"])[0]
# ---------------------------------------------------------
# 4) LECTURE DE L'ÉTAT MOTEUR
# ---------------------------------------------------------
def get_last_processed_read_id(cur, engine_table: str) -> Optional[int]:
sql = f"SELECT last_processed_read_id FROM `{engine_table}` WHERE id = 1"
cur.execute(sql)
row = cur.fetchone()
return int(row["last_processed_read_id"]) if row else None
# ---------------------------------------------------------
# 5) LECTURES BRUTES
# ---------------------------------------------------------
def get_new_lectures(cur, last_processed: int):
sql = """
SELECT rcourse_id, chrinfo_tag, chrinfo_time, chrinfo_location
FROM reader_lectures
WHERE rcourse_id > %s
ORDER BY rcourse_id ASC
LIMIT %s
"""
cur.execute(sql, (last_processed, BATCH_SIZE))
return cur.fetchall()
# ---------------------------------------------------------
# 6) MATCH TAG → COUREUR
# ---------------------------------------------------------
def get_classement_by_tag(cur, classement_table: str, tag: str) -> Optional[Dict]:
pattern = f"%*{tag}*%"
sql = f"""
SELECT cla_info_dossard, con_id, cla_list_tag
FROM `{classement_table}`
WHERE cla_list_tag LIKE %s
LIMIT 1
"""
cur.execute(sql, (pattern,))
return cur.fetchone()
# ---------------------------------------------------------
# 7) INSERT DANS DETAIL
# ---------------------------------------------------------
def insert_detail_record(
cur,
cnx,
con_id: int,
dossard: str,
lecture: Dict,
tp_id: int,
tp_name: str,
controller_location: str,
priority: int,
controller_name: str,
):
detail_table = f"resultv2_{con_id}_detail"
sql = f"""
INSERT INTO `{detail_table}`
(con_id, tp_id, tp_name,
cla_info_dossard, rcourse_id,
timestamp_read, raw_tag,
reader_location, controller_priority,
controller_name,
source)
VALUES (%s, %s, %s,
%s, %s,
%s, %s,
%s, %s,
%s,
%s)
"""
data = (
con_id,
tp_id,
tp_name,
dossard,
lecture["rcourse_id"],
str(lecture["chrinfo_time"]),
lecture["chrinfo_tag"],
controller_location,
priority,
controller_name,
"LIVE",
)
cur.execute(sql, data)
cnx.commit()
# ---------------------------------------------------------
# 8) METTRE À JOUR LE POINTEUR
# ---------------------------------------------------------
def update_last_processed_read_id(cur, cnx, engine_table: str, new_value: int):
sql = f"""
UPDATE `{engine_table}`
SET last_processed_read_id = %s,
updated_at = NOW()
WHERE id = 1
"""
cur.execute(sql, (new_value,))
cnx.commit()
# ---------------------------------------------------------
# 9) TRAITER UNE COURSE
# ---------------------------------------------------------
def process_course(cur, cnx, con_id: int):
engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement"
# CHARGER la config TP / contrôleurs **PAR course**
tp_map = load_tp_config_for_con(cur, con_id)
last_processed = get_last_processed_read_id(cur, engine_table)
if last_processed is None:
print(f"[ERREUR] Pas de ligne id=1 dans {engine_table}.")
return
print(f"[INFO] CON_ID={con_id} last_processed_read_id={last_processed}")
lectures = get_new_lectures(cur, last_processed)
if not lectures:
print(f"[INFO] CON_ID={con_id} : aucune nouvelle lecture.")
return
max_rcourse_id = last_processed
for lecture in lectures:
rcourse_id = int(lecture["rcourse_id"])
tag = str(lecture["chrinfo_tag"])
chr_location = str(lecture["chrinfo_location"])
max_rcourse_id = max(max_rcourse_id, rcourse_id)
# TAG → COUREUR
coureur = get_classement_by_tag(cur, classement_table, tag)
if not coureur:
print(f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: tag {tag} introuvable")
continue
dossard = coureur["cla_info_dossard"]
# LOCATION → TP
tp_info = resolve_tp_for_lecture(tp_map, con_id, chr_location)
if not tp_info:
print(
f"[WARN] CON_ID={con_id} rcourse_id={rcourse_id}: "
f"location {chr_location} introuvable pour cette course"
)
continue
tp_id = tp_info["tp_id"]
tp_name = tp_info["tp_name"]
priority = tp_info["priority"]
controller_name = tp_info["controller_name"]
print(
f"[DEBUG] CON_ID={con_id} rcourse_id={rcourse_id} "
f"loc={chr_location} -> tp_id={tp_id} tp_name={tp_name} "
f"prio={priority} name={controller_name}"
)
# INSERT DETAIL
insert_detail_record(
cur,
cnx,
con_id,
dossard,
lecture,
tp_id,
tp_name,
chr_location,
priority,
controller_name,
)
print(
f"[OK] CON_ID={con_id} rcourse_id={rcourse_id} "
f"tag={tag} → dossard={dossard}, tp={tp_name} (id={tp_id})"
)
update_last_processed_read_id(cur, cnx, engine_table, max_rcourse_id)
print(f"[OK] CON_ID={con_id} last_processed_read_id → {max_rcourse_id}")
# ---------------------------------------------------------
# 10) BOUCLE PRINCIPALE
# ---------------------------------------------------------
def run_once():
try:
cnx = get_connection()
cur = cnx.cursor(dictionary=True)
con_ids = get_active_con_ids(cur)
if not con_ids:
print("[INFO] Aucune course active.")
return
for con_id in con_ids:
process_course(cur, cnx, con_id)
except Error as e:
print(f"[ERREUR MYSQL] {e}")
finally:
try:
cur.close()
cnx.close()
except:
pass
if __name__ == "__main__":
run_once()

View File

@ -1,2 +0,0 @@
a valider
attention sur le serveur mysql le % devrais enlever

View File

@ -34,8 +34,9 @@ def _normalize_course_time(dt):
def process_course(cur, cnx, con_id: int):
tp_config = load_tp_config_for_con(cur, con_id)
log_debug(f"[TP_CONFIG FULL] {tp_config}")
course_cfg = load_course_config(cur, con_id)
log_debug(f"[TP_CONFIG FULL] {course_cfg }")
raw_start = course_cfg["start_time"]
raw_end = course_cfg["end_time"]

View File

@ -1,7 +1,8 @@
a valider
attention sur le serveur mysql le % devrais enlever
pour executer
python -m engine.engine
config
TABLE reader_config_detail — RÈGLES GÉNÉRALES
@ -54,3 +55,4 @@ Concept général à corriger :
si ca plente pendant la validatione des regles
valider premier tours
valider temps tours si prioryty change