prioriter fair

This commit is contained in:
2025-12-09 16:06:50 -05:00
parent f28439c9bb
commit 25e8ce1885
5 changed files with 271 additions and 38 deletions

View File

@ -9,30 +9,44 @@ from datetime import datetime, timezone
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from decimal import Decimal from decimal import Decimal
from engine.utils import format_temps_decimal from engine.utils import format_temps_decimal
LOCAL_TZ = ZoneInfo("America/Toronto") LOCAL_TZ = ZoneInfo("America/Toronto")
# -------------------------------------------------
# Mémoire locale pour la dernière lecture valide
# -------------------------------------------------
_last_valid_read = {} # (tp_id, dossard) -> datetime locale _last_valid_read = {} # (tp_id, dossard) -> datetime locale
# -------------------------------------------------
# Conversion d'un timestamp Decimal en datetime UTC
# -------------------------------------------------
def ts_to_utc(ts): def ts_to_utc(ts):
ts = Decimal(str(ts)) ts = Decimal(str(ts))
seconds = int(ts) seconds = int(ts)
micro = int((ts - Decimal(seconds)) * Decimal(1_000_000)) micro = int((ts - Decimal(seconds)) * Decimal(1_000_000))
return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(microsecond=micro) return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(microsecond=micro)
# -------------------------------------------------
# Normalisation du temps de course (start/end)
# -------------------------------------------------
def _normalize_course_time(dt): def _normalize_course_time(dt):
if dt is None: if dt is None:
return None, None return None, None
dt_utc = dt.replace(tzinfo=timezone.utc) dt_utc = dt.replace(tzinfo=timezone.utc)
dt_local = dt_utc.astimezone(LOCAL_TZ) dt_local = dt_utc
return dt_local, dt_utc return dt_local, dt_utc
# -------------------------------------------------
# Moteur principal : traitement d'une course
# -------------------------------------------------
def process_course(cur, cnx, con_id: int): def process_course(cur, cnx, con_id: int):
# -------------------------------------------------
# Lecture de la configuration TP + configuration course
# -------------------------------------------------
tp_config = load_tp_config_for_con(cur, con_id) tp_config = load_tp_config_for_con(cur, con_id)
course_cfg = load_course_config(cur, con_id) course_cfg = load_course_config(cur, con_id)
@ -45,13 +59,18 @@ def process_course(cur, cnx, con_id: int):
engine_table = f"resultv2_{con_id}_lecture" engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement" classement_table = f"resultv2_{con_id}_classement"
# last_processed # -------------------------------------------------
# Lire dernier ID traité (mécanisme incremental)
# -------------------------------------------------
last_processed = get_last_processed_read_id(cur, engine_table) last_processed = get_last_processed_read_id(cur, engine_table)
if last_processed is None: if last_processed is None:
last_processed = 0 last_processed = 0
log_info(f"[STATE] con_id={con_id} last_processed={last_processed}") log_info(f"[STATE] con_id={con_id} last_processed={last_processed}")
# -------------------------------------------------
# Récupère les lectures non traitées
# -------------------------------------------------
lectures = get_new_lectures(cur, last_processed) lectures = get_new_lectures(cur, last_processed)
log_info(f"[NEW LECTURES] con_id={con_id} count={len(lectures)}") log_info(f"[NEW LECTURES] con_id={con_id} count={len(lectures)}")
@ -60,53 +79,81 @@ def process_course(cur, cnx, con_id: int):
max_id = last_processed max_id = last_processed
# -------------------------------------------------
# Boucle sur chaque lecture brute
# -------------------------------------------------
for lec in lectures: for lec in lectures:
rcourse_id = int(lec["rcourse_id"]) rcourse_id = int(lec["rcourse_id"])
chr_loc = lec["chrinfo_location"] chr_loc = lec["chrinfo_location"]
timestamp = Decimal(lec["chrinfo_time"]) timestamp = Decimal(lec["chrinfo_time"])
max_id = max(max_id, rcourse_id) max_id = max(max_id, rcourse_id)
dt_utc = ts_to_utc(timestamp) dt_utc = ts_to_utc(timestamp)
dt_local = dt_utc.astimezone(LOCAL_TZ) dt_local = dt_utc.astimezone(LOCAL_TZ)
log_debug(f"[READ] id={rcourse_id} utc={dt_utc} loc={chr_loc}") log_debug(f"[READ] id={rcourse_id} utc={dt_utc} loc={chr_loc}")
# 1) start/end # -------------------------------------------------
# Validation start/end
# -------------------------------------------------
if start_utc and dt_utc < start_utc: if start_utc and dt_utc < start_utc:
log_debug(f"[REJECT START] id={rcourse_id} dt_utc={dt_utc} < start_utc={start_utc}") log_debug(f"[REJECT START] id={rcourse_id} dt_utc={dt_utc} < start_utc={start_utc}")
continue continue
if end_utc and dt_utc > end_utc: if end_utc and dt_utc > end_utc:
log_debug(f"[REJECT END] id={rcourse_id} dt_utc={dt_utc} > end_utc={end_utc}") log_debug(f"[REJECT END] id={rcourse_id} dt_utc={dt_utc} > end_utc={end_utc}")
continue continue
status = 1 status = 1
reason_code = None reason_code = None
reason_ref_id = None reason_ref_id = None
# 2) coureur (BIB doit exister dans le classement) # -------------------------------------------------
# Vérifier que le BIB existe dans la table classement
# -------------------------------------------------
tag = lec["chrinfo_tag"] tag = lec["chrinfo_tag"]
coureur = get_classement_by_tag(cur, classement_table, tag) coureur = get_classement_by_tag(cur, classement_table, tag)
if not coureur: if not coureur:
# IMPORTANT : on ignore complètement
continue continue
dossard = coureur["cla_info_dossard"] dossard = coureur["cla_info_dossard"]
# 3) TP obligatoire # -------------------------------------------------
# Résoudre le timing point selon la config
# -------------------------------------------------
tp_info = resolve_tp_for_lecture(tp_config, con_id, chr_loc) tp_info = resolve_tp_for_lecture(tp_config, con_id, chr_loc)
if not tp_info: if not tp_info:
continue continue
tp_id = tp_info["tp_id"] tp_id = tp_info["tp_id"]
tp_name = tp_info["tp_name"] tp_name = tp_info["tp_name"]
prio = tp_info["priority"] prio = tp_info["priority"]
controller = tp_info["controller_name"] controller = tp_info["controller_name"]
det_id = tp_info["det_id"] det_id = tp_info["det_id"]
# 4) interval rule detail_table = f"resultv2_{con_id}_detail"
# -------------------------------------------------
# Règle de priorité (peut rejeter ou forcer tour)
# -------------------------------------------------
status, reason_code, reason_ref_id, forced_tour = resolve_priority(
cur,
cnx,
detail_table,
tp_id,
dossard,
timestamp,
prio,
det_id,
tp_config
)
# -------------------------------------------------
# Règle d'intervalle minimum entre lectures
# -------------------------------------------------
interval_cfg = ( interval_cfg = (
tp_config[tp_id]["config"] tp_config[tp_id]["config"]
.get("config_tp_interval", {}) .get("config_tp_interval", {})
@ -124,19 +171,21 @@ def process_course(cur, cnx, con_id: int):
if last_dt and interval_sec > 0: if last_dt and interval_sec > 0:
delta = (dt_local - last_dt).total_seconds() delta = (dt_local - last_dt).total_seconds()
if delta < interval_sec: if delta < interval_sec:
status = 0 status = 0
reason_code = "SKIP_INTERVAL" reason_code = "SKIP_INTERVAL"
reason_ref_id = det_id reason_ref_id = det_id
log_debug(f"[SKIP INTERVAL] id={rcourse_id} {delta:.1f}s < {interval_sec}s") log_debug(f"[SKIP INTERVAL] id={rcourse_id} {delta:.1f}s < {interval_sec}s")
# 5) INSERT # -------------------------------------------------
# Calcul du tour + temps de tour
detail_table = f"resultv2_{con_id}_detail" # -------------------------------------------------
# 5) tours + tempstour
tour, prev_ts = compute_tour(cur, detail_table, tp_id, dossard) tour, prev_ts = compute_tour(cur, detail_table, tp_id, dossard)
if forced_tour is not None:
tour = forced_tour
if prev_ts is None: if prev_ts is None:
tempstour = Decimal("0") tempstour = Decimal("0")
else: else:
@ -144,12 +193,15 @@ def process_course(cur, cnx, con_id: int):
tempstour_str = format_temps_decimal(tempstour) tempstour_str = format_temps_decimal(tempstour)
# -------------------------------------------------
# Écriture dans la table detail
# -------------------------------------------------
insert_detail_record( insert_detail_record(
cur, cnx, con_id, cur, cnx, con_id,
dossard, lec, dossard, lec,
tp_id, tp_name, tp_id, tp_name,
chr_loc, prio, controller, chr_loc, prio, controller,
dt_local, dt_utc,
status=status, status=status,
reason_code=reason_code, reason_code=reason_code,
reason_ref_id=reason_ref_id, reason_ref_id=reason_ref_id,
@ -158,23 +210,24 @@ def process_course(cur, cnx, con_id: int):
tempstour_str=tempstour_str tempstour_str=tempstour_str
) )
# -------------------------------------------------
# Mise à jour mémoire locale dernière lecture valide
# -------------------------------------------------
# Mise à jour du dernier read VALIDE
if status == 1: if status == 1:
_last_valid_read[(tp_id, dossard)] = dt_local _last_valid_read[(tp_id, dossard)] = dt_local
log_info(f"[OK] id={rcourse_id} tag={tag} dossard={dossard} tp={tp_name} prio={prio}") log_info(f"[OK] id={rcourse_id} tag={tag} dossard={dossard} tp={tp_name} prio={prio}")
# -------------------------------------------------
# Mise à jour du pointeur de traitement
# -------------------------------------------------
update_last_processed_read_id(cur, cnx, engine_table, max_id) update_last_processed_read_id(cur, cnx, engine_table, max_id)
# -------------------------------------------------
# Recherche du dernier tour pour un coureur à un TP
# -------------------------------------------------
def compute_tour(cur, detail_table, tp_id, dossard): def compute_tour(cur, detail_table, tp_id, dossard):
"""
Retourne (tour, prev_timestamp_decimal)
- tour = dernier_tour + 1
- prev_timestamp_decimal = timestamp_read du tour précédent (Decimal) ou None si premier tour
"""
cur.execute(f""" cur.execute(f"""
SELECT tour, timestamp_read SELECT tour, timestamp_read
FROM `{detail_table}` FROM `{detail_table}`
@ -187,7 +240,6 @@ def compute_tour(cur, detail_table, tp_id, dossard):
row = cur.fetchone() row = cur.fetchone()
# Première lecture → tour 1, pas de temps précédent
if not row or row["tour"] is None: if not row or row["tour"] is None:
return 1, None return 1, None
@ -195,3 +247,96 @@ def compute_tour(cur, detail_table, tp_id, dossard):
prev_ts = Decimal(str(row["timestamp_read"])) prev_ts = Decimal(str(row["timestamp_read"]))
return last_tour + 1, prev_ts return last_tour + 1, prev_ts
# -------------------------------------------------
# Règle de priorité : remplace, rejette, ou force tour
# -------------------------------------------------
def resolve_priority(
cur,
cnx,
detail_table,
tp_id,
dossard,
new_timestamp,
new_priority,
det_id,
tp_config
):
# -------------------------------------------------
# Lecture de la fenêtre backup pour priorité
# -------------------------------------------------
cfg = (
tp_config[tp_id]["config"]
.get("config_tp", {})
.get("clef_temp_bbk", [])
)
backup_window = 0
if cfg:
try:
backup_window = int(cfg[0]["value1"])
except:
backup_window = 0
log_debug(f"[resolve_priority] backup_window={backup_window}")
# -------------------------------------------------
# Si backup_window = 0 → toujours accepter
# -------------------------------------------------
if backup_window <= 0:
return 1, None, None, None
# -------------------------------------------------
# Recherche d'une lecture existante dans la fenêtre
# -------------------------------------------------
cur.execute(f"""
SELECT detail_id, controller_priority, timestamp_read, tour
FROM `{detail_table}`
WHERE tp_id = %s
AND cla_info_dossard = %s
AND status = 1
AND ABS(timestamp_read - %s) <= %s
LIMIT 1
""", (tp_id, dossard, str(new_timestamp), backup_window))
row = cur.fetchone()
# -------------------------------------------------
# Aucun doublon → accepter
# -------------------------------------------------
if not row:
return 1, None, None, None
old_id = row["detail_id"]
old_priority = row["controller_priority"]
old_tour = row["tour"]
# -------------------------------------------------
# Comparaison des priorités
# -------------------------------------------------
# Nouvelle lecture moins prioritaire → rejet
if new_priority > old_priority:
return 0, "LOWER_PRIORITY", det_id, None
# Nouvelle lecture plus prioritaire → remplacer
if new_priority < old_priority:
cur.execute(f"""
UPDATE `{detail_table}`
SET
status = 0,
reason_code = 'REPLACED_BY_HIGHER_PRIORITY',
reason_ref_id = %s
WHERE detail_id = %s
""", (det_id, old_id))
cnx.commit()
forced_tour = old_tour
return 1, None, None, forced_tour
# Même priorité → rejet
return 0, "EQUAL_PRIORITY", det_id, None

View File

@ -2,8 +2,96 @@
from typing import Dict, Any, List from typing import Dict, Any, List
from engine.logger import log_debug, log_info from engine.logger import log_debug, log_info
def load_tp_config_for_con(cur, con_id: int):
"""
Charge automatiquement toute la configuration TP (det_type = 1)
pour un con_id, sans coder les groupes/celfs en dur.
def load_tp_config_for_con(cur, con_id: int) -> Dict[int, Dict[str, Dict[str, List[Dict[str, Any]]]]]: Structure produite (identique à ton ancienne fonction) :
{
tp_id: {
"tp_name": "...",
"config": {
<det_groupe>: {
<det_clef>: [
{
det_id,
value1, value2, value3
}
]
}
}
}
}
"""
# 1) Récupérer tous les TP liés au con_id
cur.execute("""
SELECT tp_id, tp_name
FROM reader_config_timingpoint
WHERE con_id = %s
""", (con_id,))
tps = cur.fetchall()
tp_config = {}
for tp in tps:
tp_id = tp["tp_id"]
tp_config[tp_id] = {
"tp_name": tp["tp_name"],
"config": {}
}
if not tps:
log_info(f"[CONFIG] Aucun TP trouvé pour con_id={con_id}")
return {}
# 2) Récupérer toutes les configs det_type = 1 liées aux TP
cur.execute("""
SELECT
d.det_id,
d.det_ref1_id AS tp_id,
d.det_groupe,
d.det_clef,
d.det_value1,
d.det_value2,
d.det_value3
FROM reader_config_detail d
JOIN reader_config_timingpoint tp ON tp.tp_id = d.det_ref1_id
WHERE tp.con_id = %s
AND d.det_type = 1
ORDER BY d.det_ref1_id, d.det_groupe, d.det_clef, d.det_id
""", (con_id,))
rows = cur.fetchall()
# 3) Construction automatique
for row in rows:
tp_id = row["tp_id"]
group = row["det_groupe"] or "default"
clef = row["det_clef"]
# Créer les niveaux si nécessaires
if group not in tp_config[tp_id]["config"]:
tp_config[tp_id]["config"][group] = {}
if clef not in tp_config[tp_id]["config"][group]:
tp_config[tp_id]["config"][group][clef] = []
# Ajouter litem
tp_config[tp_id]["config"][group][clef].append({
"det_id": row["det_id"],
"value1": row["det_value1"],
"value2": row["det_value2"],
"value3": row["det_value3"],
})
return tp_config
def load_tp_config_for_conbad(cur, con_id: int) -> Dict[int, Dict[str, Dict[str, List[Dict[str, Any]]]]]:
""" """
Charge TOUTE la configuration TP (det_type = 1) pour un con_id. Charge TOUTE la configuration TP (det_type = 1) pour un con_id.