temptours+tours

This commit is contained in:
2025-12-09 11:27:02 -05:00
commit f28439c9bb
49 changed files with 18416 additions and 0 deletions

View File

View File

@ -0,0 +1,7 @@
from typing import List
def get_active_con_ids(cur) -> List[int]:
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]

View File

@ -0,0 +1,12 @@
from typing import Optional, Dict
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()

View File

@ -0,0 +1,57 @@
# engine/engine_components/course_config.py
from datetime import datetime
import pytz
from engine.logger import log_debug, log_warn
LOCAL_TZ = "America/Toronto"
def load_course_config(cur, con_id: int):
"""
Charge toutes les configs det_type = 2 (config de course)
et retourne start_time / end_time convertis en datetime LOCAL.
"""
sql = """
SELECT det_clef, det_value1
FROM reader_config_detail
WHERE det_type = 2
AND det_ref1_id = %s
"""
cur.execute(sql, (con_id,))
rows = cur.fetchall()
cfg = {}
for row in rows:
clef = row["det_clef"]
val = row["det_value1"]
cfg[clef] = val
tz = pytz.timezone(LOCAL_TZ)
start = None
end = None
# Convertir start_time si présent
if "clef_start_time" in cfg:
try:
start = tz.localize(datetime.fromtimestamp(int(cfg["clef_start_time"])))
except Exception as e:
log_warn(f"[CONFIG][CON {con_id}] start_time invalide: {e}")
# Convertir fin_time si présent
if "clef_fin_time" in cfg:
try:
end = tz.localize(datetime.fromtimestamp(int(cfg["clef_fin_time"])))
except Exception as e:
log_warn(f"[CONFIG][CON {con_id}] end_time invalide: {e}")
log_debug(f"[CONFIG COURSE] con_id={con_id} start={start} end={end}")
return {
"start_time": start,
"end_time": end,
"raw": cfg
}

View File

