temptours+tours
This commit is contained in:
0
engine/__init__.py
Normal file
0
engine/__init__.py
Normal file
BIN
engine/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
engine/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/constants.cpython-313.pyc
Normal file
BIN
engine/__pycache__/constants.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/db.cpython-313.pyc
Normal file
BIN
engine/__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/engine.cpython-313.pyc
Normal file
BIN
engine/__pycache__/engine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/engine_loop.cpython-313.pyc
Normal file
BIN
engine/__pycache__/engine_loop.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/logger.cpython-313.pyc
Normal file
BIN
engine/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
engine/__pycache__/utils.cpython-313.pyc
Normal file
BIN
engine/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
124
engine/config.svg
Normal file
124
engine/config.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 92 KiB |
1
engine/constants.py
Normal file
1
engine/constants.py
Normal file
@ -0,0 +1 @@
|
||||
BATCH_SIZE = 1000
|
||||
74
engine/db.py
Normal file
74
engine/db.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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():
|
||||
print("[OK] Connexion MySQL réussie.")
|
||||
else:
|
||||
print("[ERREUR] Connexion MySQL NON établie.")
|
||||
except Error as e:
|
||||
print(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()
|
||||
33
engine/engine.py
Normal file
33
engine/engine.py
Normal file
@ -0,0 +1,33 @@
|
||||
from mysql.connector import Error
|
||||
from engine.db import get_connection
|
||||
|
||||
from engine.engine_components.active_courses import get_active_con_ids
|
||||
from engine.engine_components.process_course import process_course
|
||||
from engine.logger import log_info, log_debug, log_warn, log_error
|
||||
|
||||
def run_once():
|
||||
try:
|
||||
cnx = get_connection()
|
||||
cur = cnx.cursor(dictionary=True)
|
||||
|
||||
con_ids = get_active_con_ids(cur)
|
||||
if not con_ids:
|
||||
log_info("[INFO] Aucune course active.")
|
||||
return
|
||||
|
||||
for con_id in con_ids:
|
||||
process_course(cur, cnx, con_id)
|
||||
|
||||
except Error as e:
|
||||
log_error(f"[ERREUR MYSQL] {e}")
|
||||
|
||||
finally:
|
||||
try:
|
||||
cur.close()
|
||||
cnx.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_once()
|
||||
0
engine/engine_components/__init__.py
Normal file
0
engine/engine_components/__init__.py
Normal file
BIN
engine/engine_components/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
engine/engine_components/__pycache__/classement.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/classement.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
engine/engine_components/__pycache__/lectures.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/lectures.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
engine/engine_components/__pycache__/tp_config.cpython-313.pyc
Normal file
BIN
engine/engine_components/__pycache__/tp_config.cpython-313.pyc
Normal file
Binary file not shown.
7
engine/engine_components/active_courses.py
Normal file
7
engine/engine_components/active_courses.py
Normal 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]
|
||||
12
engine/engine_components/classement.py
Normal file
12
engine/engine_components/classement.py
Normal 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()
|
||||
57
engine/engine_components/course_config.py
Normal file
57
engine/engine_components/course_config.py
Normal 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
|
||||
}
|
||||
85
engine/engine_components/detail_writer.py
Normal file
85
engine/engine_components/detail_writer.py
Normal 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()
|
||||
16
engine/engine_components/engine_state.py
Normal file
16
engine/engine_components/engine_state.py
Normal 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()
|
||||
12
engine/engine_components/lectures.py
Normal file
12
engine/engine_components/lectures.py
Normal 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()
|
||||
197
engine/engine_components/process_course.py
Normal file
197
engine/engine_components/process_course.py
Normal 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
|
||||
162
engine/engine_components/tp_config.py
Normal file
162
engine/engine_components/tp_config.py
Normal 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
|
||||
39
engine/info.txt
Normal file
39
engine/info.txt
Normal file
@ -0,0 +1,39 @@
|
||||
a valider
|
||||
attention sur le serveur mysql le % devrais enlever
|
||||
|
||||
|
||||
|
||||
config
|
||||
TABLE reader_config_detail — RÈGLES GÉNÉRALES
|
||||
---------------------------------------------
|
||||
|
||||
1) det_type
|
||||
- 1 = règles associées aux Timing Points (TP)
|
||||
- 2 = règles associées aux Courses
|
||||
=> Toujours utiliser det_type pour savoir si la règle appartient à un TP ou à une course.
|
||||
|
||||
2) det_ref1_id
|
||||
- Pour det_type = 1 → det_ref1_id = tp_id
|
||||
- Pour det_type = 2 → det_ref1_id = con_id
|
||||
=> Identifie exactement à quel TP ou à quelle course la règle appartient.
|
||||
|
||||
3) det_groupe
|
||||
- Sert uniquement au regroupement logique / visuel.
|
||||
- NE DOIT PAS être utilisé pour la logique du moteur.
|
||||
=> Le moteur utilise seulement det_type + det_ref1_id + det_clef.
|
||||
|
||||
4) det_clef
|
||||
- Nom symbolique de la règle (ex: clef_controleur, clef_temp_minimum_read_entre, clef_start_time).
|
||||
|
||||
5) det_value1, det_value2, det_value3
|
||||
- Contiennent les valeurs de la règle.
|
||||
- Le moteur doit toujours accepter 1, 2 ou 3 valeurs selon le type de règle.
|
||||
|
||||
6) det_trie
|
||||
- Indique l'ordre de priorité entre plusieurs règles d'un même type pour un même TP ou course.
|
||||
- Le moteur doit TOUJOURS trier par det_trie ASC avant d'interpréter.
|
||||
=> Une règle avec det_trie = 1 est appliquée avant une règle avec det_trie = 10.
|
||||
|
||||
7) Source unique
|
||||
- Toutes les informations de configuration (TP et course) doivent provenir de reader_config_detail.
|
||||
- Aucune autre source de règle ne doit être utilisée dans le moteur.
|
||||
21
engine/logger.py
Normal file
21
engine/logger.py
Normal file
@ -0,0 +1,21 @@
|
||||
import sys
|
||||
|
||||
# ENGINE_MODE = "DEV" ou "PROD"
|
||||
ENGINE_MODE = "DEV" # change à PROD sur ton serveur
|
||||
|
||||
def log_debug(msg: str):
|
||||
if ENGINE_MODE == "DEV":
|
||||
print(f"[DEBUG] {msg}")
|
||||
|
||||
def log_info(msg: str):
|
||||
if ENGINE_MODE == "DEV":
|
||||
print(f"[INFO] {msg}")
|
||||
|
||||
def log_warn(msg: str):
|
||||
# On affiche les warnings en DEV mais pas en PROD
|
||||
if ENGINE_MODE == "DEV":
|
||||
print(f"[WARN] {msg}")
|
||||
|
||||
def log_error(msg: str):
|
||||
# Toujours afficher les erreurs, même en production
|
||||
print(f"[ERROR] {msg}", file=sys.stderr)
|
||||
16
engine/utils.py
Normal file
16
engine/utils.py
Normal file
@ -0,0 +1,16 @@
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
def format_temps_decimal(d):
|
||||
"""
|
||||
Formatte un Decimal représentant un nombre de secondes
|
||||
en HH:MM:SS.mmm — exemple : 00:06:52.820
|
||||
"""
|
||||
d = Decimal(d)
|
||||
total_sec = float(d)
|
||||
|
||||
hours = int(total_sec // 3600)
|
||||
minutes = int((total_sec % 3600) // 60)
|
||||
seconds = total_sec % 60
|
||||
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"
|
||||
BIN
engine/utils/__pycache__/time_utils.cpython-313.pyc
Normal file
BIN
engine/utils/__pycache__/time_utils.cpython-313.pyc
Normal file
Binary file not shown.
20
engine/utils/time_utils.py
Normal file
20
engine/utils/time_utils.py
Normal file
@ -0,0 +1,20 @@
|
||||
# engine/utils/time_utils.py
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import pytz
|
||||
|
||||
LOCAL_TZ = pytz.timezone("America/Toronto")
|
||||
|
||||
def utc_ts_to_local_datetime(ts: float):
|
||||
"""Timestamp UNIX (UTC réel) → datetime locale America/Toronto."""
|
||||
return datetime.fromtimestamp(ts, timezone.utc).astimezone(LOCAL_TZ)
|
||||
|
||||
def utc_ts_to_utc_datetime(ts: float):
|
||||
"""Timestamp UNIX (UTC réel) → datetime UTC."""
|
||||
return datetime.fromtimestamp(ts, timezone.utc)
|
||||
|
||||
def local_datetime_to_utc(dt):
|
||||
"""Datetime locale → UTC."""
|
||||
if dt is None:
|
||||
return None
|
||||
return dt.astimezone(timezone.utc)
|
||||
Reference in New Issue
Block a user