Skip to content

Commit dd2dd38

Browse files
committed
Make time related param consistent
1 parent b4f7d44 commit dd2dd38

2 files changed

Lines changed: 89 additions & 44 deletions

File tree

dagfactory/dagbuilder.py

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
import re
99
import warnings
1010
from copy import deepcopy
11-
from datetime import datetime, timedelta
11+
from datetime import timedelta
1212
from functools import partial, reduce
1313
from typing import Any, Callable, Dict, List, Tuple, Union
1414

1515
from airflow import configuration
1616

17+
from dagfactory.utils import convert_to_datetime_datetime, convert_to_timedelta
18+
1719
try:
1820
from airflow.sdk.bases.operator import BaseOperator
1921
from airflow.sdk.definitions.dag import DAG
@@ -145,27 +147,28 @@ def get_dag_params(self) -> Dict[str, Any]:
145147
if utils.check_dict_key(dag_params, "schedule_interval") and dag_params["schedule_interval"] == "None":
146148
dag_params["schedule_interval"] = None
147149

148-
# Convert from 'dagrun_timeout_sec: int' to 'dagrun_timeout: timedelta'
149-
if utils.check_dict_key(dag_params, "dagrun_timeout_sec"):
150-
dag_params["dagrun_timeout"]: timedelta = timedelta(seconds=dag_params["dagrun_timeout_sec"])
151-
del dag_params["dagrun_timeout_sec"]
150+
# Convert from 'dagrun_timeout: int' to 'dagrun_timeout: timedelta'
151+
if utils.check_dict_key(dag_params, "dagrun_timeout"):
152+
if isinstance(dag_params["dagrun_timeout"], int):
153+
dag_params["dagrun_timeout"]: timedelta = timedelta(seconds=dag_params["dagrun_timeout"])
152154

