11import dataclasses
22from collections .abc import Iterable
3- from datetime import datetime
3+ from datetime import date , datetime
44from typing import TypedDict
55from uuid import UUID
66
7+ import pandas as pd
8+
79from testgen .common import read_template_sql_file
810from testgen .common .clean_sql import concat_columns
911from testgen .common .database .database_service import get_flavor_service , get_tg_schema , replace_params
12+ from testgen .common .freshness_service import (
13+ count_excluded_minutes ,
14+ get_schedule_params ,
15+ is_excluded_day ,
16+ resolve_holiday_dates ,
17+ )
1018from testgen .common .models .connection import Connection
19+ from testgen .common .models .scheduler import JobSchedule
1120from testgen .common .models .table_group import TableGroup
1221from testgen .common .models .test_definition import TestRunType , TestScope
1322from testgen .common .models .test_run import TestRun
23+ from testgen .common .models .test_suite import TestSuite
1424from testgen .common .read_file import replace_templated_functions
1525from testgen .utils import to_sql_timestamp
1626
@@ -49,6 +59,7 @@ class TestExecutionDef(InputParameters):
4959 skip_errors : int
5060 history_calculation : str
5161 custom_query : str
62+ prediction : dict | str | None
5263 run_type : TestRunType
5364 test_scope : TestScope
5465 template : str
@@ -88,14 +99,27 @@ class TestExecutionSQL:
8899 "result_measure" ,
89100 )
90101
91- def __init__ (self , connection : Connection , table_group : TableGroup , test_run : TestRun ):
102+ def __init__ (self , connection : Connection , table_group : TableGroup , test_suite : TestSuite , test_run : TestRun ):
92103 self .connection = connection
93104 self .table_group = table_group
105+ self .test_suite = test_suite
94106 self .test_run = test_run
95107 self .run_date = test_run .test_starttime
96108 self .flavor = connection .sql_flavor
97109 self .flavor_service = get_flavor_service (self .flavor )
98110
111+ self ._exclude_weekends = bool (self .test_suite .predict_exclude_weekends )
112+ self ._holiday_dates : set [date ] | None = None
113+ self ._schedule_tz : str | None = None
114+ if test_suite .is_monitor :
115+ schedule = JobSchedule .get (JobSchedule .kwargs ["test_suite_id" ].astext == str (test_suite .id ))
116+ self ._schedule_tz = schedule .cron_tz or "UTC" if schedule else None
117+ if test_suite .holiday_codes_list :
118+ self ._holiday_dates = resolve_holiday_dates (
119+ test_suite .holiday_codes_list ,
120+ pd .DatetimeIndex ([datetime (self .run_date .year - 1 , 1 , 1 ), datetime (self .run_date .year + 1 , 12 , 31 )]),
121+ )
122+
99123 def _get_input_parameters (self , test_def : TestExecutionDef ) -> str :
100124 return "; " .join (
101125 f"{ field .name } ={ getattr (test_def , field .name )} "
@@ -135,8 +159,8 @@ def _get_params(self, test_def: TestExecutionDef | None = None) -> dict:
135159 "BASELINE_SUM" : test_def .baseline_sum ,
136160 "BASELINE_AVG" : test_def .baseline_avg ,
137161 "BASELINE_SD" : test_def .baseline_sd ,
138- "LOWER_TOLERANCE" : test_def .lower_tolerance or "NULL" ,
139- "UPPER_TOLERANCE" : test_def .upper_tolerance or "NULL" ,
162+ "LOWER_TOLERANCE" : "NULL" if test_def .lower_tolerance in ( None , "" ) else test_def . lower_tolerance ,
163+ "UPPER_TOLERANCE" : "NULL" if test_def .upper_tolerance in ( None , "" ) else test_def . upper_tolerance ,
140164 # SUBSET_CONDITION should be replaced after CUSTOM_QUERY
141165 # since the latter may contain the former
142166 "SUBSET_CONDITION" : test_def .subset_condition or "1=1" ,
@@ -154,6 +178,32 @@ def _get_params(self, test_def: TestExecutionDef | None = None) -> dict:
154178 "COLUMN_TYPE" : test_def .column_type ,
155179 "INPUT_PARAMETERS" : self ._get_input_parameters (test_def ),
156180 })
181+
182+ # Freshness exclusion params — computed per test at execution time
183+ if test_def .test_type == "Freshness_Trend" and test_def .baseline_sum :
184+ sched = get_schedule_params (test_def .prediction )
185+ has_exclusions = self ._exclude_weekends or sched .excluded_days or sched .window_start is not None
186+ if has_exclusions :
187+ last_update = pd .Timestamp (test_def .baseline_sum )
188+ excluded = int (count_excluded_minutes (
189+ last_update , self .run_date , self ._exclude_weekends , self ._holiday_dates ,
190+ tz = self ._schedule_tz , excluded_days = sched .excluded_days ,
191+ window_start = sched .window_start , window_end = sched .window_end ,
192+ ))
193+ is_excl = 1 if is_excluded_day (
194+ pd .Timestamp (self .run_date ), self ._exclude_weekends , self ._holiday_dates ,
195+ tz = self ._schedule_tz , excluded_days = sched .excluded_days ,
196+ window_start = sched .window_start , window_end = sched .window_end ,
197+ ) else 0
198+ params ["EXCLUDED_MINUTES" ] = excluded
199+ params ["IS_EXCLUDED_DAY" ] = is_excl
200+ else :
201+ params ["EXCLUDED_MINUTES" ] = 0
202+ params ["IS_EXCLUDED_DAY" ] = 0
203+ else :
204+ params ["EXCLUDED_MINUTES" ] = 0
205+ params ["IS_EXCLUDED_DAY" ] = 0
206+
157207 return params
158208
159209 def _get_query (
@@ -266,7 +316,7 @@ def aggregate_cat_tests(
266316 td .measure_expression = f"COALESCE(CAST({ measure } AS { varchar_type } ) { concat_operator } '|', '{ self .null_value } |')"
267317
268318 # For prediction mode, return -1 during training period
269- if td .history_calculation == "PREDICT" and (not td .lower_tolerance or not td .upper_tolerance ):
319+ if td .history_calculation == "PREDICT" and (td .lower_tolerance in ( None , "" ) or td .upper_tolerance in ( None , "" ) ):
270320 td .condition_expression = "'-1,'"
271321 else :
272322 condition = (
0 commit comments