@ -0,0 +1,85 @@
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,
dt_local,
status=1,
reason_code=None,
reason_ref_id=None,
tour=None,
tempstour=None,
tempstour_str=None # <-- AJOUT OBLIGATOIRE
):
if isinstance(dt_local, tuple):
dt_local = dt_local[1]
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,
timestamp_read_local,
timestamp_read_local_str,
raw_tag,
reader_location, controller_priority,
controller_name,
source,
status, reason_code, reason_ref_id,
tour, TempsTour, tempstour_str)
VALUES (%s, %s, %s,
%s, %s,
%s,
%s,
%s,
%s,
%s, %s,
%s,
%s,
%s, %s, %s,
%s, %s, %s)
"""
timestamp_read_local_unix = f"{dt_local.timestamp():.2f}"
timestamp_read_local_str = (
f"{dt_local.strftime('%Y-%m-%d %H:%M:%S')}."
f"{dt_local.microsecond // 10000:02d}"
)
if isinstance(tempstour, tuple):
tempstour = None
data = (
con_id,
tp_id,
tp_name,
dossard,
lecture["rcourse_id"],
lecture["chrinfo_time"],
timestamp_read_local_unix,
timestamp_read_local_str,
lecture["chrinfo_tag"],
controller_location,
priority,
controller_name,
"LIVE",
status,
reason_code,
reason_ref_id,
tour,
tempstour,
tempstour_str
)
cur.execute(sql, data)
cnx.commit()

View File

@ -0,0 +1,16 @@
def get_last_processed_read_id(cur, engine_table: str):
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
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()

View File

@ -0,0 +1,12 @@
from engine.constants import BATCH_SIZE
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()

View File

@ -0,0 +1,197 @@
from engine.engine_components.tp_config import load_tp_config_for_con, resolve_tp_for_lecture
from engine.engine_components.course_config import load_course_config
from engine.engine_components.engine_state import get_last_processed_read_id, update_last_processed_read_id
from engine.engine_components.lectures import get_new_lectures
from engine.engine_components.classement import get_classement_by_tag
from engine.engine_components.detail_writer import insert_detail_record
from engine.logger import log_info, log_debug
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from decimal import Decimal
from engine.utils import format_temps_decimal
LOCAL_TZ = ZoneInfo("America/Toronto")
_last_valid_read = {} # (tp_id, dossard) -> datetime locale
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)
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)
return dt_local, dt_utc
def process_course(cur, cnx, con_id: int):
tp_config = load_tp_config_for_con(cur, con_id)
course_cfg = load_course_config(cur, con_id)
raw_start = course_cfg["start_time"]
raw_end = course_cfg["end_time"]
start_local, start_utc = _normalize_course_time(raw_start)
end_local, end_utc = _normalize_course_time(raw_end)
engine_table = f"resultv2_{con_id}_lecture"
classement_table = f"resultv2_{con_id}_classement"
# last_processed
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}")
lectures = get_new_lectures(cur, last_processed)
log_info(f"[NEW LECTURES] con_id={con_id} count={len(lectures)}")
if not lectures:
return
max_id = last_processed
for lec in lectures:
rcourse_id = int(lec["rcourse_id"])
chr_loc = lec["chrinfo_location"]
timestamp = Decimal(lec["chrinfo_time"])
max_id = max(max_id, rcourse_id)
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
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
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
status = 1
reason_code = None
reason_ref_id = None
# 2) coureur (BIB doit exister dans le 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
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"]
controller = tp_info["controller_name"]
det_id = tp_info["det_id"]
# 4) interval rule
interval_cfg = (
tp_config[tp_id]["config"]
.get("config_tp_interval", {})
.get("clef_temp_minimum_read_entre", [])
)
interval_sec = 0
if interval_cfg:
try:
interval_sec = int(interval_cfg[0]["value1"])
except:
interval_sec = 0
last_dt = _last_valid_read.get((tp_id, dossard))
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
tour, prev_ts = compute_tour(cur, detail_table, tp_id, dossard)
if prev_ts is None:
tempstour = Decimal("0")
else:
tempstour = timestamp - prev_ts
tempstour_str = format_temps_decimal(tempstour)
insert_detail_record(
cur, cnx, con_id,
dossard, lec,
tp_id, tp_name,
chr_loc, prio, controller,
dt_local,
status=status,
reason_code=reason_code,
reason_ref_id=reason_ref_id,
tour=tour,
tempstour=tempstour,
tempstour_str=tempstour_str
)
# Mise à jour du dernier read 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}")
update_last_processed_read_id(cur, cnx, engine_table, max_id)
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}`
WHERE tp_id = %s
AND cla_info_dossard = %s
AND status = 1
ORDER BY detail_id DESC
LIMIT 1
""", (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
last_tour = row["tour"]
prev_ts = Decimal(str(row["timestamp_read"]))
return last_tour + 1, prev_ts

View File

@ -0,0 +1,162 @@
from typing import Dict, Any, List
from engine.logger import log_debug, log_info
def load_tp_config_for_con(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.
Structure retournée :
{
tp_id: {
"tp_name": "...",
"config": {
"config_tp": {
"clef_controleur": [
{ det_id, value1, value2, value3 }
],
"clef_temp_read": [
...
]
},
"config_tp_interval": {
"clef_temp_minimum_read_entre": [
{ det_id, value1, value2, value3 }
]
}
}
}
}
"""
# 1) Lire la liste des TP pour ce con_id
sql_tp = """
SELECT tp_id, tp_name
FROM reader_config_timingpoint
WHERE con_id = %s
"""
cur.execute(sql_tp, (con_id,))
tps = cur.fetchall()
if not tps:
log_info(f"[CONFIG] Aucun TP trouvé pour con_id={con_id}")
return {}
# Préparer structure finale
tp_config: Dict[int, Dict] = {}
for tp in tps:
tp_id = tp["tp_id"]
tp_config[tp_id] = {
"tp_name": tp["tp_name"],
"config": {}
}
# 2) Lire toutes les configs det_type = 1 pour ce con_id
sql_conf = """
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_groupe, d.det_clef, d.det_id
"""
cur.execute(sql_conf, (con_id,))
rows = cur.fetchall()
# 3) Assembler
for row in rows:
tp_id = row["tp_id"]
group = row["det_groupe"] or "default"
clef = row["det_clef"]
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] = []
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"],
})
# 4) Affichage debug clair
log_info(f"[CONFIG] Chargé config TP pour con_id={con_id}")
for tp_id, data in tp_config.items():
log_info(f" TP {tp_id} ({data['tp_name']})")
for group, clefs in data["config"].items():
log_debug(f" [{group}]")
for clef, items in clefs.items():
for it in items:
log_debug(
f" {clef}: det_id={it['det_id']} "
f"v1={it['value1']} v2={it['value2']} v3={it['value3']}"
)
return tp_config
def resolve_tp_for_lecture(tp_config, con_id: int, chr_location: str):
"""
Retourne l'entrée TP correspondant à la location lue.
On cherche dans les configs TP la clef :
config_tp → clef_controleur → value1 = chr_location
On choisit automatiquement :
- la priorité la plus basse (1 est meilleur que 2)
- si ties : on prend la première
Retourne un dict complet :
{
"tp_id": ...,
"tp_name": "...",
"priority": ...,
"controller_name": "...",
"det_id": ...
}
"""
best = None
for tp_id, data in tp_config.items():
# Toute config pour ce timing point
groups = data.get("config", {})
# Groupe config_tp (celui des contrôleurs)
cfg_tp = groups.get("config_tp", {})
# Liste des clefs clef_controleur
controllers = cfg_tp.get("clef_controleur", [])
for ctrl in controllers:
if ctrl["value1"] != chr_location:
continue
prio = int(ctrl["value2"]) if ctrl["value2"] else 999
item = {
"tp_id": tp_id,
"tp_name": data["tp_name"],
"priority": prio,
"controller_name": ctrl["value3"],
"det_id": ctrl["det_id"],
}
# Choisir le meilleur contrôleur (prio la plus petite)
if best is None or prio < best["priority"]:
best = item
return best