1717from pathlib import Path
1818from enum import Enum
1919from dotenv import load_dotenv , set_key , unset_key
20- from datetime import datetime
20+ from datetime import datetime , timedelta
2121import pandas as pd
2222import shutil
2323import locale
@@ -180,7 +180,7 @@ def read_parquet_file(filename):
180180
181181def read_file (filename , base_dir = None ):
182182 filepath = os .path .join (base_dir , filename ) if base_dir else filename
183-
183+
184184 if filename .endswith ('.json' ):
185185 return read_json_file (filepath )
186186 elif filename .endswith ('.csv' ):
@@ -247,6 +247,16 @@ def is_unix() -> bool:
247247def is_windows () -> bool :
248248 return platform .system () == "Windows"
249249
250+ def date_windows_locale_str (dt : datetime ) -> str :
251+ if locale .getlocale (locale .LC_TIME ) == (None , None ):
252+ locale .setlocale (locale .LC_TIME , "" )
253+ return dt .strftime ("%x" )
254+
255+ def time_windows_locale_str (dt : datetime ) -> str :
256+ if locale .getlocale (locale .LC_TIME ) == (None , None ):
257+ locale .setlocale (locale .LC_TIME , "" )
258+ return dt .strftime ("%X" )
259+
250260def schedule_on_unix (script_path , args , interval_minutes , prefix = SCHEDULE_PREFIX ):
251261 job_name = f"{ prefix } _{ datetime .now ().strftime ('%Y%m%d-%H%M' )} "
252262 cron_expr = f"*/{ interval_minutes } * * * *"
@@ -260,19 +270,29 @@ def schedule_on_unix(script_path, args, interval_minutes, prefix=SCHEDULE_PREFIX
260270 print (f"Cron job scheduled every { interval_minutes } minutes on Unix." )
261271
262272def schedule_on_windows (script_path , args , start_dt , end_dt , interval_minutes , prefix = SCHEDULE_PREFIX ):
263- locale .setlocale (locale .LC_TIME , '' )
264-
265273 if shutil .which ("schtasks" ) is None :
266274 raise RuntimeError ("Windows Task Scheduler (schtasks) not found." )
267275
268276 if interval_minutes < 1 or interval_minutes > 1439 :
269277 raise ValueError ("Interval must be between 1 and 1439 minutes on Windows." )
270278
279+ # Workaround on schtasks.exe to satisfy:
280+ # (end_time - start_time) > interval
281+ # only when end_time > start_time
282+ start_time = datetime (start_dt .year , start_dt .month , start_dt .day , start_dt .hour , start_dt .minute )
283+ end_time = datetime (start_dt .year , start_dt .month , start_dt .day , end_dt .hour , end_dt .minute )
284+
285+ if end_time >= start_time :
286+ diff = end_time - start_time
287+ if diff <= timedelta (minutes = interval_minutes ):
288+ compensation = (timedelta (minutes = interval_minutes + 1 ) - diff )
289+ end_time = end_time + compensation
290+
271291 task_name = f"{ prefix } _{ datetime .now ().strftime ('%Y%m%d-%H%M' )} "
272- start_time = start_dt . strftime ( "%X" )
273- start_date = start_dt . strftime ( "%x" )
274- end_time = end_dt . strftime ( "%X" )
275- end_date = end_dt . strftime ( "%x" )
292+ start_date_str = date_windows_locale_str ( start_dt )
293+ start_time_str = time_windows_locale_str ( start_time )
294+ end_date_str = date_windows_locale_str ( end_dt )
295+ end_time_str = time_windows_locale_str ( end_time )
276296
277297 quoted_args = ' ' .join (args )
278298 full_cmd = f'{ script_path } { quoted_args } '
@@ -285,10 +305,10 @@ def schedule_on_windows(script_path, args, start_dt, end_dt, interval_minutes, p
285305 "/TR" , full_cmd ,
286306 "/SC" , "MINUTE" ,
287307 "/MO" , str (interval_minutes ),
288- "/ST" , start_time ,
289- "/SD" , start_date ,
290- "/ED" , end_date ,
291- "/ET" , end_time ,
308+ "/ST" , start_time_str ,
309+ "/SD" , start_date_str ,
310+ "/ED" , end_date_str ,
311+ "/ET" , end_time_str ,
292312 "/F" ,
293313 "/RL" , "LIMITED"
294314 ]
@@ -299,7 +319,7 @@ def schedule_on_windows(script_path, args, start_dt, end_dt, interval_minutes, p
299319 except subprocess .CalledProcessError as e :
300320 print ("Failed to schedule task:" , e )
301321 raise Exception ("Failed to schedule task" )
302-
322+
303323def remove_cron_jobs_with_prefix (prefix = SCHEDULE_PREFIX ):
304324 result = subprocess .run ("crontab -l" , shell = True , capture_output = True , text = True )
305325 if result .returncode != 0 :
0 commit comments