This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 3b9df4f8c93 TimeDeltaSensor - delta templated (#71314)
3b9df4f8c93 is described below

commit 3b9df4f8c93a8fef16b0470863b782d82b613994
Author: raphaelauv <[email protected]>
AuthorDate: Wed Sep 9 14:17:24 2026 +0200

    TimeDeltaSensor - delta templated (#71314)
    
    * TimeDeltaSensor - delta templated
    
    * review 1 - add doc
    
    * review 2 - add test on templating error
    
    ---------
    
    Co-authored-by: raphaelauv <[email protected]>
---
 .../providers/standard/sensors/time_delta.py       | 19 ++++++++++----
 .../tests/unit/standard/sensors/test_time_delta.py | 30 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 5 deletions(-)

diff --git 
a/providers/standard/src/airflow/providers/standard/sensors/time_delta.py 
b/providers/standard/src/airflow/providers/standard/sensors/time_delta.py
index 0f9bcf45d07..1295a829bb3 100644
--- a/providers/standard/src/airflow/providers/standard/sensors/time_delta.py
+++ b/providers/standard/src/airflow/providers/standard/sensors/time_delta.py
@@ -48,7 +48,7 @@ class TimeDeltaSensor(BaseSensorOperator):
     The delta will be evaluated against data_interval_end if present for the 
dag run,
     otherwise run_after will be used.
 
-    :param delta: time to wait before succeeding.
+    :param delta: time to wait before succeeding. If a templated value is 
provided, it will be cast to an integer representing the duration in minutes.
     :param deferrable: Run sensor in deferrable mode. If set to True, task 
will defer itself to avoid taking up a worker slot while it is waiting.
 
     .. seealso::
@@ -57,10 +57,12 @@ class TimeDeltaSensor(BaseSensorOperator):
 
     """
 
+    template_fields: Sequence[str] = ("delta",)
+
     def __init__(
         self,
         *,
-        delta: timedelta,
+        delta: timedelta | int,
         deferrable: bool = conf.getboolean("operators", "default_deferrable", 
fallback=False),
         end_from_trigger: bool = False,
         **kwargs,
@@ -70,6 +72,12 @@ class TimeDeltaSensor(BaseSensorOperator):
         self.deferrable = deferrable
         self.end_from_trigger = end_from_trigger
 
+    def _resolve_delta(self) -> timedelta:
+        value = self.delta
+        if isinstance(value, timedelta):
+            return value
+        return timedelta(minutes=int(value))
+
     def _derive_base_time(self, context: Context) -> datetime:
         """
         Get the "base time" against which the delta should be calculated.
@@ -93,8 +101,9 @@ class TimeDeltaSensor(BaseSensorOperator):
 
     def poke(self, context: Context) -> bool:
         base_time = self._derive_base_time(context=context)
-        target_dttm = base_time + self.delta
-        self.log.info("Checking if the delta has elapsed base_time=%s, 
delta=%s", base_time, self.delta)
+        delta = self._resolve_delta()
+        target_dttm = base_time + delta
+        self.log.info("Checking if the delta has elapsed base_time=%s, 
delta=%s", base_time, delta)
         return timezone.utcnow() > target_dttm
 
     """
@@ -113,7 +122,7 @@ class TimeDeltaSensor(BaseSensorOperator):
 
         # Deferrable path
         base_time = self._derive_base_time(context=context)
-        target_dttm: datetime = base_time + self.delta
+        target_dttm: datetime = base_time + self._resolve_delta()
 
         if timezone.utcnow() > target_dttm:
             # If the target datetime is in the past, return immediately
diff --git a/providers/standard/tests/unit/standard/sensors/test_time_delta.py 
b/providers/standard/tests/unit/standard/sensors/test_time_delta.py
index 9ec8fc4233d..b0c201583b3 100644
--- a/providers/standard/tests/unit/standard/sensors/test_time_delta.py
+++ b/providers/standard/tests/unit/standard/sensors/test_time_delta.py
@@ -255,6 +255,36 @@ class TestTimeDeltaSensorAsync:
 
         assert caught.value.trigger.moment == expected_time
 
+    @pytest.mark.parametrize("delta", [timedelta(minutes=1), 1, "{{ 1*2 }}"])
+    def test_time_delta_sensor_templating(self, mocker, delta):
+        defer_mock = mocker.patch(DEFER_PATH)
+        op = TimeDeltaSensor(task_id="time_sensor_check", delta=delta, 
dag=self.dag, deferrable=True)
+
+        time = pendulum.datetime(year=2024, month=8, day=1, tz="UTC")
+
+        with time_machine.travel(time, tick=False):
+            data_interval_end = time.add(hours=1)
+
+            context = {"data_interval_end": data_interval_end}
+            op.render_template_fields(context)
+            op.execute(context)
+            defer_mock.assert_called_once()
+
+    def test_time_delta_sensor_templating_error(self, mocker):
+        op = TimeDeltaSensor(
+            task_id="time_sensor_check", delta="{{ 'nothing' }}", 
dag=self.dag, deferrable=True
+        )
+        time = pendulum.datetime(year=2024, month=8, day=1, tz="UTC")
+
+        with time_machine.travel(time, tick=False):
+            data_interval_end = time.add(hours=1)
+            context = {"data_interval_end": data_interval_end}
+            op.render_template_fields(context)
+            with pytest.raises(
+                ValueError, match=re.escape("invalid literal for int() with 
base 10: 'nothing'")
+            ):
+                op.execute(context)
+
     @pytest.mark.parametrize(
         "time_to_wait",
         [timedelta(minutes=1), 1, "{{ 1*2 }}"],

Reply via email to