diff --git a/src/server/BreCal/__init__.py b/src/server/BreCal/__init__.py index 1a6dc48..c4cf995 100644 --- a/src/server/BreCal/__init__.py +++ b/src/server/BreCal/__init__.py @@ -13,6 +13,25 @@ from .api import ships from .api import login from .api import user +from BreCal.brecal_utils.file_handling import get_project_root, ensure_path +from BreCal.brecal_utils.test_handling import execute_test_with_pytest, execute_coverage_test +from BreCal.brecal_utils.time_handling import difference_to_then + +from BreCal.validators.time_logic import TimeLogic +from BreCal.validators.validation_rules import ValidationRules +from BreCal.validators.schema_validation import validation_state_and_validation_name + +from BreCal.stubs.times_agency import get_times_agency +from BreCal.stubs.times_bsmd import get_times_bsmd +from BreCal.stubs.times_mooring import get_times_mooring +from BreCal.stubs.times_pilot import get_times_pilot +from BreCal.stubs.times_portauthority import get_times_port_authority +from BreCal.stubs.times_terminal import get_times_terminal +from BreCal.stubs.times_tug import get_times_tug +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.stubs.df_times import get_df_times + + def create_app(test_config=None): app = Flask(__name__, instance_relative_config=True) @@ -47,22 +66,24 @@ def create_app(test_config=None): return app -from BreCal.brecal_utils.file_handling import get_project_root, ensure_path -from BreCal.brecal_utils.test_handling import execute_test_with_pytest, execute_coverage_test -from BreCal.brecal_utils.time_handling import difference_to_then - -from BreCal.validators.time_logic import TimeLogic -from BreCal.validators.validation_rules import ValidationRules -from BreCal.validators.schema_validation import validation_state_and_validation_name - __all__ = [ - "get_project_root", - "ensure_path", - "execute_test_with_pytest", - "execute_coverage_test", + "get_project_root", + "ensure_path", + "execute_test_with_pytest", + "execute_coverage_test", "difference_to_then", "TimeLogic", "ValidationRules", "validation_state_and_validation_name", + + "get_times_agency", + "get_times_bsmd", + "get_times_mooring", + "get_times_pilot", + "get_times_port_authority", + "get_times_terminal", + "get_times_tug", + "get_times_full_simple", + "get_df_times", ] diff --git a/src/server/BreCal/database/sql_handler.py b/src/server/BreCal/database/sql_handler.py index 457b950..6c1acd2 100644 --- a/src/server/BreCal/database/sql_handler.py +++ b/src/server/BreCal/database/sql_handler.py @@ -221,6 +221,9 @@ class SQLHandler(): # filter out all NaN and NaT entries if non_null_column is not None: + # in the Pandas documentation, it says for .isnull(): + # "This function takes a scalar or array-like object and indicates whether values are missing + # (NaN in numeric arrays, None or NaN in object arrays, NaT in datetimelike)." df_times = df_times.loc[~df_times[non_null_column].isnull()] # NOT null filter # filter by the agency participant_type @@ -230,13 +233,20 @@ class SQLHandler(): def filter_df_by_key_value(self, df, key, value)->pd.DataFrame: return df.loc[df[key]==value] - def get_unique_ship_counts(self, all_df_times:pd.DataFrame, query:str, rounding:str="min", maximum_threshold=3): + def get_unique_ship_counts(self, all_df_times:pd.DataFrame, times_agency:pd.DataFrame, query:str, rounding:str="min", maximum_threshold=3): """given a dataframe of all agency times, get all unique ship counts, their values (datetime) and the string tags. returns a tuple (values,unique,counts)""" - # get values and optional: rounding - values = all_df_times.loc[:, query] + # optional: rounding if rounding is not None: - values = values.dt.round(rounding) # e.g., 'min' + all_df_times.loc[:, query] = all_df_times.loc[:, query].dt.round(rounding) # e.g., 'min' + query_time_agency = times_agency[query].iloc[0].round(rounding)# e.g., 'min' - unique, counts = np.unique(values, return_counts=True) - violation_state = np.any(np.greater(counts, maximum_threshold)) - return (values, unique, counts) + # after rounding, filter {all_df_times}, so only those, which match the current query are of interest + # takes 'times_agency' to sample, which value should match + all_df_times = all_df_times.loc[all_df_times[query]==query_time_agency] + + # finally, count all remaining entries + values = all_df_times.loc[:, query] + + # get unique entries and counts + counts = len(values) # unique, counts = np.unique(values, return_counts=True) + return counts # (values, unique, counts) diff --git a/src/server/BreCal/stubs/__init__.py b/src/server/BreCal/stubs/__init__.py index 83dd2ed..95578de 100644 --- a/src/server/BreCal/stubs/__init__.py +++ b/src/server/BreCal/stubs/__init__.py @@ -3,3 +3,4 @@ def generate_uuid1_int(): """# TODO: clarify, what kind of integer ID is used in mysql. Generates a proxy ID, which is used in the stubs""" from uuid import uuid1 return uuid1().int>>64 + diff --git a/src/server/BreCal/stubs/berth.py b/src/server/BreCal/stubs/berth.py index 240bcbe..2a782d7 100644 --- a/src/server/BreCal/stubs/berth.py +++ b/src/server/BreCal/stubs/berth.py @@ -7,7 +7,6 @@ def get_berth_simple(): # Note: #TODO: name, participant_id & lock state are arbitrary name = "Avangard Dalben" - participant_id = 1# e.g., Avangard lock = False owner_id = 1 # e.g., Avangard authority_id = 1 # e.g., Avangard @@ -19,7 +18,6 @@ def get_berth_simple(): berth = Berth( berth_id, name, - participant_id, lock, owner_id, authority_id, diff --git a/src/server/BreCal/stubs/df_times.py b/src/server/BreCal/stubs/df_times.py new file mode 100644 index 0000000..08d8fd7 --- /dev/null +++ b/src/server/BreCal/stubs/df_times.py @@ -0,0 +1,78 @@ +import pandas as pd +import random +import datetime + +from BreCal.stubs.times_agency import get_times_agency +from BreCal.stubs.times_bsmd import get_times_bsmd +from BreCal.stubs.times_mooring import get_times_mooring +from BreCal.stubs.times_pilot import get_times_pilot +from BreCal.stubs.times_portauthority import get_times_port_authority +from BreCal.stubs.times_terminal import get_times_terminal +from BreCal.stubs.times_tug import get_times_tug +from BreCal.database.enums import ParticipantType + +def get_df_times(shipcall=None): + """in case of providing a shipcall, one can read the id to set each times entry in the dataframe towards that shipcall id""" + df_times = pd.DataFrame([ + fct() + + for fct in [ + get_times_agency, + get_times_bsmd, + get_times_mooring, + get_times_pilot, + get_times_port_authority, + get_times_terminal, + get_times_tug + ] + ]) + if shipcall is not None: + df_times.loc[:,"shipcall_id"] = shipcall.id + return df_times + +def random_time_perturbation(df_times, query): + # random perturbations + population = [datetime.datetime.now(), None, pd.NaT] + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.MOORING.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.PORT_ADMINISTRATION.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, query] = random.sample(population,k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.TUG.value, query] = random.sample(population,k=1)[0] + return df_times + +def get_df_times_participants_disagree(query, shipcall=None, df_times = None): + if df_times is None: + df_times = get_df_times(shipcall) + df_times = random_time_perturbation(df_times=df_times, query=query) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, query] = datetime.datetime.now()+datetime.timedelta(hours=2, minutes=14) + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, query] = datetime.datetime.now()+datetime.timedelta(hours=1, minutes=7) + + return df_times + +def build_stub_df_times(shipcall, query, reference_time): + """creates an artificial dataset, which simulates having too many shipcalls with too many identical times""" + df_times_a = get_df_times(shipcall) + df_times_a = df_times_a.loc[df_times_a["participant_type"]==ParticipantType.AGENCY.value] + df_times_a.loc[:,query] = reference_time + datetime.timedelta(seconds=17) + + df_times_b = get_df_times(shipcall) + df_times_b = df_times_b.loc[df_times_b["participant_type"]==ParticipantType.AGENCY.value] + df_times_b.loc[:,query] = reference_time + datetime.timedelta(seconds=21) + + df_times_c = get_df_times(shipcall) + df_times_c = df_times_c.loc[df_times_c["participant_type"]==ParticipantType.AGENCY.value] + df_times_c.loc[:,query] = reference_time + datetime.timedelta(seconds=26) + + df_times_d = get_df_times(shipcall) + df_times_d = df_times_d.loc[df_times_d["participant_type"]==ParticipantType.AGENCY.value] + df_times_d.loc[:,query] = reference_time + datetime.timedelta(seconds=28) + + df_times_e = get_df_times(shipcall) + df_times_e = df_times_e.loc[df_times_e["participant_type"]==ParticipantType.AGENCY.value] + df_times_e.loc[:,query] = reference_time + datetime.timedelta(seconds=29) + + return pd.concat([df_times_a, df_times_b, df_times_c, df_times_d, df_times_e],axis=0) + + diff --git a/src/server/BreCal/stubs/times_agency.py b/src/server/BreCal/stubs/times_agency.py index e69de29..31fc0e7 100644 --- a/src/server/BreCal/stubs/times_agency.py +++ b/src/server/BreCal/stubs/times_agency.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_agency(): + times_agency = get_times_full_simple() + times_agency.participant_type = ParticipantType.AGENCY.value + return times_agency + diff --git a/src/server/BreCal/stubs/times_bsmd.py b/src/server/BreCal/stubs/times_bsmd.py new file mode 100644 index 0000000..3f36e24 --- /dev/null +++ b/src/server/BreCal/stubs/times_bsmd.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_bsmd(): + times_bsmd = get_times_full_simple() + times_bsmd.participant_type = ParticipantType.BSMD.value + return times_bsmd + diff --git a/src/server/BreCal/stubs/times_full.py b/src/server/BreCal/stubs/times_full.py index 984f3db..f0176a1 100644 --- a/src/server/BreCal/stubs/times_full.py +++ b/src/server/BreCal/stubs/times_full.py @@ -3,9 +3,11 @@ this stub creates an example time object, where the times of every role are pres users will thereby be able to modify these values """ import datetime + from BreCal.stubs import generate_uuid1_int from BreCal.schemas.model import Times + def get_times_full_simple(): # only used for the stub base_time = datetime.datetime.now() @@ -65,3 +67,5 @@ def get_times_full_simple(): modified=modified, ) return times + + diff --git a/src/server/BreCal/stubs/times_mooring.py b/src/server/BreCal/stubs/times_mooring.py index e69de29..5e6570a 100644 --- a/src/server/BreCal/stubs/times_mooring.py +++ b/src/server/BreCal/stubs/times_mooring.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_mooring(): + times_mooring = get_times_full_simple() + times_mooring.participant_type = ParticipantType.MOORING.value + return times_mooring + diff --git a/src/server/BreCal/stubs/times_pilot.py b/src/server/BreCal/stubs/times_pilot.py index e69de29..1282133 100644 --- a/src/server/BreCal/stubs/times_pilot.py +++ b/src/server/BreCal/stubs/times_pilot.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_pilot(): + times_pilot = get_times_full_simple() + times_pilot.participant_type = ParticipantType.PILOT.value + return times_pilot + diff --git a/src/server/BreCal/stubs/times_portauthority.py b/src/server/BreCal/stubs/times_portauthority.py index e69de29..61ad4a5 100644 --- a/src/server/BreCal/stubs/times_portauthority.py +++ b/src/server/BreCal/stubs/times_portauthority.py @@ -0,0 +1,9 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_port_authority(): + times_port_authority = get_times_full_simple() + times_port_authority.participant_type = ParticipantType.PORT_ADMINISTRATION.value + return times_port_authority + + diff --git a/src/server/BreCal/stubs/times_terminal.py b/src/server/BreCal/stubs/times_terminal.py index e69de29..549ddb5 100644 --- a/src/server/BreCal/stubs/times_terminal.py +++ b/src/server/BreCal/stubs/times_terminal.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_terminal(): + times_terminal = get_times_full_simple() + times_terminal.participant_type = ParticipantType.TERMINAL.value + return times_terminal + diff --git a/src/server/BreCal/stubs/times_tug.py b/src/server/BreCal/stubs/times_tug.py new file mode 100644 index 0000000..0e8cdd1 --- /dev/null +++ b/src/server/BreCal/stubs/times_tug.py @@ -0,0 +1,8 @@ +from BreCal.stubs.times_full import get_times_full_simple +from BreCal.database.enums import ParticipantType + +def get_times_tug(): + times_tug = get_times_full_simple() + times_tug.participant_type = ParticipantType.TUG.value + return times_tug + diff --git a/src/server/BreCal/validators/validation_rule_functions.py b/src/server/BreCal/validators/validation_rule_functions.py index 9e51eaf..fec7a7c 100644 --- a/src/server/BreCal/validators/validation_rule_functions.py +++ b/src/server/BreCal/validators/validation_rule_functions.py @@ -73,17 +73,22 @@ class ValidationRuleBaseFunctions(): when the query_time lays in the future, the delta is positive returns a violation state depending on whether the delta is - Violation, if: 0 >= delta > threshold + Violation, if: 0 >= delta <= threshold When the key time is defined (not None), there is no violation. Returns False options: query_time: will be used to measure the time difference of 'now' until the query time key_time: will be used to check, whether the respective key already has a value - threshold: threshold where a time difference becomes crucial. When the delta is below the threshold, a violation might occur + threshold: threshold where a time difference becomes crucial. When the delta is below the threshold, a violation might occur (minutes) """ # rule is not applicable -> return 'GREEN' - if self.check_is_not_a_time_or_is_none(key_time) or self.check_is_not_a_time_or_is_none(query_time): + # rule is only applicable, when 'key_time' is not defined (neither None, nor pd.NaT) + if (key_time is not None) and (key_time is not pd.NaT): + return False + + # when query_time is not valid, the rule cannot be applied + if self.check_is_not_a_time_or_is_none(query_time): return False # otherwise, this rule applies and the difference between 'now' and the query time is measured @@ -91,7 +96,7 @@ class ValidationRuleBaseFunctions(): # a violation occurs, when the delta (in minutes) exceeds the specified threshold of a participant # to prevent past-events from triggering violations, negative values are ignored - # Violation, if 0 >= delta >= threshold + # Violation, if 0 <= delta <= threshold violation_state = (delta >= 0) and (delta<=threshold) return violation_state @@ -141,7 +146,7 @@ class ValidationRuleBaseFunctions(): violation_state = n_unique_times!=1 return violation_state - def check_unique_shipcall_counts(self, query:str, rounding="min", maximum_threshold=3)->bool: + def check_unique_shipcall_counts(self, query:str, times_agency:pd.DataFrame, rounding="min", maximum_threshold=3, all_times_agency=None)->bool: """ # base function for all validation rules in the group {0005} A&B @@ -150,10 +155,11 @@ class ValidationRuleBaseFunctions(): """ # filter the df: keep only times_agents # filter out all NaN and NaT entries - times_agency = self.sql_handler.get_times_for_agency(non_null_column=query) + if all_times_agency is None: + all_times_agency = self.sql_handler.get_times_for_agency(non_null_column=query) - # get values and optionally round the values - (values, unique, counts) = self.sql_handler.get_unique_ship_counts(all_df_times=times_agency, query=query, rounding=rounding, maximum_threshold=maximum_threshold) + # get values and optionally round the values (internally) + counts = self.sql_handler.get_unique_ship_counts(all_df_times=all_times_agency, times_agency=times_agency, query=query, rounding=rounding, maximum_threshold=maximum_threshold) # when ANY of the unique values exceeds the threshold, a violation is observed violation_state = np.any(np.greater(counts, maximum_threshold)) @@ -651,7 +657,7 @@ class ValidationRuleFunctions(ValidationRuleBaseFunctions): return (StatusFlags.GREEN, None) # check, whether the start of operations is AFTER the estimated arrival time - violation_state = times_terminal.operations_start StatusFlags.GREEN.value, f"a violation must be identified" - assert description is not None, f"a violation description must be identified" + threshold = 60*5 + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert not violation_state, f"the key time is filled in, so there should not be a violation" return +def test_check_time_delta_violation_query_time_to_now_no_key_time_but_event_in_distant_future(build_sql_proxy_connection): + vr = build_sql_proxy_connection["vr"] + + # ship arrives in three hours, while the threshold for an alert is (e.g.) 5 hours + # key time is given, so the function should always return False (no violation) + # query time (-> delta) & threshold have the same time -> no violation + query_time = datetime.datetime.now() + datetime.timedelta(hours=5, seconds=10) # when the delta & threshold are identical, microseconds between checking the time and defining it here, raise the violation + key_time = None + + threshold = 60*5 + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert not violation_state, f"the event is still far enough away, so there should not be a violation." + return + +def test_check_time_delta_violation_query_time_to_now_key_time_is_none(build_sql_proxy_connection): + vr = build_sql_proxy_connection["vr"] + + # ship arrives in three hours, while the threshold for an alert is (e.g.) 5 hours + # key time is given, so the function should always return False (no violation) + query_time = datetime.datetime.now() + datetime.timedelta(hours=3) + key_time = None + + threshold = 60*5 # minutes + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert violation_state, f"when the key time is not filled in and the query time is 'dangerously close', there should be a violation to indicate the traffic state" + return + +def test_check_time_delta_violation_query_time_to_now_key_time_is_pd_nat(build_sql_proxy_connection): + vr = build_sql_proxy_connection["vr"] + + # ship arrives in three hours, while the threshold for an alert is (e.g.) 5 hours + # key time is given, so the function should always return False (no violation) + query_time = datetime.datetime.now() + datetime.timedelta(hours=3) + key_time = pd.NaT + + threshold = 60*5 # minutes + violation_state = vr.check_time_delta_violation_query_time_to_now(query_time=query_time, key_time=key_time, threshold=threshold) + assert violation_state, f"when the key time is not filled in and the query time is 'dangerously close', there should be a violation to indicate the traffic state" + return + + + +def test_validation_rule_fct_missing_time_agency_berth_eta__missing_time_agency_no_violation(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # artificially remove the agency, so the function is properly checked + df_times = df_times.loc[df_times["participant_type"]!=ParticipantType.AGENCY.value] + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the agency's entry is not present" + assert msg is None, f"with a 'green' state, there should be no message returned" + return + +def test_validation_rule_fct_missing_time_agency_berth_eta__shipcall_eta_dangerously_close_no_times_agency(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall happens 'soon' + shipcall.eta = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.AGENCY-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_agency_berth_eta__shipcall_eta_distant_enough_no_times_agency(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall happens 'soon' + shipcall.eta = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.AGENCY+10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_agency_berth_eta__shipcall_eta_is_undefined_agency_eta_is_defined(build_sql_proxy_connection): + """0001-A validation_rule_fct_missing_time_agency_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall is undefined + shipcall.eta = None + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_agency_berth_etd__shipcall_etd_is_undefined_agency_etd_is_defined(build_sql_proxy_connection): + """0001-B validation_rule_fct_missing_time_agency_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # the shipcall etd is 'soon' + shipcall.etd = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.AGENCY-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_agency_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the agency's entry is not present and the shipcall takes place soon" + return + +def test_validation_rule_fct_missing_time_mooring_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-C validation_rule_fct_missing_time_mooring_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.MOORING-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.MOORING.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_mooring_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_mooring_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-D validation_rule_fct_missing_time_mooring_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.MOORING-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.MOORING.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_mooring_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_portadministration_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-F validation_rule_fct_missing_time_portadministration_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PORT_ADMINISTRATION-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PORT_ADMINISTRATION.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_portadministration_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_portadministration_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-G validation_rule_fct_missing_time_portadministration_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PORT_ADMINISTRATION-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PORT_ADMINISTRATION.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_portadministration_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + +def test_validation_rule_fct_missing_time_pilot_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-H validation_rule_fct_missing_time_pilot_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PILOT-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_pilot_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + +def test_validation_rule_fct_missing_time_pilot_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-I validation_rule_fct_missing_time_pilot_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.PILOT-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.PILOT.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_pilot_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_tug_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-J validation_rule_fct_missing_time_tug_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TUG-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TUG.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_tug_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_tug_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-K validation_rule_fct_missing_time_tug_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TUG-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TUG.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_tug_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_terminal_berth_eta__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-L validation_rule_fct_missing_time_terminal_berth_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TERMINAL-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "eta_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_terminal_berth_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + + +def test_validation_rule_fct_missing_time_terminal_berth_etd__shipcall_soon_but_participant_estimated_time_undefined(build_sql_proxy_connection): + """0001-M validation_rule_fct_missing_time_terminal_berth_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + # according to the agency, a shipcall takes place soon (ETA/ETD) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = datetime.datetime.now() + datetime.timedelta(minutes=ParticipantwiseTimeDelta.TERMINAL-10) + + # set times agency to be undetermined + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "etd_berth"] = None + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_missing_time_terminal_berth_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.YELLOW, f"function should return 'yellow', because the participant did not provide a time and the shipcall takes place soon (according to the agency)" + return + + +def test_validation_rule_fct_shipcall_incoming_participants_disagree_on_eta__participants_disagree_on_time_but_different_shipcall_type(build_sql_proxy_connection): + """0002-A validation_rule_fct_shipcall_incoming_participants_disagree_on_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "eta_berth" + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_incoming_participants_disagree_on_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the shipcall type does not match the function" + return + + +def test_validation_rule_fct_shipcall_incoming_participants_disagree_on_eta__participants_disagree_on_time(build_sql_proxy_connection): + """0002-A validation_rule_fct_shipcall_incoming_participants_disagree_on_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to match the function + query = "eta_berth" + shipcall.type = ShipcallType.INCOMING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_incoming_participants_disagree_on_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.RED, f"function should return 'red', because agency and pilot disagree on the query" + return + +def test_validation_rule_fct_shipcall_incoming_participants_disagree_on_eta__participants_agree_on_time(build_sql_proxy_connection): + """0002-A validation_rule_fct_shipcall_incoming_participants_disagree_on_eta""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "eta_berth" + shipcall.type = ShipcallType.INCOMING.value + df_times = get_df_times(shipcall) + df_times.loc[:,query] = datetime.datetime.now() + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_incoming_participants_disagree_on_eta(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the participants fully agree on the time" + return + +def test_validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd__participants_disagree_on_time_but_different_shipcall_type(build_sql_proxy_connection): + """0002-B validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "etd_berth" + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the shipcall type does not match the function" + return + + +def test_validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd__participants_disagree_on_time(build_sql_proxy_connection): + """0002-B validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to match the function + query = "etd_berth" + shipcall.type = ShipcallType.OUTGOING.value + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.RED, f"function should return 'red', because agency and pilot disagree on the query" + return + +def test_validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd__participants_agree_on_time(build_sql_proxy_connection): + """0002-B validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + + # set shipcall type to NOT match the function -> returns 'green' + query = "etd_berth" + shipcall.type = ShipcallType.OUTGOING.value + df_times = get_df_times(shipcall) + df_times.loc[:,query] = datetime.datetime.now() + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_outgoing_participants_disagree_on_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the participants fully agree on the time" + return + + + +def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd__participants_disagree_on_time_but_different_shipcall_type(build_sql_proxy_connection): + """0002-C validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + shipcall.type = ShipcallType.INCOMING.value + df_times = get_df_times(shipcall) + + # set shipcall type to match the function + query = "eta_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # set shipcall type to match the function + query = "etd_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the shipcall type does not match the function" + return + +def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd__participants_disagree_on_time(build_sql_proxy_connection): + """0002-C validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times(shipcall) + + # set shipcall type to match the function + query = "eta_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # set shipcall type to match the function + query = "etd_berth" + df_times = get_df_times_participants_disagree(query=query, shipcall=shipcall, df_times=df_times) # makes sure that there is disagreement among participants + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.RED, f"function should return 'red', because agency and pilot disagree on the query" + return + +def test_validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd__participants_agree_on_time(build_sql_proxy_connection): + """0002-C validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd""" + vr = build_sql_proxy_connection['vr'] + + shipcall = get_shipcall_simple() + shipcall.type = ShipcallType.SHIFTING.value + df_times = get_df_times(shipcall) + + # set shipcall type to match the function + query = "eta_berth" + df_times.loc[:,query] = datetime.datetime.now() + + # set shipcall type to match the function + query = "etd_berth" + df_times.loc[:,query] = datetime.datetime.now() + + # apply the validation rule + (state, msg) = vr.validation_rule_fct_shipcall_shifting_participants_disagree_on_eta_or_etd(shipcall=shipcall, df_times=df_times) + + # expectation: green state, no msg + assert state==StatusFlags.GREEN, f"function should return 'green', because the participants fully agree on the time" + return + +def test_validation_rule_fct_eta_time_not_in_operation_window__times_dont_match(build_sql_proxy_connection): + """0003-A validation_rule_fct_eta_time_not_in_operation_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() # reference time for easier readability + + # the planned operations_start is before eta_berth (by one minute in this case) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=1) + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_start"] = t0_time + datetime.timedelta(minutes=0) + + (code, msg) = vr.validation_rule_fct_eta_time_not_in_operation_window(shipcall, df_times) + assert code==StatusFlags.RED, f"status flag should be 'red', because the planned operations start is BEFORE the estimated time of arrival for the shipcall" + return + +def test_validation_rule_fct_etd_time_not_in_operation_window__times_dont_match(build_sql_proxy_connection): + """0003-B validation_rule_fct_etd_time_not_in_operation_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() # reference time for easier readability + + # the planned operations_end is after etd_berth (by one minute in this case) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1) + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_end"] = t0_time+datetime.timedelta(hours=1, minutes=1) + + + (code, msg) = vr.validation_rule_fct_etd_time_not_in_operation_window(shipcall, df_times) + assert code==StatusFlags.RED, f"status flag should be 'red', because the planned operations end is AFTER the estimated time of departure for the shipcall" + return + +def test_validation_rule_fct_eta_time_not_in_operation_window_and_validation_rule_fct_etd_time_not_in_operation_window__always_okay(build_sql_proxy_connection): + """ + 0003-A validation_rule_fct_eta_time_not_in_operation_window + 0003-B validation_rule_fct_etd_time_not_in_operation_window + """ + vr = build_sql_proxy_connection['vr'] + import random + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # 10 random permutations of None/pd.NaT/suitable values + # each of these combinations is okay and should return a 'green' state + for _i in range(10): + # eta_berth & operations start + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = random.sample([None, pd.NaT, t0_time + datetime.timedelta(minutes=0)],k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_start"] = random.sample([None, pd.NaT, t0_time + datetime.timedelta(minutes=0)], k=1)[0] + + # etd_berth & operations start + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = random.sample([None, pd.NaT, t0_time + datetime.timedelta(hours=1)],k=1)[0] + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "operations_end"] = random.sample([None, pd.NaT, t0_time+datetime.timedelta(hours=1)],k=1)[0] + + (code, msg) = vr.validation_rule_fct_eta_time_not_in_operation_window(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status flag should be 'green', as any of these perturbations sets operation & estimated time to be on par ot one the values missed" + (code, msg) = vr.validation_rule_fct_etd_time_not_in_operation_window(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status flag should be 'green', as any of these perturbations sets operation & estimated time to be on par ot one the values missed" + return + +def test_validation_rule_fct_eta_time_not_in_tidal_window__is_okay(build_sql_proxy_connection): + """0004-A validation_rule_fct_eta_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # eta berth: + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=1) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because eta_berth matches precisely the tidal_window_from" + + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(hours=1) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because eta_berth matches precisely the tidal_window_to (in this case, the etd_berth would likely cause an issue)" + return + +def test_validation_rule_fct_eta_time_not_in_tidal_window__eta_outside_tidal_window(build_sql_proxy_connection): + """0004-A validation_rule_fct_eta_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # eta berth: t0+0min + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(minutes=0) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', eta_berth takes place before the tidal window" + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "eta_berth"] = t0_time + datetime.timedelta(hours=1, minutes=1) + (code, msg) = vr.validation_rule_fct_eta_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', eta_berth takes place after the tidal window" + return + + + +def test_validation_rule_fct_etd_time_not_in_tidal_window__is_okay(build_sql_proxy_connection): + """0004-B validation_rule_fct_etd_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # etd berth: + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(minutes=1) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because etd_berth matches precisely the tidal_window_from" + + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.GREEN, f"state should be 'green', because etd_berth matches precisely the tidal_window_to (in this case, the etd_berth would likely cause an issue)" + return + +def test_validation_rule_fct_etd_time_not_in_tidal_window__etd_outside_tidal_window(build_sql_proxy_connection): + """0004-B validation_rule_fct_etd_time_not_in_tidal_window""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + + t0_time = datetime.datetime.now() + + # tidal window: [t0 +1min, t0 +1hr) + # etd berth: + shipcall.tidal_window_from = t0_time + datetime.timedelta(minutes=1) + shipcall.tidal_window_to = t0_time + datetime.timedelta(hours=1) + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(minutes=0) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', etd_berth takes place before the tidal window" + + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "etd_berth"] = t0_time + datetime.timedelta(hours=1, minutes=1) + (code, msg) = vr.validation_rule_fct_etd_time_not_in_tidal_window(shipcall, df_times) + assert code==StatusFlags.RED, f"state should be 'red', etd_berth takes place after the tidal window" + return + +def test_validation_rule_fct_too_many_identical_eta_times__is_violated_by_too_many_identical_times(build_sql_proxy_connection): + """0005-A validation_rule_fct_too_many_identical_eta_times""" + vr = build_sql_proxy_connection['vr'] + query = "eta_berth" + + reference_time = pd.Timestamp(datetime.datetime.now()) + reference_time = reference_time.round("min") + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value] + df_times.loc[:,query] = reference_time + datetime.timedelta(seconds=12) + + all_times_df = build_stub_df_times(shipcall, query, reference_time) + (code, msg) = vr.validation_rule_fct_too_many_identical_eta_times(shipcall=shipcall, df_times=df_times, all_times_agency=all_times_df) + assert code == StatusFlags.YELLOW, f"status should be 'yellow', because the artificial 'all_times_df' contains five shipcalls with identical times, which exceeds the threshold" + return + +def test_validation_rule_fct_too_many_identical_etd_times__is_violated_by_too_many_identical_times(build_sql_proxy_connection): + """0005-B validation_rule_fct_too_many_identical_etd_times""" + vr = build_sql_proxy_connection['vr'] + query = "etd_berth" + + reference_time = pd.Timestamp(datetime.datetime.now()) + reference_time = reference_time.round("min") + + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times = df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value] + df_times.loc[:,query] = reference_time + datetime.timedelta(seconds=12) + + all_times_df = build_stub_df_times(shipcall, query, reference_time) + (code, msg) = vr.validation_rule_fct_too_many_identical_etd_times(shipcall=shipcall, df_times=df_times, all_times_agency=all_times_df) + assert code == StatusFlags.YELLOW, f"status should be 'yellow', because the artificial 'all_times_df' contains five shipcalls with identical times, which exceeds the threshold" + return + +def test_validation_rule_fct_agency_and_terminal_berth_id_disagreement__agency_and_terminal_agree(build_sql_proxy_connection): + """0006-A validation_rule_fct_agency_and_terminal_berth_id_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "berth_id"] = 143 + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "berth_id"] = 143 + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_berth_id_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status should be 'green', because agency and terminal agree on the selected berth id" + return + +def test_validation_rule_fct_agency_and_terminal_berth_id_disagreement__agency_and_terminal_disagree(build_sql_proxy_connection): + """0006-A validation_rule_fct_agency_and_terminal_berth_id_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "berth_id"] = 143 + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "berth_id"] = 145 + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_berth_id_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.YELLOW, f"status should be 'yellow', because agency and terminal do not agree on the selected berth id" + return + +def test_validation_rule_fct_agency_and_terminal_pier_side_disagreement__agency_and_terminal_agree(build_sql_proxy_connection): + """0006-B validation_rule_fct_agency_and_terminal_pier_side_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "pier_side"] = True + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "pier_side"] = True + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_pier_side_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.GREEN, f"status should be 'green', because agency and terminal agree on the selected pier side" + return + +def test_validation_rule_fct_agency_and_terminal_pier_side_disagreement__agency_and_terminal_disagree(build_sql_proxy_connection): + """0006-B validation_rule_fct_agency_and_terminal_pier_side_disagreement""" + vr = build_sql_proxy_connection['vr'] + shipcall = get_shipcall_simple() + df_times = get_df_times(shipcall) + df_times.loc[df_times["participant_type"]==ParticipantType.AGENCY.value, "pier_side"] = True + df_times.loc[df_times["participant_type"]==ParticipantType.TERMINAL.value, "pier_side"] = False + + (code, msg) = vr.validation_rule_fct_agency_and_terminal_pier_side_disagreement(shipcall=shipcall, df_times=df_times) + assert code==StatusFlags.YELLOW, f"status should be 'yellow', because agency and terminal do not agree on the selected pier side" + return + + def test_validation_rule_fct_agency_and_terminal_pier_side_agreement(build_sql_proxy_connection): """#0006-A validation_rule_fct_agency_and_terminal_pier_side_disagreement""" @@ -89,8 +853,36 @@ def test_validation_rule_fct_agency_and_terminal_pier_side_agreement(build_sql_p assert description is None, f"no violation should be observed" return +def test_validation_rule_fct_agency_and_terminal_pier_side_disagreement(build_sql_proxy_connection): + """#0006-A validation_rule_fct_agency_and_terminal_pier_side_disagreement""" + import pandas as pd + from BreCal.stubs.times_full import get_times_full_simple + from BreCal.stubs.shipcall import get_shipcall_simple + from BreCal.database.enums import ParticipantType + from BreCal.database.enums import StatusFlags + vr = build_sql_proxy_connection["vr"] + shipcall = get_shipcall_simple() + t1 = get_times_full_simple() + t2 = get_times_full_simple() + + # roles: agency & terminal + t1.participant_type = ParticipantType.AGENCY.value + t2.participant_type = ParticipantType.TERMINAL.value + + # disagreement + t1.pier_side = True + t2.pier_side = False + + time_objects = [t1, t2] + df_times = pd.DataFrame.from_records([to_.__dict__ for to_ in time_objects]) + df_times.set_index('id',inplace=True) + + (state, description) = vr.validation_rule_fct_agency_and_terminal_pier_side_disagreement(shipcall, df_times) + assert state.value > StatusFlags.GREEN.value, f"a violation must be identified" + assert description is not None, f"a violation description must be identified" + return def test_validation_rule_fct_agency_and_terminal_berth_id_disagreement(build_sql_proxy_connection): """#0006-B validation_rule_fct_agency_and_terminal_pier_side_disagreement"""