This is an automated email from the ASF dual-hosted git repository.
eladkal 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 d42096908f6 Make Amazon SageMaker triggers inherit AWS base classes
(#68927)
d42096908f6 is described below
commit d42096908f6a8a4622d0238dd4da0662c217439f
Author: Niko Oliveira <[email protected]>
AuthorDate: Wed Jun 24 21:58:52 2026 -0700
Make Amazon SageMaker triggers inherit AWS base classes (#68927)
* Make Amazon SageMaker triggers inherit AWS base classes
SageMakerTrigger and SageMakerPipelineTrigger now inherit
AwsBaseWaiterTrigger,
so they accept and propagate the generic AWS hook parameters (region_name,
verify, botocore_config) consistently with the rest of the Amazon provider,
completing the trigger portion of the SageMaker migration in
apache/airflow#35278.
* Keep poke_interval/max_attempts as deprecated SageMakerTrigger aliases
Preserve backward compatibility for SageMakerTrigger after switching to the
AwsBaseWaiterTrigger naming: poke_interval and max_attempts are still
accepted
as deprecated aliases for waiter_delay and waiter_max_attempts, emitting
AirflowProviderDeprecationWarning. This keeps existing keyword callers
working
and lets deferred-task triggers serialized by older versions deserialize
after
upgrade.
---
.../providers/amazon/aws/operators/sagemaker.py | 22 +--
.../providers/amazon/aws/triggers/sagemaker.py | 181 +++++++++++++--------
.../unit/amazon/aws/triggers/test_sagemaker.py | 173 ++++++++++++++++++--
3 files changed, 284 insertions(+), 92 deletions(-)
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py
b/providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py
index c99228a3e42..ebafeb85065 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/sagemaker.py
@@ -350,8 +350,8 @@ class SageMakerProcessingOperator(SageMakerBaseOperator):
trigger=SageMakerTrigger(
job_name=self.config["ProcessingJobName"],
job_type="Processing",
- poke_interval=self.check_interval,
- max_attempts=self.max_attempts,
+ waiter_delay=self.check_interval,
+ waiter_max_attempts=self.max_attempts,
aws_conn_id=self.aws_conn_id,
),
method_name="execute_complete",
@@ -366,7 +366,7 @@ class SageMakerProcessingOperator(SageMakerBaseOperator):
if validated_event["status"] != "success":
raise AirflowException(f"Error while running job:
{validated_event}")
- self.log.info(validated_event["message"])
+ self.log.info("SageMaker job %s completed.",
validated_event["job_name"])
self.serialized_job =
serialize(self.hook.describe_processing_job(validated_event["job_name"]))
self.log.info("%s completed successfully.", self.task_id)
return {"Processing": self.serialized_job}
@@ -602,7 +602,7 @@ class SageMakerEndpointOperator(SageMakerBaseOperator):
trigger=SageMakerTrigger(
job_name=endpoint_info["EndpointName"],
job_type="endpoint",
- poke_interval=self.check_interval,
+ waiter_delay=self.check_interval,
aws_conn_id=self.aws_conn_id,
),
method_name="execute_complete",
@@ -829,8 +829,8 @@ class SageMakerTransformOperator(SageMakerBaseOperator):
trigger=SageMakerTrigger(
job_name=transform_config["TransformJobName"],
job_type="Transform",
- poke_interval=self.check_interval,
- max_attempts=self.max_attempts,
+ waiter_delay=self.check_interval,
+ waiter_max_attempts=self.max_attempts,
aws_conn_id=self.aws_conn_id,
),
method_name="execute_complete",
@@ -852,7 +852,7 @@ class SageMakerTransformOperator(SageMakerBaseOperator):
def execute_complete(self, context: Context, event: dict[str, Any] | None
= None) -> dict[str, dict]:
validated_event = validate_execute_complete_event(event)
- self.log.info(validated_event["message"])
+ self.log.info("SageMaker job %s completed.",
validated_event["job_name"])
return self.serialize_result(validated_event["job_name"])
def serialize_result(self, job_name: str) -> dict[str, dict]:
@@ -1003,7 +1003,7 @@ class SageMakerTuningOperator(SageMakerBaseOperator):
trigger=SageMakerTrigger(
job_name=self.config["HyperParameterTuningJobName"],
job_type="tuning",
- poke_interval=self.check_interval,
+ waiter_delay=self.check_interval,
aws_conn_id=self.aws_conn_id,
),
method_name="execute_complete",
@@ -1234,8 +1234,8 @@ class SageMakerTrainingOperator(SageMakerBaseOperator):
trigger=SageMakerTrigger(
job_name=self.config["TrainingJobName"],
job_type="Training",
- poke_interval=self.check_interval,
- max_attempts=self.max_attempts,
+ waiter_delay=self.check_interval,
+ waiter_max_attempts=self.max_attempts,
aws_conn_id=self.aws_conn_id,
),
method_name="execute_complete",
@@ -1249,7 +1249,7 @@ class SageMakerTrainingOperator(SageMakerBaseOperator):
if validated_event["status"] != "success":
raise AirflowException(f"Error while running job:
{validated_event}")
- self.log.info(validated_event["message"])
+ self.log.info("SageMaker job %s completed.",
validated_event["job_name"])
return self.serialize_result(validated_event["job_name"])
def serialize_result(self, job_name: str) -> dict[str, dict]:
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/triggers/sagemaker.py
b/providers/amazon/src/airflow/providers/amazon/aws/triggers/sagemaker.py
index 57fbbe58713..271fe8b51fd 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/triggers/sagemaker.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/triggers/sagemaker.py
@@ -18,63 +18,95 @@
from __future__ import annotations
import asyncio
+import warnings
from collections import Counter
from collections.abc import AsyncIterator
from enum import IntEnum
-from functools import cached_property
-from typing import Any
+from typing import TYPE_CHECKING
from botocore.exceptions import WaiterError
+from airflow.exceptions import AirflowProviderDeprecationWarning
from airflow.providers.amazon.aws.hooks.sagemaker import SageMakerHook
-from airflow.providers.amazon.aws.utils.waiter_with_logging import async_wait
+from airflow.providers.amazon.aws.triggers.base import AwsBaseWaiterTrigger
from airflow.providers.common.compat.sdk import AirflowException
-from airflow.triggers.base import BaseTrigger, TriggerEvent
+from airflow.triggers.base import TriggerEvent
+if TYPE_CHECKING:
+ from airflow.providers.amazon.aws.hooks.base_aws import AwsGenericHook
-class SageMakerTrigger(BaseTrigger):
+
+class SageMakerTrigger(AwsBaseWaiterTrigger):
"""
SageMakerTrigger is fired as deferred class with params to run the task in
triggerer.
:param job_name: name of the job to check status
:param job_type: Type of the sagemaker job whether it is Transform or
Training
- :param poke_interval: polling period in seconds to check for the status
- :param max_attempts: Number of times to poll for query state before
returning the current state,
- defaults to None.
+ :param waiter_delay: polling period in seconds to check for the status
+ :param waiter_max_attempts: The maximum number of attempts to be made.
:param aws_conn_id: AWS connection ID for sagemaker
+ :param region_name: The AWS region where the job is running. Used to build
the hook.
+ :param verify: Whether or not to verify SSL certificates. Used to build
the hook.
+ :param botocore_config: Configuration dictionary for the botocore client.
Used to build the hook.
+ :param poke_interval: (deprecated) use ``waiter_delay`` instead.
+ :param max_attempts: (deprecated) use ``waiter_max_attempts`` instead.
"""
def __init__(
self,
job_name: str,
job_type: str,
- poke_interval: int = 30,
- max_attempts: int = 480,
+ waiter_delay: int = 30,
+ waiter_max_attempts: int = 480,
aws_conn_id: str | None = "aws_default",
+ region_name: str | None = None,
+ verify: bool | str | None = None,
+ botocore_config: dict | None = None,
+ poke_interval: int | None = None,
+ max_attempts: int | None = None,
):
- super().__init__()
+ if poke_interval is not None:
+ warnings.warn(
+ "`poke_interval` is deprecated and will be removed in a future
release. "
+ "Please use `waiter_delay` instead.",
+ AirflowProviderDeprecationWarning,
+ stacklevel=2,
+ )
+ waiter_delay = poke_interval
+ if max_attempts is not None:
+ warnings.warn(
+ "`max_attempts` is deprecated and will be removed in a future
release. "
+ "Please use `waiter_max_attempts` instead.",
+ AirflowProviderDeprecationWarning,
+ stacklevel=2,
+ )
+ waiter_max_attempts = max_attempts
self.job_name = job_name
self.job_type = job_type
- self.poke_interval = poke_interval
- self.max_attempts = max_attempts
- self.aws_conn_id = aws_conn_id
-
- def serialize(self) -> tuple[str, dict[str, Any]]:
- """Serialize SagemakerTrigger arguments and classpath."""
- return (
- "airflow.providers.amazon.aws.triggers.sagemaker.SageMakerTrigger",
- {
- "job_name": self.job_name,
- "job_type": self.job_type,
- "poke_interval": self.poke_interval,
- "max_attempts": self.max_attempts,
- "aws_conn_id": self.aws_conn_id,
- },
+ super().__init__(
+ serialized_fields={"job_name": job_name, "job_type": job_type},
+ waiter_name=self._get_job_type_waiter(job_type),
+ waiter_args={self._get_waiter_arg_name(job_type): job_name},
+ failure_message=f"Error while waiting for {job_type} job",
+ status_message=f"{job_type} job not done yet",
+ status_queries=[self._get_response_status_key(job_type)],
+ return_key="job_name",
+ return_value=job_name,
+ waiter_delay=waiter_delay,
+ waiter_max_attempts=waiter_max_attempts,
+ aws_conn_id=aws_conn_id,
+ region_name=region_name,
+ verify=verify,
+ botocore_config=botocore_config,
)
- @cached_property
- def hook(self) -> SageMakerHook:
- return SageMakerHook(aws_conn_id=self.aws_conn_id)
+ def hook(self) -> AwsGenericHook:
+ return SageMakerHook(
+ aws_conn_id=self.aws_conn_id,
+ region_name=self.region_name,
+ verify=self.verify,
+ config=self.botocore_config,
+ )
@staticmethod
def _get_job_type_waiter(job_type: str) -> str:
@@ -106,26 +138,20 @@ class SageMakerTrigger(BaseTrigger):
"endpoint": "EndpointStatus",
}[job_type.lower()]
- async def run(self):
- self.log.info("job name is %s and job type is %s", self.job_name,
self.job_type)
- async with await self.hook.get_async_conn() as client:
- waiter = self.hook.get_waiter(
- self._get_job_type_waiter(self.job_type), deferrable=True,
client=client
- )
- await async_wait(
- waiter=waiter,
- waiter_delay=self.poke_interval,
- waiter_max_attempts=self.max_attempts,
- args={self._get_waiter_arg_name(self.job_type): self.job_name},
- failure_message=f"Error while waiting for {self.job_type} job",
- status_message=f"{self.job_type} job not done yet",
- status_args=[self._get_response_status_key(self.job_type)],
- )
- yield TriggerEvent({"status": "success", "message": "Job
completed.", "job_name": self.job_name})
-
-class SageMakerPipelineTrigger(BaseTrigger):
- """Trigger to wait for a sagemaker pipeline execution to finish."""
+class SageMakerPipelineTrigger(AwsBaseWaiterTrigger):
+ """
+ Trigger to wait for a sagemaker pipeline execution to finish.
+
+ :param waiter_type: Type of waiter to use, see ``Type`` enum.
+ :param pipeline_execution_arn: ARN of the pipeline execution to wait for.
+ :param waiter_delay: The amount of time in seconds to wait between
attempts.
+ :param waiter_max_attempts: The maximum number of attempts to be made.
+ :param aws_conn_id: The Airflow connection used for AWS credentials.
+ :param region_name: The AWS region where the pipeline runs. Used to build
the hook.
+ :param verify: Whether or not to verify SSL certificates. Used to build
the hook.
+ :param botocore_config: Configuration dictionary for the botocore client.
Used to build the hook.
+ """
class Type(IntEnum):
"""Type of waiter to use."""
@@ -133,42 +159,59 @@ class SageMakerPipelineTrigger(BaseTrigger):
COMPLETE = 1
STOPPED = 2
+ _waiter_name = {
+ Type.COMPLETE: "PipelineExecutionComplete",
+ Type.STOPPED: "PipelineExecutionStopped",
+ }
+
def __init__(
self,
- waiter_type: Type,
+ waiter_type: Type | int,
pipeline_execution_arn: str,
waiter_delay: int,
waiter_max_attempts: int,
aws_conn_id: str | None,
+ region_name: str | None = None,
+ verify: bool | str | None = None,
+ botocore_config: dict | None = None,
):
- self.waiter_type = waiter_type
+ # waiter_type arrives as an int when deserialized from a serialized
trigger.
+ self.waiter_type = self.Type(waiter_type)
self.pipeline_execution_arn = pipeline_execution_arn
- self.waiter_delay = waiter_delay
- self.waiter_max_attempts = waiter_max_attempts
- self.aws_conn_id = aws_conn_id
-
- def serialize(self) -> tuple[str, dict[str, Any]]:
- return (
- self.__class__.__module__ + "." + self.__class__.__qualname__,
- {
+ super().__init__(
+ serialized_fields={
"waiter_type": self.waiter_type.value, # saving the int value
here
- "pipeline_execution_arn": self.pipeline_execution_arn,
- "waiter_delay": self.waiter_delay,
- "waiter_max_attempts": self.waiter_max_attempts,
- "aws_conn_id": self.aws_conn_id,
+ "pipeline_execution_arn": pipeline_execution_arn,
},
+ waiter_name=self._waiter_name[self.waiter_type],
+ waiter_args={"PipelineExecutionArn": pipeline_execution_arn},
+ failure_message="Error while waiting for the pipeline execution to
finish",
+ status_message="Pipeline execution not done yet",
+ status_queries=["PipelineExecutionStatus"],
+ return_value=pipeline_execution_arn,
+ waiter_delay=waiter_delay,
+ waiter_max_attempts=waiter_max_attempts,
+ aws_conn_id=aws_conn_id,
+ region_name=region_name,
+ verify=verify,
+ botocore_config=botocore_config,
)
- _waiter_name = {
- Type.COMPLETE: "PipelineExecutionComplete",
- Type.STOPPED: "PipelineExecutionStopped",
- }
+ def hook(self) -> AwsGenericHook:
+ return SageMakerHook(
+ aws_conn_id=self.aws_conn_id,
+ region_name=self.region_name,
+ verify=self.verify,
+ config=self.botocore_config,
+ )
async def run(self) -> AsyncIterator[TriggerEvent]:
- hook = SageMakerHook(aws_conn_id=self.aws_conn_id)
+ # Custom polling loop (instead of the base waiter loop) so we can
surface
+ # per-step pipeline progress in the logs between attempts.
+ hook = self.hook()
async with await hook.get_async_conn() as conn:
- waiter = hook.get_waiter(self._waiter_name[self.waiter_type],
deferrable=True, client=conn)
- for _ in range(self.waiter_max_attempts):
+ waiter = hook.get_waiter(self.waiter_name, deferrable=True,
client=conn)
+ for _ in range(self.attempts):
try:
await waiter.wait(
PipelineExecutionArn=self.pipeline_execution_arn,
WaiterConfig={"MaxAttempts": 1}
diff --git a/providers/amazon/tests/unit/amazon/aws/triggers/test_sagemaker.py
b/providers/amazon/tests/unit/amazon/aws/triggers/test_sagemaker.py
index 0f1851df048..68cf554fbb1 100644
--- a/providers/amazon/tests/unit/amazon/aws/triggers/test_sagemaker.py
+++ b/providers/amazon/tests/unit/amazon/aws/triggers/test_sagemaker.py
@@ -20,15 +20,20 @@ from unittest import mock
from unittest.mock import AsyncMock
import pytest
+from botocore.exceptions import WaiterError
-from airflow.providers.amazon.aws.triggers.sagemaker import SageMakerTrigger
+from airflow.exceptions import AirflowProviderDeprecationWarning
+from airflow.providers.amazon.aws.hooks.sagemaker import SageMakerHook
+from airflow.providers.amazon.aws.triggers.sagemaker import
SageMakerPipelineTrigger, SageMakerTrigger
from airflow.triggers.base import TriggerEvent
JOB_NAME = "job_name"
-JOB_TYPE = "job_type"
+JOB_TYPE = "training"
AWS_CONN_ID = "aws_sagemaker_conn"
-POKE_INTERVAL = 30
-MAX_ATTEMPTS = 60
+WAITER_DELAY = 30
+WAITER_MAX_ATTEMPTS = 60
+REGION_NAME = "us-west-2"
+PIPELINE_ARN =
"arn:aws:sagemaker:us-west-2:123456789012:pipeline/my-pipeline/execution/abc"
class TestSagemakerTrigger:
@@ -36,17 +41,54 @@ class TestSagemakerTrigger:
sagemaker_trigger = SageMakerTrigger(
job_name=JOB_NAME,
job_type=JOB_TYPE,
- poke_interval=POKE_INTERVAL,
- max_attempts=MAX_ATTEMPTS,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
aws_conn_id=AWS_CONN_ID,
+ region_name=REGION_NAME,
)
class_path, args = sagemaker_trigger.serialize()
assert class_path ==
"airflow.providers.amazon.aws.triggers.sagemaker.SageMakerTrigger"
assert args["job_name"] == JOB_NAME
assert args["job_type"] == JOB_TYPE
- assert args["poke_interval"] == POKE_INTERVAL
- assert args["max_attempts"] == MAX_ATTEMPTS
+ assert args["waiter_delay"] == WAITER_DELAY
+ assert args["waiter_max_attempts"] == WAITER_MAX_ATTEMPTS
assert args["aws_conn_id"] == AWS_CONN_ID
+ assert args["region_name"] == REGION_NAME
+
+ @pytest.mark.parametrize(
+ ("deprecated_kwarg", "canonical_attr", "value"),
+ [
+ ("poke_interval", "waiter_delay", 17),
+ ("max_attempts", "attempts", 21),
+ ],
+ )
+ def test_sagemaker_trigger_deprecated_params(self, deprecated_kwarg,
canonical_attr, value):
+ with pytest.warns(AirflowProviderDeprecationWarning,
match=deprecated_kwarg):
+ trigger = SageMakerTrigger(
+ job_name=JOB_NAME,
+ job_type=JOB_TYPE,
+ aws_conn_id=AWS_CONN_ID,
+ **{deprecated_kwarg: value},
+ )
+ assert getattr(trigger, canonical_attr) == value
+
+ def test_sagemaker_trigger_hook_uses_generic_params(self):
+ sagemaker_trigger = SageMakerTrigger(
+ job_name=JOB_NAME,
+ job_type=JOB_TYPE,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
+ aws_conn_id=AWS_CONN_ID,
+ region_name=REGION_NAME,
+ verify=False,
+ botocore_config={"read_timeout": 10},
+ )
+ hook = sagemaker_trigger.hook()
+ assert isinstance(hook, SageMakerHook)
+ assert hook.aws_conn_id == AWS_CONN_ID
+ assert hook._region_name == REGION_NAME
+ assert hook._verify is False
+ assert hook._config.read_timeout == 10
@pytest.mark.asyncio
@pytest.mark.parametrize(
@@ -69,14 +111,121 @@ class TestSagemakerTrigger:
sagemaker_trigger = SageMakerTrigger(
job_name=JOB_NAME,
job_type=job_type,
- poke_interval=POKE_INTERVAL,
- max_attempts=MAX_ATTEMPTS,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
aws_conn_id=AWS_CONN_ID,
)
generator = sagemaker_trigger.run()
response = await generator.asend(None)
- assert response == TriggerEvent(
- {"status": "success", "message": "Job completed.", "job_name":
JOB_NAME}
+ assert response == TriggerEvent({"status": "success", "job_name":
JOB_NAME})
+
+
+class TestSagemakerPipelineTrigger:
+ def test_serialize(self):
+ trigger = SageMakerPipelineTrigger(
+ waiter_type=SageMakerPipelineTrigger.Type.COMPLETE,
+ pipeline_execution_arn=PIPELINE_ARN,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
+ aws_conn_id=AWS_CONN_ID,
)
+ class_path, args = trigger.serialize()
+ assert class_path ==
"airflow.providers.amazon.aws.triggers.sagemaker.SageMakerPipelineTrigger"
+ assert args["waiter_type"] ==
SageMakerPipelineTrigger.Type.COMPLETE.value
+ assert args["pipeline_execution_arn"] == PIPELINE_ARN
+ assert args["waiter_delay"] == WAITER_DELAY
+ assert args["waiter_max_attempts"] == WAITER_MAX_ATTEMPTS
+ assert args["aws_conn_id"] == AWS_CONN_ID
+
+ def test_deserialize_accepts_int_waiter_type(self):
+ # On deserialization the waiter_type is passed back as the stored int
value.
+ trigger = SageMakerPipelineTrigger(
+ waiter_type=SageMakerPipelineTrigger.Type.STOPPED.value,
+ pipeline_execution_arn=PIPELINE_ARN,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
+ aws_conn_id=AWS_CONN_ID,
+ )
+ assert trigger.waiter_type == SageMakerPipelineTrigger.Type.STOPPED
+ assert trigger.waiter_name == "PipelineExecutionStopped"
+
+ @pytest.mark.asyncio
+
@mock.patch("airflow.providers.amazon.aws.hooks.sagemaker.SageMakerHook.get_waiter")
+
@mock.patch("airflow.providers.amazon.aws.hooks.sagemaker.SageMakerHook.get_async_conn")
+ async def test_run_success(self, mock_async_conn, mock_get_waiter):
+ mock_async_conn.return_value.__aenter__.return_value = mock.MagicMock()
+ mock_get_waiter().wait = AsyncMock()
+
+ trigger = SageMakerPipelineTrigger(
+ waiter_type=SageMakerPipelineTrigger.Type.COMPLETE,
+ pipeline_execution_arn=PIPELINE_ARN,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
+ aws_conn_id=AWS_CONN_ID,
+ )
+
+ response = await trigger.run().asend(None)
+
+ assert response == TriggerEvent({"status": "success", "value":
PIPELINE_ARN})
+
+ @pytest.mark.asyncio
+
@mock.patch("airflow.providers.amazon.aws.triggers.sagemaker.asyncio.sleep")
+
@mock.patch("airflow.providers.amazon.aws.hooks.sagemaker.SageMakerHook.get_waiter")
+
@mock.patch("airflow.providers.amazon.aws.hooks.sagemaker.SageMakerHook.get_async_conn")
+ async def test_run_logs_steps_then_succeeds(self, mock_async_conn,
mock_get_waiter, mock_sleep):
+ conn = mock.MagicMock()
+ conn.list_pipeline_execution_steps = AsyncMock(
+ return_value={
+ "PipelineExecutionSteps": [
+ {"StepName": "step-1", "StepStatus": "Executing"},
+ {"StepName": "step-2", "StepStatus": "Succeeded"},
+ ]
+ }
+ )
+ mock_async_conn.return_value.__aenter__.return_value = conn
+ mock_sleep.return_value = None
+
+ non_terminal_error = WaiterError(
+ name="PipelineExecutionComplete",
+ reason="not done yet",
+ last_response={"PipelineExecutionStatus": "Executing"},
+ )
+ mock_get_waiter().wait = AsyncMock(side_effect=[non_terminal_error,
None])
+
+ trigger = SageMakerPipelineTrigger(
+ waiter_type=SageMakerPipelineTrigger.Type.COMPLETE,
+ pipeline_execution_arn=PIPELINE_ARN,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
+ aws_conn_id=AWS_CONN_ID,
+ )
+
+ response = await trigger.run().asend(None)
+
+ assert response == TriggerEvent({"status": "success", "value":
PIPELINE_ARN})
+
conn.list_pipeline_execution_steps.assert_awaited_once_with(PipelineExecutionArn=PIPELINE_ARN)
+
+ @pytest.mark.asyncio
+
@mock.patch("airflow.providers.amazon.aws.hooks.sagemaker.SageMakerHook.get_waiter")
+
@mock.patch("airflow.providers.amazon.aws.hooks.sagemaker.SageMakerHook.get_async_conn")
+ async def test_run_raises_on_terminal_failure(self, mock_async_conn,
mock_get_waiter):
+ mock_async_conn.return_value.__aenter__.return_value = mock.MagicMock()
+ terminal_error = WaiterError(
+ name="PipelineExecutionComplete",
+ reason="terminal failure",
+ last_response={"PipelineExecutionStatus": "Failed"},
+ )
+ mock_get_waiter().wait = AsyncMock(side_effect=terminal_error)
+
+ trigger = SageMakerPipelineTrigger(
+ waiter_type=SageMakerPipelineTrigger.Type.COMPLETE,
+ pipeline_execution_arn=PIPELINE_ARN,
+ waiter_delay=WAITER_DELAY,
+ waiter_max_attempts=WAITER_MAX_ATTEMPTS,
+ aws_conn_id=AWS_CONN_ID,
+ )
+
+ with pytest.raises(WaiterError, match="terminal failure"):
+ await trigger.run().asend(None)