From 25e8ce18858656c1cb0c42b703201f361f15b3b6 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 9 Dec 2025 16:06:50 -0500 Subject: [PATCH] prioriter fair --- .../__pycache__/detail_writer.cpython-313.pyc | Bin 2253 -> 2253 bytes .../process_course.cpython-313.pyc | Bin 6466 -> 8101 bytes .../__pycache__/tp_config.cpython-313.pyc | Bin 4873 -> 6915 bytes engine/engine_components/process_course.py | 219 +++++++++++++++--- engine/engine_components/tp_config.py | 90 ++++++- 5 files changed, 271 insertions(+), 38 deletions(-) diff --git a/engine/engine_components/__pycache__/detail_writer.cpython-313.pyc b/engine/engine_components/__pycache__/detail_writer.cpython-313.pyc index 3af2d2cff2b2a4f43d8dc67d9f17afe058d6ba35..62e6e0bb7729812b27c5db195e866b294541a747 100644 GIT binary patch delta 19 ZcmX>rcvg_>GcPX}0}x#8-pF;B0{}YV1;hXV delta 19 ZcmX>rcvg_>GcPX}0}!nD-pF;B0{}R^1zrFE diff --git a/engine/engine_components/__pycache__/process_course.cpython-313.pyc b/engine/engine_components/__pycache__/process_course.cpython-313.pyc index b34f11941076342c9dc0a6aefad8f3fffd86becf..b8adb2254dafd59be49259f1cdf9ad102f7c11e1 100644 GIT binary patch delta 3149 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 Au>b%7 delta 1739 zcmZuyZERCj7(Tb3TfgsZd)sbZd)M!~PaKX75Kx$GhOOwX2c{L4x)yBQZkN-RDKUZY z>-rBon9Rf3&ud0irf3YLZEjsT+!?bMlG2s7F4Mf@~DLIAHT(zkaVN zPE6`AjNptuV*c4KRPDkgZ_VC=75xBnaqTPpD8uk;4Z|bm4AjCdJxP`V6sNlh{7Q0_%5*pWvb{SR zfsrke-2lL_WY~2Pfb=lY-7wNmIz1i5U${EYL}3($cacJAKRJd$=B1GAf!(q<)w#(j zg&}JLQUpe&s2xZ#7$#PHyK%F_0+6&&Lwt$tS~-@Y4QvVoFb2C|=diA2mq;7rxSW8A zIST+OmU_=nki<1Vc2Qj-%hUE3c_xp`c&7OfscNC2YkM{)en+UrBK^(ah%915Uz3iQBu#x`c{t!w8> zh=BYKO5j!7C(Iz8w0qrAniZpPfWjDs7xBCHh(K-#pdAG2x;w zVtC1Mj4|L_jy~;%J%|k~T|Ib&?FzX%mk*R}RsUekwM}Wi!@ABNS$@67CKSU4+p)Z} z#zyfecHo2oSlr8eS*-bb@0gqwKC#lVV!1rAeq?>(+EisaRe9x&itE@-)A8FT?l&`c z{#4mm^Th9%T@_D!#Z-Rp^62`(_0em)Z!F!&RopW-&9B`yJMXfbVo>=G#jJY$inU?D zX3lr?SBvW|=c{z|Rk?n}_`m?1;)-v5yvB_w#$W9GZ4!2PmW@@1py*Z3w=7nb(Utg+u$KSUqI^`t(&>7NhwE6K>r?O{P|^a2_rVb@(1*(5mC0MW7aldT3}fw9gTe=vzY26(zH4U>F`Dlg-612Mb^Uav z^eJ(grnAL-3CUR@LDxQ^b+NgHY@wK5SR~^(ms?2Bi>7{o#)UmXCX3`;7R@%zUf~%M zdHOdbx46ZFvgwSFDYZxz(e>3i@%m~eD;I^t*y9pHz9b~`WG96jWn3Udn&~Dy=>9f9 z$4=CsA+jXvwkL_v5N7%4%ZEt}y-lGx*K}Sf-1JG3NzpC>wp(g9UP|l-? diff --git a/engine/engine_components/__pycache__/tp_config.cpython-313.pyc b/engine/engine_components/__pycache__/tp_config.cpython-313.pyc index 5578785e6de8cb8415dcd33507f40c0f8cf0c152..47fb95da04dc94e16e1c4b0661b7c39d6e0f5027 100644 GIT binary patch delta 1787 zcmZuxO>9(E6u$5OXQtDc{s4tmZ(A%3w54TQEzp9YC6Mrw;A2Q*OY39i%`m2YGu-zc z8dwy=!XgW7Zb)3p#)um=jj@RfNlb_f7t=%|dC|y@#MLB47vjQm-b_&gH@WAWd+z!9 z?z#7#n|(K4O>8tZnFNODkCi^?TuE#bmREKS^o)p>yq&x)Gu@JAHA|TlW;K{bG$o`f zm%6*u$i$3Hv{_?Bw)997D;jBY<&)Psb0WrW3>~-xbE75@a)>0Zh%0AhOH{ztRX~zZ8X}fFC{B;=0{L_sAu&QpR@@CM9Qh<6GiVkKOUVie zk`YHC5e^F8I~xhrdcq@WR)u8mLJBRbJxiup8b+BAo&|;zP-5w>4pv44$(X*8Rrs*j zJ~N7I%S4*h5!f`(D*x4?d%j6h+!e(#hZ_NNY}BvRTogcHoy6ZNGFma26fcr+zu`9q!1zOJ=1UFdDct zS9a%1`F`rzt_O>}!{~zJ(IP7c7026`a~297bzGVc*q~`zJ_~Ywz#Lj(<$O@`9lE=e zhtQaUe)lnCaH;L)N{;I~v`}_)eyQxH=|S_FNn!XENOq83V z8f}hfKeyl%V0Y%ZU9@`MXVVu$!?9lIg*I30gb)w>~HYQi|qNlIx76d@2Cz9 zmFNJW2F$e=oqHmS!+nG*RIyMh{=uKBd)n=<@CG)4SjSEsn>Sp0#0 zLP+ptV_)|@nYBhCA|S(|S_jnNnY7G*Ht3nO8A`c;G5kuQZ}LqNokYoK;YR#ustXKjdq}*!Dz|L_VDja!Un?C9Psf^(U=gAVVesw9>Xi1GFu7%9ry75SP$xYk?chB3Ze|vef;%Qr!on9 z54fHBLBno&CLQM^EyGKP&H?cDv|`~LTn)w9{^`8TUGbJe$U mJE}9e_4E1lvrhGtvwpl#Jz7|wC|1Xc>zPaZ_m&+pg7^obQk8E2 delta 658 zcmXX>Id2m|7~S#gWhahf2M5Q*!ZGI(%wZC)#6$uMi6%nY!jUjI_U_m$S|7}=T|`0@ zR8+{CA3z%rg(V}AGGz*iT{@-#s0ArQqRCZq4b+=EU+jnS` zQ6IC>h}*|gd_d&`yhwg4nFJNhhTEW$VWXCVbL4$$F+%GktKK11bwj$$Hsr~#)R{=+ zNSMg!b+{JiAkkw~PL$xQ9MHu*8!;JNd#Ktvge>DXhsxP`uQJQGll)&|PJ=kd0)=hl zQfPHuZ5l7}Ccgt;;VrR=95 datetime locale +# ------------------------------------------------- +# Conversion d'un timestamp Decimal en datetime UTC +# ------------------------------------------------- def ts_to_utc(ts): ts = Decimal(str(ts)) seconds = int(ts) micro = int((ts - Decimal(seconds)) * Decimal(1_000_000)) - return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(microsecond=micro) - +# ------------------------------------------------- +# Normalisation du temps de course (start/end) +# ------------------------------------------------- def _normalize_course_time(dt): if dt is None: return None, None dt_utc = dt.replace(tzinfo=timezone.utc) - dt_local = dt_utc.astimezone(LOCAL_TZ) + dt_local = dt_utc return dt_local, dt_utc - +# ------------------------------------------------- +# Moteur principal : traitement d'une course +# ------------------------------------------------- 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) 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" 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) if last_processed is None: last_processed = 0 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) 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 + # ------------------------------------------------- + # Boucle sur chaque lecture brute + # ------------------------------------------------- for lec in lectures: rcourse_id = int(lec["rcourse_id"]) chr_loc = lec["chrinfo_location"] - timestamp = Decimal(lec["chrinfo_time"]) + timestamp = Decimal(lec["chrinfo_time"]) + 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) 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: - log_debug(f"[REJECT START] id={rcourse_id} dt_utc={dt_utc} < start_utc={start_utc}") - continue + log_debug(f"[REJECT START] id={rcourse_id} dt_utc={dt_utc} < start_utc={start_utc}") + continue if end_utc and dt_utc > end_utc: - log_debug(f"[REJECT END] id={rcourse_id} dt_utc={dt_utc} > end_utc={end_utc}") - continue + log_debug(f"[REJECT END] id={rcourse_id} dt_utc={dt_utc} > end_utc={end_utc}") + continue status = 1 reason_code = 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"] coureur = get_classement_by_tag(cur, classement_table, tag) if not coureur: - # IMPORTANT : on ignore complètement continue 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) if not tp_info: continue - tp_id = tp_info["tp_id"] - tp_name = tp_info["tp_name"] - prio = tp_info["priority"] + tp_id = tp_info["tp_id"] + tp_name = tp_info["tp_name"] + prio = tp_info["priority"] 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 = ( tp_config[tp_id]["config"] .get("config_tp_interval", {}) @@ -124,19 +171,21 @@ def process_course(cur, cnx, con_id: int): if last_dt and interval_sec > 0: delta = (dt_local - last_dt).total_seconds() + if delta < interval_sec: status = 0 reason_code = "SKIP_INTERVAL" reason_ref_id = det_id log_debug(f"[SKIP INTERVAL] id={rcourse_id} {delta:.1f}s < {interval_sec}s") - # 5) INSERT - - detail_table = f"resultv2_{con_id}_detail" - - # 5) tours + tempstour + # ------------------------------------------------- + # Calcul du tour + temps de tour + # ------------------------------------------------- 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: tempstour = Decimal("0") else: @@ -144,12 +193,15 @@ def process_course(cur, cnx, con_id: int): tempstour_str = format_temps_decimal(tempstour) + # ------------------------------------------------- + # Écriture dans la table detail + # ------------------------------------------------- insert_detail_record( cur, cnx, con_id, dossard, lec, tp_id, tp_name, chr_loc, prio, controller, - dt_local, + dt_utc, status=status, reason_code=reason_code, reason_ref_id=reason_ref_id, @@ -158,23 +210,24 @@ def process_course(cur, cnx, con_id: int): tempstour_str=tempstour_str ) - - - - - # Mise à jour du dernier read VALIDE + # ------------------------------------------------- + # Mise à jour mémoire locale dernière lecture valide + # ------------------------------------------------- if status == 1: _last_valid_read[(tp_id, dossard)] = dt_local 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) + +# ------------------------------------------------- +# Recherche du dernier tour pour un coureur à un TP +# ------------------------------------------------- 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""" SELECT tour, timestamp_read FROM `{detail_table}` @@ -187,7 +240,6 @@ def compute_tour(cur, detail_table, tp_id, dossard): row = cur.fetchone() - # Première lecture → tour 1, pas de temps précédent if not row or row["tour"] is None: return 1, None @@ -195,3 +247,96 @@ def compute_tour(cur, detail_table, tp_id, dossard): prev_ts = Decimal(str(row["timestamp_read"])) 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 diff --git a/engine/engine_components/tp_config.py b/engine/engine_components/tp_config.py index 924c33d..697d38d 100644 --- a/engine/engine_components/tp_config.py +++ b/engine/engine_components/tp_config.py @@ -2,8 +2,96 @@ from typing import Dict, Any, List 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_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 l’item + 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.