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

o-nikolas 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 6b81cda9b23 Narrow AirflowException to specific exceptions in DataSync 
operator (#70152)
6b81cda9b23 is described below

commit 6b81cda9b23d677382879ae91a521792abe8f8fd
Author: Haseeb Malik <[email protected]>
AuthorDate: Fri Aug 14 20:29:43 2026 -0400

    Narrow AirflowException to specific exceptions in DataSync operator (#70152)
---
 generated/known_airflow_exceptions.txt             |  1 -
 .../src/airflow/providers/amazon/aws/exceptions.py | 24 ++++++
 .../providers/amazon/aws/operators/datasync.py     | 39 ++++++----
 .../unit/amazon/aws/operators/test_datasync.py     | 91 ++++++++++++++++++----
 4 files changed, 127 insertions(+), 28 deletions(-)

diff --git a/generated/known_airflow_exceptions.txt 
b/generated/known_airflow_exceptions.txt
index 8be3fafad4a..202df42602e 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -68,7 +68,6 @@ 
providers/amazon/src/airflow/providers/amazon/aws/operators/athena.py::3
 providers/amazon/src/airflow/providers/amazon/aws/operators/batch.py::5
 providers/amazon/src/airflow/providers/amazon/aws/operators/bedrock.py::6
 providers/amazon/src/airflow/providers/amazon/aws/operators/comprehend.py::2
-providers/amazon/src/airflow/providers/amazon/aws/operators/datasync.py::10
 providers/amazon/src/airflow/providers/amazon/aws/operators/dms.py::5
 providers/amazon/src/airflow/providers/amazon/aws/operators/ec2.py::1
 providers/amazon/src/airflow/providers/amazon/aws/operators/ecs.py::7
diff --git a/providers/amazon/src/airflow/providers/amazon/aws/exceptions.py 
b/providers/amazon/src/airflow/providers/amazon/aws/exceptions.py
index fc6d668ae74..9760067ce5b 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/exceptions.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/exceptions.py
@@ -82,3 +82,27 @@ class NeptuneImportTaskFailedError(AirflowException):
 
 class GlueJobRunStoppedError(AirflowException):
     """Raised when a Glue job run finishes in a state that is not a real 
success."""
+
+
+class DataSyncTaskNotFoundError(AirflowException):
+    """Raised when a DataSync task could not be identified or created for the 
requested locations."""
+
+
+class DataSyncMultipleTasksError(AirflowException):
+    """Raised when multiple DataSync tasks match and random task choice is not 
allowed."""
+
+
+class DataSyncMultipleLocationsError(AirflowException):
+    """Raised when multiple DataSync locations match and random location 
choice is not allowed."""
+
+
+class DataSyncLocationNotFoundError(AirflowException):
+    """Raised when a DataSync location could not be determined or created."""
+
+
+class DataSyncTaskCreationError(AirflowException):
+    """Raised when DataSync task creation did not return a task ARN."""
+
+
+class DataSyncTaskExecutionFailedError(AirflowException):
+    """Raised when a DataSync task execution could not be started or did not 
complete successfully."""
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/operators/datasync.py 
b/providers/amazon/src/airflow/providers/amazon/aws/operators/datasync.py
index 1b97ec399af..936949b7ab1 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/operators/datasync.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/datasync.py
@@ -23,6 +23,14 @@ import random
 from collections.abc import Sequence
 from typing import TYPE_CHECKING, Any
 
+from airflow.providers.amazon.aws.exceptions import (
+    DataSyncLocationNotFoundError,
+    DataSyncMultipleLocationsError,
+    DataSyncMultipleTasksError,
+    DataSyncTaskCreationError,
+    DataSyncTaskExecutionFailedError,
+    DataSyncTaskNotFoundError,
+)
 from airflow.providers.amazon.aws.hooks.datasync import DataSyncHook
 from airflow.providers.amazon.aws.links.datasync import 
DataSyncTaskExecutionLink, DataSyncTaskLink
 from airflow.providers.amazon.aws.operators.base_aws import AwsBaseOperator
@@ -102,13 +110,16 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
         
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
     :param botocore_config: Configuration dictionary (key-values) for botocore 
client. See:
         
https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
-    :raises AirflowException: If ``task_arn`` was not specified, or if
+    :raises ValueError: If ``task_arn`` was not specified, or if
         either ``source_location_uri`` or ``destination_location_uri`` were
         not specified.
-    :raises AirflowException: If source or destination Location were not found
+    :raises DataSyncLocationNotFoundError: If source or destination Location 
were not found
         and could not be created.
-    :raises AirflowException: If ``choose_task`` or ``choose_location`` fails.
-    :raises AirflowException: If Task creation, update, execution or delete 
fails.
+    :raises DataSyncMultipleTasksError: If ``choose_task`` fails.
+    :raises DataSyncMultipleLocationsError: If ``choose_location`` fails.
+    :raises DataSyncTaskNotFoundError: If a task could not be identified or 
created.
+    :raises DataSyncTaskCreationError: If Task creation fails.
+    :raises DataSyncTaskExecutionFailedError: If Task execution fails.
     """
 
     aws_hook_class = DataSyncHook
@@ -195,7 +206,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
         if self.source_location_uri and self.destination_location_uri:
             valid = True
         if not valid:
-            raise AirflowException(
+            raise ValueError(
                 f"Either specify task_arn or both source_location_uri and 
destination_location_uri. "
                 f"task_arn={self.task_arn!r}, 
source_location_uri={self.source_location_uri!r}, "
                 f"destination_location_uri={self.destination_location_uri!r}"
@@ -218,7 +229,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
             self._create_datasync_task()
 
         if not self.task_arn:
-            raise AirflowException("DataSync TaskArn could not be identified 
or created.")
+            raise DataSyncTaskNotFoundError("DataSync TaskArn could not be 
identified or created.")
 
         task_id = self.task_arn.split("/")[-1]
 
@@ -247,7 +258,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
         self._execute_datasync_task(context=context)
 
         if not self.task_execution_arn:
-            raise AirflowException("Nothing was executed")
+            raise DataSyncTaskExecutionFailedError("Nothing was executed")
 
         # Delete the DataSyncTask
         if self.delete_task_after_execution:
@@ -288,7 +299,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
             # from AWS and might lead to confusion. Rather explicitly
             # choose a random one
             return random.choice(task_arn_list)
-        raise AirflowException(f"Unable to choose a Task from {task_arn_list}")
+        raise DataSyncMultipleTasksError(f"Unable to choose a Task from 
{task_arn_list}")
 
     def choose_location(self, location_arn_list: list[str] | None) -> str | 
None:
         """Select 1 DataSync LocationArn from a list."""
@@ -302,7 +313,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
             # from AWS and might lead to confusion. Rather explicitly
             # choose a random one
             return random.choice(location_arn_list)
-        raise AirflowException(f"Unable to choose a Location from 
{location_arn_list}")
+        raise DataSyncMultipleLocationsError(f"Unable to choose a Location 
from {location_arn_list}")
 
     def _create_datasync_task(self) -> None:
         """Create a AWS DataSyncTask."""
@@ -313,7 +324,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
                 self.source_location_uri, **self.create_source_location_kwargs
             )
         if not self.source_location_arn:
-            raise AirflowException(
+            raise DataSyncLocationNotFoundError(
                 "Unable to determine source LocationArn. Does a suitable 
DataSync Location exist?"
             )
 
@@ -328,7 +339,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
                 self.destination_location_uri, 
**self.create_destination_location_kwargs
             )
         if not self.destination_location_arn:
-            raise AirflowException(
+            raise DataSyncLocationNotFoundError(
                 "Unable to determine destination LocationArn. Does a suitable 
DataSync Location exist?"
             )
 
@@ -337,7 +348,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
             self.source_location_arn, self.destination_location_arn, 
**self.create_task_kwargs
         )
         if not self.task_arn:
-            raise AirflowException("Task could not be created")
+            raise DataSyncTaskCreationError("Task could not be created")
         self.log.info("Created a Task with TaskArn %s", self.task_arn)
 
     def _update_datasync_task(self) -> None:
@@ -352,7 +363,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
     def _execute_datasync_task(self, context: Context) -> None:
         """Create and monitor an AWS DataSync TaskExecution for a Task."""
         if not self.task_arn:
-            raise AirflowException("Missing TaskArn")
+            raise ValueError("Missing TaskArn")
 
         # Create a task execution:
         self.log.info("Starting execution for TaskArn %s", self.task_arn)
@@ -406,7 +417,7 @@ class DataSyncOperator(AwsBaseOperator[DataSyncHook]):
                     self.log.log(level, "%s=%s", k, v)
 
         if not result:
-            raise AirflowException(f"Failed TaskExecutionArn 
{self.task_execution_arn}")
+            raise DataSyncTaskExecutionFailedError(f"Failed TaskExecutionArn 
{self.task_execution_arn}")
 
     def _cancel_datasync_task_execution(self):
         """Cancel the submitted DataSync task."""
diff --git a/providers/amazon/tests/unit/amazon/aws/operators/test_datasync.py 
b/providers/amazon/tests/unit/amazon/aws/operators/test_datasync.py
index 40a9f8788f9..959f8d609b2 100644
--- a/providers/amazon/tests/unit/amazon/aws/operators/test_datasync.py
+++ b/providers/amazon/tests/unit/amazon/aws/operators/test_datasync.py
@@ -23,10 +23,17 @@ import pytest
 from moto import mock_aws
 
 from airflow.models import DAG, DagRun, TaskInstance
+from airflow.providers.amazon.aws.exceptions import (
+    DataSyncLocationNotFoundError,
+    DataSyncMultipleLocationsError,
+    DataSyncMultipleTasksError,
+    DataSyncTaskCreationError,
+    DataSyncTaskExecutionFailedError,
+    DataSyncTaskNotFoundError,
+)
 from airflow.providers.amazon.aws.hooks.datasync import DataSyncHook
 from airflow.providers.amazon.aws.links.datasync import DataSyncTaskLink
 from airflow.providers.amazon.aws.operators.datasync import DataSyncOperator
-from airflow.providers.common.compat.sdk import AirflowException
 from airflow.utils.state import DagRunState
 from airflow.utils.types import DagRunType
 
@@ -212,13 +219,13 @@ class TestDataSyncOperatorCreate(DataSyncTestCaseBase):
         # ### Begin tests:
 
         self.set_up_operator(task_id="task_1", source_location_uri=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         self.set_up_operator(task_id="task_2", destination_location_uri=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         self.set_up_operator(task_id="task_3", source_location_uri=None, 
destination_location_uri=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         # ### Check mocks:
         mock_get_conn.assert_not_called()
@@ -320,7 +327,7 @@ class TestDataSyncOperatorCreate(DataSyncTestCaseBase):
         
self.client.create_location_smb(**MOCK_DATA["create_source_location_kwargs"])
 
         self.set_up_operator(task_id="datasync_task1")
-        with pytest.raises(AirflowException):
+        with pytest.raises(DataSyncMultipleLocationsError):
             self.datasync.execute(None)
 
         # Delete all tasks:
@@ -333,6 +340,64 @@ class TestDataSyncOperatorCreate(DataSyncTestCaseBase):
         # ### Check mocks:
         mock_get_conn.assert_called()
 
+    def test_no_task_identified_or_created(self, mock_get_conn):
+        # ### Set up mocks:
+        mock_get_conn.return_value = self.client
+        # ### Begin tests:
+
+        # Delete all tasks so none can be matched.
+        tasks = self.client.list_tasks()
+        for task in tasks["Tasks"]:
+            self.client.delete_task(TaskArn=task["TaskArn"])
+
+        # Without create_task_kwargs there is nothing to run or create.
+        self.datasync = DataSyncOperator(
+            task_id="datasync_no_task",
+            dag=self.dag,
+            source_location_uri=SOURCE_LOCATION_URI,
+            destination_location_uri=DESTINATION_LOCATION_URI,
+            wait_interval_seconds=0,
+        )
+        with pytest.raises(DataSyncTaskNotFoundError):
+            self.datasync.execute(None)
+        # ### Check mocks:
+        mock_get_conn.assert_called()
+
+    def test_create_task_no_source_location(self, mock_get_conn):
+        # ### Set up mocks:
+        mock_get_conn.return_value = self.client
+        # ### Begin tests:
+
+        self.datasync = DataSyncOperator(
+            task_id="datasync_no_source_location",
+            dag=self.dag,
+            source_location_uri="smb://nowhere/subdir",
+            destination_location_uri=DESTINATION_LOCATION_URI,
+            create_task_kwargs={"Options": {"VerifyMode": "NONE"}},
+            wait_interval_seconds=0,
+        )
+        with pytest.raises(DataSyncLocationNotFoundError):
+            self.datasync.execute(None)
+        # ### Check mocks:
+        mock_get_conn.assert_called()
+
+    @mock.patch.object(DataSyncHook, "create_task", return_value=None)
+    def test_create_task_without_task_arn(self, mock_create_task, 
mock_get_conn):
+        # ### Set up mocks:
+        mock_get_conn.return_value = self.client
+        # ### Begin tests:
+
+        # Delete all tasks so the operator falls through to creation.
+        tasks = self.client.list_tasks()
+        for task in tasks["Tasks"]:
+            self.client.delete_task(TaskArn=task["TaskArn"])
+
+        self.set_up_operator()
+        with pytest.raises(DataSyncTaskCreationError):
+            self.datasync.execute(None)
+        # ### Check mocks:
+        mock_get_conn.assert_called()
+
     def test_execute_specific_task(self, mock_get_conn):
         # ### Set up mocks:
         mock_get_conn.return_value = self.client
@@ -439,13 +504,13 @@ class TestDataSyncOperatorGetTasks(DataSyncTestCaseBase):
         # ### Begin tests:
 
         self.set_up_operator(task_id="task_1", source_location_uri=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         self.set_up_operator(task_id="task_2", destination_location_uri=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         self.set_up_operator(task_id="task_3", source_location_uri=None, 
destination_location_uri=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         # ### Check mocks:
         mock_get_conn.assert_not_called()
@@ -543,7 +608,7 @@ class TestDataSyncOperatorGetTasks(DataSyncTestCaseBase):
         assert len(locations["Locations"]) == 2
 
         # Execute the task
-        with pytest.raises(AirflowException):
+        with pytest.raises(DataSyncMultipleTasksError):
             self.datasync.execute(None)
 
         # Assert 0 additional task and 0 additional locations
@@ -652,7 +717,7 @@ class TestDataSyncOperatorUpdate(DataSyncTestCaseBase):
         # ### Begin tests:
 
         self.set_up_operator(task_arn=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         # ### Check mocks:
         mock_get_conn.assert_not_called()
@@ -774,7 +839,7 @@ class TestDataSyncOperator(DataSyncTestCaseBase):
         # ### Begin tests:
 
         self.set_up_operator(task_arn=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         # ### Check mocks:
         mock_get_conn.assert_not_called()
@@ -863,7 +928,7 @@ class TestDataSyncOperator(DataSyncTestCaseBase):
         self.set_up_operator()
 
         # Execute the task
-        with pytest.raises(AirflowException):
+        with pytest.raises(DataSyncTaskExecutionFailedError):
             self.datasync.execute(None)
         # ### Check mocks:
         mock_get_conn.assert_called()
@@ -987,7 +1052,7 @@ class TestDataSyncOperatorDelete(DataSyncTestCaseBase):
         # ### Begin tests:
 
         self.set_up_operator(task_arn=None)
-        with pytest.raises(AirflowException):
+        with pytest.raises(ValueError, match="Either specify task_arn"):
             self.datasync.execute(None)
         # ### Check mocks:
         mock_get_conn.assert_not_called()

Reply via email to