Added logging and a lock (threading) to make sure all notifications are created

This commit is contained in:
Daniel Schick 2025-12-15 17:56:41 +01:00
parent d6becc43ea
commit 39001b37a3

View File

@ -5,11 +5,13 @@ import re
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import datetime import datetime
import threading
from BreCal.database.enums import StatusFlags from BreCal.database.enums import StatusFlags
from BreCal.validators.validation_rule_functions import ValidationRuleFunctions from BreCal.validators.validation_rule_functions import ValidationRuleFunctions
from BreCal.schemas.model import Shipcall from BreCal.schemas.model import Shipcall
from BreCal.local_db import getPoolConnection from BreCal.local_db import getPoolConnection
_evaluation_lock = threading.Lock()
class ValidationRules(ValidationRuleFunctions): class ValidationRules(ValidationRuleFunctions):
""" """
@ -74,79 +76,86 @@ class ValidationRules(ValidationRuleFunctions):
return evaluation_state, violations return evaluation_state, violations
def evaluate_shipcalls(self, shipcall_df:pd.DataFrame)->pd.DataFrame: def evaluate_shipcalls(self, shipcall_df:pd.DataFrame)->pd.DataFrame:
"""apply 'evaluate_shipcall_from_df' to each individual shipcall in {shipcall_df}. Returns shipcall_df ('evaluation', 'evaluation_message', 'evaluation_time' and 'evaluation_notifications_sent' are updated)"""
evaluation_states_old = [state_old for state_old in shipcall_df.loc[:,"evaluation"]]
evaluation_states_old = [state_old if not pd.isna(state_old) else 0 for state_old in evaluation_states_old]
results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values # returns tuple (state, message)
# unbundle individual results. evaluation_states becomes an integer, violation # Acquire lock to prevent race conditions during evaluation and notification creation
evaluation_states_new = [StatusFlags(res[0]).value for res in results] with _evaluation_lock:
violations = [",\r\n".join(res[1]) if len(res[1])>0 else None for res in results] """apply 'evaluate_shipcall_from_df' to each individual shipcall in {shipcall_df}. Returnsshipcall_df ('evaluation', 'evaluation_message', 'evaluation_time' and 'evaluation_notifications_sent' are updated)"""
violations = [self.concise_evaluation_message_if_too_long(violation) for violation in violations] evaluation_states_old = [state_old for state_old in shipcall_df.loc[:,"evaluation"]]
evaluation_states_old = [state_old if not pd.isna(state_old) else 0 for state_old in evaluation_states_old]
results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values # returns tuple (state, message)
# build the list of evaluation times ('now', as isoformat) # unbundle individual results. evaluation_states becomes an integer, violation
#evaluation_time = self.get_notification_times(evaluation_states_new) evaluation_states_new = [StatusFlags(res[0]).value for res in results]
violations = [",\r\n".join(res[1]) if len(res[1])>0 else None for res in results]
violations = [self.concise_evaluation_message_if_too_long(violation) for violation in violations]
if evaluation_states_old is not None and evaluation_states_new is not None: # build the list of evaluation times ('now', as isoformat)
pooledConnection = None #evaluation_time = self.get_notification_times(evaluation_states_new)
try:
pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection)
for shipcall_id, state_old_raw, state_new_raw, violation in zip(shipcall_df.index, evaluation_states_old, evaluation_states_new, violations): if evaluation_states_old is not None and evaluation_states_new is not None:
state_old = int(state_old_raw) if state_old_raw is not None else 0 pooledConnection = None
state_new = int(state_new_raw) if state_new_raw is not None else 0 try:
pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection)
if state_old == state_new: for shipcall_id, state_old_raw, state_new_raw, violation in zip(shipcall_df.index, evaluation_states_old, evaluation_states_new, violations):
continue state_old = int(state_old_raw) if state_old_raw is not None else 0
state_new = int(state_new_raw) if state_new_raw is not None else 0
logging.info(f"Shipcall {shipcall_id}: state_old={state_old}, state_new={state_new}")
if state_old == state_new:
continue
notification_type = 3 # RED (mapped to time_conflict) notification_type = 3 # RED (mapped to time_conflict)
send_notification = False send_notification = False
if state_new == 2: if state_new == 2:
match state_old: match state_old:
case 0: case 0:
send_notification = True send_notification = True
case 1: case 1:
send_notification = True send_notification = True
notification_type = 6 # YELLOW (mapped to missing_data) notification_type = 6 # YELLOW (mapped to missing_data)
elif state_new == 3: elif state_new == 3:
match state_old: match state_old:
case 0: case 0:
send_notification = True send_notification = True
case 1: case 1:
send_notification = True send_notification = True
case 2: case 2:
send_notification = True send_notification = True
if send_notification: if send_notification:
query = f"INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, {notification_type}, 0, ?message?)" logging.info(f"Creating notification for shipcall {shipcall_id}, type={notification_type}")
commands.execute(query, param={"shipcall_id" : int(shipcall_id), "message" : violation}) query = f"INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, {notification_type}, 0, ?message?)"
commands.execute(query, param={"shipcall_id" : int(shipcall_id), "message" : violation})
if state_new == 1 and state_old != 0: # this resolves the conflict if state_new == 1 and state_old != 0: # this resolves the time conflict
query = f"SELECT * from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0" logging.info(f"Resolving notifications for shipcall {shipcall_id}, type={notification_type}")
existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_id)}) query = f"SELECT * from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0"
if len(existing_notification) > 0: existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_id)})
query = "DELETE from notification where id = ?id?" logging.info(f"Found {len(existing_notification)} existing notifications (yet unsent)")
commands.execute(query, param={"id" : existing_notification[0]["id"]}) if len(existing_notification) > 0:
else: logging.info(f"Deleting notification id={existing_notification[0]['id']}")
query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)" query = "DELETE from notification where id = ?id?"
commands.execute(query, param={"shipcall_id" : int(shipcall_id)}) commands.execute(query, param={"id" : existing_notification[0]["id"]})
finally: else:
if pooledConnection is not None: query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)"
pooledConnection.close() commands.execute(query, param={"shipcall_id" : int(shipcall_id)})
finally:
if pooledConnection is not None:
pooledConnection.close()
# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created # build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created
#evaluation_notifications_sent = self.get_notification_states(evaluation_states_old, evaluation_states_new) #evaluation_notifications_sent = self.get_notification_states(evaluation_states_old, evaluation_states_new)
# TODO: detect evaluation state changes and create notifications # TODO: detect evaluation state changes and create notifications
shipcall_df.loc[:,"evaluation"] = evaluation_states_new shipcall_df.loc[:,"evaluation"] = evaluation_states_new
shipcall_df.loc[:,"evaluation_message"] = violations shipcall_df.loc[:,"evaluation_message"] = violations
#shipcall_df.loc[:,"evaluation_time"] = evaluation_time #shipcall_df.loc[:,"evaluation_time"] = evaluation_time
#shipcall_df.loc[:,"evaluation_notifications_sent"] = evaluation_notifications_sent #shipcall_df.loc[:,"evaluation_notifications_sent"] = evaluation_notifications_sent
return shipcall_df return shipcall_df
def concise_evaluation_message_if_too_long(self, violation): def concise_evaluation_message_if_too_long(self, violation):
""" """