153-
# Convert from 'end_date: Union[str, datetime, date]' to 'end_date: datetime'
154-
if utils.check_dict_key(dag_params["default_args"], "end_date"):
155-
dag_params["default_args"]["end_date"]: datetime = utils.get_datetime(
156-
date_value=dag_params["default_args"]["end_date"],
157-
timezone=dag_params["default_args"].get("timezone", "UTC"),
155+
if dag_params["default_args"]["start_date"] and isinstance(dag_params["default_args"]["start_date"], str):
156+
dag_params["default_args"]["start_date"] = convert_to_datetime_datetime(
157+
dag_params["default_args"]["start_date"]
158158
)
159159

160-
if utils.check_dict_key(dag_params["default_args"], "retry_delay_sec"):
161-
dag_params["default_args"]["retry_delay"]: timedelta = timedelta(
162-
seconds=dag_params["default_args"]["retry_delay_sec"]
160+
if dag_params["default_args"]["end_date"] and isinstance(dag_params["default_args"]["end_date"], str):
161+
dag_params["default_args"]["end_date"] = convert_to_datetime_datetime(
162+
dag_params["default_args"]["end_date"]
163163
)
164-
del dag_params["default_args"]["retry_delay_sec"]
165164

166-
if utils.check_dict_key(dag_params["default_args"], "sla_secs"):
167-
dag_params["default_args"]["sla"]: timedelta = timedelta(seconds=dag_params["default_args"]["sla_secs"])
168-
del dag_params["default_args"]["sla_secs"]
165+
if utils.check_dict_key(dag_params["default_args"], "retry_delay"):
166+
if isinstance(dag_params["default_args"]["retry_delay"], int):
167+
dag_params["default_args"]["retry_delay"] = timedelta(seconds=dag_params["default_args"]["retry"])
168+
169+
if utils.check_dict_key(dag_params["default_args"], "sla"):
170+
if isinstance(dag_params["default_args"]["sla"], int):
171+
dag_params["default_args"]["sla"]: timedelta = timedelta(seconds=dag_params["default_args"]["sla"])
169172

170173
# Parse callbacks at the DAG-level and at the Task-level, configured in default_args. Note that the version
171174
# check has gone into the set_callback method
@@ -232,16 +235,12 @@ def get_dag_params(self) -> Dict[str, Any]:
232235
else:
233236
raise DagFactoryException("render_template_as_native_obj should be bool type!")
234237

235-
try:
236-
# ensure that default_args dictionary contains key "start_date"
237-
# with "datetime" value in specified timezone
238-
dag_params["default_args"]["start_date"]: datetime = utils.get_datetime(
239-
date_value=dag_params["default_args"]["start_date"],
240-
timezone=dag_params["default_args"].get("timezone", "UTC"),
241-
)
242-
except KeyError as err:
243-
# pylint: disable=line-too-long
244-
raise DagFactoryConfigException(f"{self.dag_name} config is missing start_date") from err
238+
if dag_params.get("start_date") and isinstance(dag_params["start_date"], str):
239+
dag_params["start_date"] = convert_to_datetime_datetime(dag_params["start_date"])
240+
241+
if dag_params.get("end_date") and isinstance(dag_params["end_date"], str):
242+
dag_params["end_date"] = convert_to_datetime_datetime(dag_params["end_date"])
243+
245244
return dag_params
246245

247246
@staticmethod
@@ -854,10 +853,7 @@ def build(self) -> Dict[str, Union[str, DAG]]:
854853
)
855854

856855
if dag_params.get("timetable"):
857-
timetable_args = dag_params.get("timetable")
858-
dag_kwargs["timetable"] = DagBuilder.make_timetable(
859-
timetable_args.get("callable"), timetable_args.get("params")
860-
)
856+
dag_kwargs["timetable"] = dag_params.get("timetable")
861857

862858
dag_kwargs["catchup"] = dag_params.get(
863859
"catchup", configuration.conf.getboolean("scheduler", "catchup_by_default")
@@ -1020,27 +1016,27 @@ def topological_sort_tasks(tasks_configs: dict[str, Any]) -> list[tuple(str, Any
10201016
return sorted_tasks
10211017

10221018
@staticmethod
1023-
def adjust_general_task_params(task_params: dict(str, Any)):
1019+
def adjust_general_task_params(task_params: dict[str, Any]):
10241020
"""Adjusts in place the task params argument"""
1025-
if utils.check_dict_key(task_params, "execution_timeout_secs"):
1026-
task_params["execution_timeout"]: timedelta = timedelta(seconds=task_params["execution_timeout_secs"])
1027-
del task_params["execution_timeout_secs"]
1021+
if utils.check_dict_key(task_params, "execution_timeout"):
1022+
if isinstance(task_params["execution_timeout"], int):
1023+
task_params["execution_timeout"]: timedelta = timedelta(seconds=task_params["execution_timeout"])
10281024

1029-
if utils.check_dict_key(task_params, "sla_secs"):
1030-
task_params["sla"]: timedelta = timedelta(seconds=task_params["sla_secs"])
1031-
del task_params["sla_secs"]
1025+
if utils.check_dict_key(task_params, "sla"):
1026+
if isinstance(task_params["sla"], int):
1027+
task_params["sla"]: timedelta = timedelta(seconds=task_params["sla"])
10321028

1033-
if utils.check_dict_key(task_params, "execution_delta_secs"):
1034-
task_params["execution_delta"]: timedelta = timedelta(seconds=task_params["execution_delta_secs"])
1035-
del task_params["execution_delta_secs"]
1029+
if utils.check_dict_key(task_params, "execution_delta"):
1030+
if isinstance(task_params["execution_delta"], int):
1031+
task_params["execution_delta"]: timedelta = timedelta(seconds=task_params["execution_delta"])
10361032

10371033
# Used by airflow.sensors.external_task_sensor.ExternalTaskSensor
10381034
if utils.check_dict_key(task_params, "execution_date_fn"):
10391035
python_callable: Callable = import_string(task_params["execution_date_fn"])
10401036
task_params["execution_date_fn"] = python_callable
10411037
elif utils.check_dict_key(task_params, "execution_delta"):
1042-
execution_delta = utils.get_time_delta(task_params["execution_delta"])
1043-
task_params["execution_delta"] = execution_delta
1038+
task_params["execution_delta"] = convert_to_timedelta(task_params["execution_delta"])
1039+
10441040
elif utils.check_dict_key(task_params, "execution_date_fn_name") and utils.check_dict_key(
10451041
task_params, "execution_date_fn_file"
10461042
):
@@ -1174,7 +1170,7 @@ def make_decorator(
11741170
return decorator(**decorator_kwargs)(**callable_kwargs)
11751171

11761172
@staticmethod
1177-
def replace_kwargs_values_as_tasks(kwargs: dict(str, Any), tasks_dict: dict(str, Any)):
1173+
def replace_kwargs_values_as_tasks(kwargs: dict[str, Any], tasks_dict: dict[str, Any]):
11781174
for key, value in kwargs.items():
11791175
if isinstance(value, str) and value.startswith("+"):
11801176
upstream_task_name = value.split("+")[-1]

dagfactory/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,52 @@ def cast_with_type(data):
417417
return [cast_with_type(item) for item in data]
418418

419419
return data
420+
421+
422+
def convert_to_datetime_datetime(value: str) -> datetime:
423+
value_list = value.split("-")
424+
425+
if len(value_list) < 3 or len(value_list) > 8:
426+
raise ValueError(
427+
"Invalid 'datime' format: expected a complete date with at least year, month, and day (e.g., '2025-02-15')."
428+
)
429+
430+
candidate_tz = value_list[-1]
431+
timezone = "UTC"
432+
433+
try:
434+
# Check if the last element is part of the date/time (i.e., numeric)
435+
int(candidate_tz)
436+
time_parts = value_list
437+
except ValueError:
438+
# It's likely a timezone string
439+
timezone = pendulum.timezone(candidate_tz)
440+
time_parts = value_list[:-1]
441+
442+
# Convert all time parts to integers (e.g., year, month, day, hour, ...)
443+
try:
444+
time_parts = [int(val) for val in time_parts]
445+
except ValueError:
446+
raise ValueError("Date/time components must be integers (e.g., '2025-07-23-15-30').")
447+
448+
return datetime(*time_parts, tzinfo=timezone)
449+
450+
451+
def convert_to_timedelta(value: str) -> timedelta:
452+
"""
453+
Converts a hyphen-separated string to a timedelta.
454+
Expected format: 'days-seconds-microseconds-milliseconds-minutes-hours-weeks'
455+
You can provide as few as 1 and up to 7 integer values, in order.
456+
"""
457+
try:
458+
time_parts = value.split("-")
459+
time_parts = [int(val) for val in time_parts]
460+
return timedelta(*time_parts)
461+
except ValueError:
462+
raise ValueError("Invalid input: all parts must be integers.")
463+
except TypeError:
464+
raise TypeError(
465+
"Invalid values provided to timedelta().\n"
466+
"Expected format: 'days-seconds-microseconds-milliseconds-minutes-hours-weeks'\n"
467+
"Maximum 7 values allowed (e.g., '1-0-0-0-0-0-1')."
468+
)

0 commit comments

Comments
 (0)