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 76ffb4693fe Add deferrable mode to QuickSight operator and sensor 
(#70218)
76ffb4693fe is described below

commit 76ffb4693feb30c5e1e9f7e0a5e630d0c59a5d63
Author: Emin Özata <[email protected]>
AuthorDate: Fri Sep 11 02:21:14 2026 +0300

    Add deferrable mode to QuickSight operator and sensor (#70218)
    
    The QuickSight create-ingestion wait path used a hand-written polling
    loop that held a worker slot with time.sleep and had no upper bound, so
    a stuck ingestion could block a worker indefinitely. Moving the wait to
    a custom boto waiter lets both the operator and the sensor defer to the
    triggerer with deferrable=True, matching the deferrable pattern used
    across the Amazon provider, and bounds the synchronous wait as well.
    
    AWS waits up to 36 hours for a SPICE ingestion in its own CloudFormation
    wait policy, where 36 hours is both the default and the maximum.
    Refreshes that run for several hours are common, so a shorter cap fails
    Dags that previously waited without a limit.
---
 generated/known_airflow_exceptions.txt             |   1 -
 providers/amazon/docs/operators/quicksight.rst     |   6 ++
 providers/amazon/provider.yaml                     |   3 +
 .../providers/amazon/aws/hooks/quicksight.py       |  85 +++++++++++++-----
 .../providers/amazon/aws/operators/quicksight.py   |  77 ++++++++++++++--
 .../providers/amazon/aws/sensors/quicksight.py     |  49 +++++++++-
 .../providers/amazon/aws/triggers/quicksight.py    |  87 ++++++++++++++++++
 .../providers/amazon/aws/waiters/quicksight.json   |  48 ++++++++++
 .../airflow/providers/amazon/get_provider_info.py  |   4 +
 .../tests/unit/amazon/aws/hooks/test_quicksight.py |  77 +++++++++++++---
 .../unit/amazon/aws/operators/test_quicksight.py   | 100 +++++++++++++++++++--
 .../unit/amazon/aws/sensors/test_quicksight.py     |  46 +++++++++-
 .../unit/amazon/aws/triggers/test_quicksight.py    |  64 +++++++++++++
 .../unit/amazon/aws/waiters/test_quicksight.py     |  76 ++++++++++++++++
 14 files changed, 668 insertions(+), 55 deletions(-)

diff --git a/generated/known_airflow_exceptions.txt 
b/generated/known_airflow_exceptions.txt
index 2c619d8eaff..4269e6eb801 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -105,7 +105,6 @@ 
providers/amazon/src/airflow/providers/amazon/aws/sensors/kinesis_analytics.py::
 providers/amazon/src/airflow/providers/amazon/aws/sensors/lambda_function.py::1
 providers/amazon/src/airflow/providers/amazon/aws/sensors/mwaa.py::4
 
providers/amazon/src/airflow/providers/amazon/aws/sensors/opensearch_serverless.py::1
-providers/amazon/src/airflow/providers/amazon/aws/sensors/quicksight.py::1
 providers/amazon/src/airflow/providers/amazon/aws/sensors/rds.py::1
 
providers/amazon/src/airflow/providers/amazon/aws/sensors/redshift_cluster.py::1
 providers/amazon/src/airflow/providers/amazon/aws/sensors/s3.py::3
diff --git a/providers/amazon/docs/operators/quicksight.rst 
b/providers/amazon/docs/operators/quicksight.rst
index 07cd48f7256..e5453cf2a8b 100644
--- a/providers/amazon/docs/operators/quicksight.rst
+++ b/providers/amazon/docs/operators/quicksight.rst
@@ -46,6 +46,9 @@ Amazon QuickSight create ingestion
 The ``QuickSightCreateIngestionOperator`` creates and starts a new SPICE 
ingestion for a dataset.
 The operator also refreshes existing SPICE datasets.
 
+This operator can be run in deferrable mode by passing ``deferrable=True`` as 
a parameter. This requires
+the aiobotocore module to be installed.
+
 .. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_quicksight.py
     :language: python
     :dedent: 4
@@ -62,6 +65,9 @@ Amazon QuickSight ingestion sensor
 
 The ``QuickSightSensor`` waits for an Amazon QuickSight create ingestion until 
it reaches a terminal state.
 
+This sensor can be run in deferrable mode by passing ``deferrable=True`` as a 
parameter. This requires
+the aiobotocore module to be installed.
+
 .. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_quicksight.py
     :language: python
     :dedent: 4
diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml
index 00a365e552d..b7731887e17 100644
--- a/providers/amazon/provider.yaml
+++ b/providers/amazon/provider.yaml
@@ -898,6 +898,9 @@ triggers:
   - integration-name: AWS Database Migration Service
     python-modules:
       - airflow.providers.amazon.aws.triggers.dms
+  - integration-name: Amazon QuickSight
+    python-modules:
+      - airflow.providers.amazon.aws.triggers.quicksight
 
 transfers:
   - source-integration-name: Amazon DynamoDB
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/hooks/quicksight.py 
b/providers/amazon/src/airflow/providers/amazon/aws/hooks/quicksight.py
index 0585213fff3..3441291aa56 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/hooks/quicksight.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/hooks/quicksight.py
@@ -18,10 +18,13 @@
 from __future__ import annotations
 
 import time
+import warnings
 
 from botocore.exceptions import ClientError
 
+from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.providers.amazon.aws.hooks.base_aws import AwsBaseHook
+from airflow.providers.amazon.aws.utils.waiter_with_logging import wait
 from airflow.providers.common.compat.sdk import AirflowException
 
 
@@ -52,6 +55,7 @@ class QuickSightHook(AwsBaseHook):
         wait_for_completion: bool = True,
         check_interval: int = 30,
         aws_account_id: str | None = None,
+        waiter_max_attempts: int = 4320,
     ) -> dict:
         """
         Create and start a new SPICE ingestion for a dataset; refresh the 
SPICE datasets.
@@ -66,31 +70,63 @@ class QuickSightHook(AwsBaseHook):
         :param check_interval: the time interval in seconds which the operator
             will check the status of QuickSight Ingestion
         :param aws_account_id: An AWS Account ID, if set to ``None`` then use 
associated AWS Account ID.
+        :param waiter_max_attempts: The maximum number of attempts to be made.
         :return: Returns descriptive information about the created data 
ingestion
             having Ingestion ARN, HTTP status, ingestion ID and ingestion 
status.
         """
         aws_account_id = aws_account_id or self.account_id
         self.log.info("Creating QuickSight Ingestion for data set id %s.", 
data_set_id)
-        try:
-            create_ingestion_response = self.conn.create_ingestion(
-                DataSetId=data_set_id,
-                IngestionId=ingestion_id,
-                IngestionType=ingestion_type,
-                AwsAccountId=aws_account_id,
+        create_ingestion_response = self.conn.create_ingestion(
+            DataSetId=data_set_id,
+            IngestionId=ingestion_id,
+            IngestionType=ingestion_type,
+            AwsAccountId=aws_account_id,
+        )
+        if wait_for_completion:
+            self.wait_for_ingestion(
+                data_set_id=data_set_id,
+                ingestion_id=ingestion_id,
+                aws_account_id=aws_account_id,
+                waiter_delay=check_interval,
+                waiter_max_attempts=waiter_max_attempts,
             )
+        return create_ingestion_response
+
+    def wait_for_ingestion(
+        self,
+        *,
+        data_set_id: str,
+        ingestion_id: str,
+        aws_account_id: str | None = None,
+        waiter_delay: int = 30,
+        waiter_max_attempts: int = 4320,
+    ) -> None:
+        """
+        Poll a SPICE ingestion until it completes.
 
-            if wait_for_completion:
-                self.wait_for_state(
-                    aws_account_id=aws_account_id,
-                    data_set_id=data_set_id,
-                    ingestion_id=ingestion_id,
-                    target_state={"COMPLETED"},
-                    check_interval=check_interval,
-                )
-            return create_ingestion_response
-        except Exception as general_error:
-            self.log.error("Failed to run Amazon QuickSight create_ingestion 
API, error: %s", general_error)
-            raise
+        :param data_set_id: QuickSight Data Set ID
+        :param ingestion_id: QuickSight Ingestion ID
+        :param aws_account_id: An AWS Account ID, if set to ``None`` then use 
associated AWS Account ID.
+        :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.
+        :raises RuntimeError: If the ingestion fails, is cancelled or times 
out.
+        """
+        try:
+            wait(
+                waiter=self.get_waiter("ingestion_complete"),
+                waiter_delay=waiter_delay,
+                waiter_max_attempts=waiter_max_attempts,
+                args={
+                    "AwsAccountId": aws_account_id or self.account_id,
+                    "DataSetId": data_set_id,
+                    "IngestionId": ingestion_id,
+                },
+                failure_message="Amazon QuickSight SPICE ingestion failed.",
+                status_message="Status of Amazon QuickSight SPICE ingestion 
is",
+                status_args=["Ingestion.IngestionStatus", 
"Ingestion.ErrorInfo"],
+            )
+        except AirflowException as e:
+            raise RuntimeError(str(e)) from e
 
     def get_status(self, aws_account_id: str | None, data_set_id: str, 
ingestion_id: str) -> str:
         """
@@ -144,14 +180,23 @@ class QuickSightHook(AwsBaseHook):
         """
         Check status of a QuickSight Create Ingestion API.
 
+        .. deprecated::
+            Use :meth:`wait_for_ingestion` instead.
+
         :param aws_account_id: An AWS Account ID, if set to ``None`` then use 
associated AWS Account ID.
         :param data_set_id: QuickSight Data Set ID
         :param ingestion_id: QuickSight Ingestion ID
         :param target_state: Describes the QuickSight Job's Target State
         :param check_interval: the time interval in seconds which the operator
             will check the status of QuickSight Ingestion
-        :return: response of describe_ingestion call after Ingestion is done
+        :return: the final status of the ingestion
         """
+        warnings.warn(
+            "`QuickSightHook.wait_for_state` is deprecated and will be removed 
in a future release. "
+            "Use `QuickSightHook.wait_for_ingestion` instead.",
+            AirflowProviderDeprecationWarning,
+            stacklevel=2,
+        )
         aws_account_id = aws_account_id or self.account_id
 
         while True:
@@ -162,7 +207,7 @@ class QuickSightHook(AwsBaseHook):
                 raise AirflowException(f"The Amazon QuickSight Ingestion 
failed. Error info: {info}")
             if status == "CANCELLED":
                 raise AirflowException("The Amazon QuickSight SPICE ingestion 
cancelled!")
-            if status not in self.NON_TERMINAL_STATES or status == 
target_state:
+            if status not in self.NON_TERMINAL_STATES:
                 break
             time.sleep(check_interval)
 
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/operators/quicksight.py 
b/providers/amazon/src/airflow/providers/amazon/aws/operators/quicksight.py
index 51b0b3b372e..64086104b79 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/operators/quicksight.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/quicksight.py
@@ -16,12 +16,17 @@
 # under the License.
 from __future__ import annotations
 
+import warnings
 from collections.abc import Sequence
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
+from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
 from airflow.providers.amazon.aws.operators.base_aws import AwsBaseOperator
+from airflow.providers.amazon.aws.triggers.quicksight import 
QuickSightIngestionCompletedTrigger
+from airflow.providers.amazon.aws.utils import validate_execute_complete_event
 from airflow.providers.amazon.aws.utils.mixins import aws_template_fields
+from airflow.providers.common.compat.sdk import conf
 
 if TYPE_CHECKING:
     from airflow.sdk import Context
@@ -39,10 +44,13 @@ class 
QuickSightCreateIngestionOperator(AwsBaseOperator[QuickSightHook]):
     :param ingestion_id: ID for the ingestion.
     :param ingestion_type: Type of ingestion. Values Can be  
INCREMENTAL_REFRESH or FULL_REFRESH.
         Default FULL_REFRESH.
-    :param wait_for_completion: If wait is set to True, the time interval, in 
seconds,
-        that the operation waits to check the status of the Amazon QuickSight 
Ingestion.
-    :param check_interval: if wait is set to be true, this is the time interval
-        in seconds which the operator will check the status of the Amazon 
QuickSight Ingestion
+    :param wait_for_completion: If True, wait for the ingestion to reach a 
terminal state. (default: True)
+    :param waiter_delay: Time in seconds to wait between status checks. 
(default: 30)
+    :param waiter_max_attempts: Maximum number of attempts to check for 
completion. (default: 4320)
+    :param check_interval: Deprecated, use ``waiter_delay`` instead.
+    :param deferrable: If True, the operator will wait asynchronously for the 
ingestion to complete.
+        This implies waiting for completion. This mode requires aiobotocore 
module to be installed.
+        (default: False, but can be overridden in config file by setting 
default_deferrable to True)
     :param aws_conn_id: The Airflow connection used for AWS credentials.
         If this is ``None`` or empty then the default boto3 behaviour is used. 
If
         running Airflow in a distributed manner and aws_conn_id is None or
@@ -61,6 +69,8 @@ class 
QuickSightCreateIngestionOperator(AwsBaseOperator[QuickSightHook]):
         "ingestion_id",
         "ingestion_type",
         "wait_for_completion",
+        "waiter_delay",
+        "waiter_max_attempts",
         "check_interval",
     )
     ui_color = "#ffd700"
@@ -71,7 +81,10 @@ class 
QuickSightCreateIngestionOperator(AwsBaseOperator[QuickSightHook]):
         ingestion_id: str,
         ingestion_type: str = "FULL_REFRESH",
         wait_for_completion: bool = True,
-        check_interval: int = 30,
+        waiter_delay: int = 30,
+        waiter_max_attempts: int = 4320,
+        check_interval: int | None = None,
+        deferrable: bool = conf.getboolean("operators", "default_deferrable", 
fallback=False),
         **kwargs,
     ):
         super().__init__(**kwargs)
@@ -79,14 +92,60 @@ class 
QuickSightCreateIngestionOperator(AwsBaseOperator[QuickSightHook]):
         self.ingestion_id = ingestion_id
         self.ingestion_type = ingestion_type
         self.wait_for_completion = wait_for_completion
+        self.waiter_delay = waiter_delay
+        self.waiter_max_attempts = waiter_max_attempts
         self.check_interval = check_interval
+        self.deferrable = deferrable
 
     def execute(self, context: Context):
+        # check_interval may be templated, so it is only resolvable once 
rendering has happened
+        if self.check_interval is not None:
+            warnings.warn(
+                "The `check_interval` parameter is deprecated and will be 
removed in a future release. "
+                "Use `waiter_delay` instead. While `check_interval` is set, it 
takes precedence over "
+                "`waiter_delay`.",
+                AirflowProviderDeprecationWarning,
+                stacklevel=2,
+            )
         self.log.info("Running the Amazon QuickSight SPICE Ingestion on 
Dataset ID: %s", self.data_set_id)
-        return self.hook.create_ingestion(
+        ingestion = self.hook.create_ingestion(
             data_set_id=self.data_set_id,
             ingestion_id=self.ingestion_id,
             ingestion_type=self.ingestion_type,
-            wait_for_completion=self.wait_for_completion,
-            check_interval=self.check_interval,
+            wait_for_completion=False,
         )
+        waiter_delay = int(self.waiter_delay if self.check_interval is None 
else self.check_interval)
+        waiter_max_attempts = int(self.waiter_max_attempts)
+        if self.deferrable:
+            self.defer(
+                trigger=QuickSightIngestionCompletedTrigger(
+                    data_set_id=self.data_set_id,
+                    ingestion_id=self.ingestion_id,
+                    aws_account_id=self.hook.account_id,
+                    waiter_delay=waiter_delay,
+                    waiter_max_attempts=waiter_max_attempts,
+                    aws_conn_id=self.aws_conn_id,
+                    region_name=self.region_name,
+                    verify=self.verify,
+                    botocore_config=self.botocore_config,
+                ),
+                method_name="execute_complete",
+                kwargs={"ingestion": ingestion},
+            )
+        elif self.wait_for_completion:
+            self.hook.wait_for_ingestion(
+                data_set_id=self.data_set_id,
+                ingestion_id=self.ingestion_id,
+                waiter_delay=waiter_delay,
+                waiter_max_attempts=waiter_max_attempts,
+            )
+        return ingestion
+
+    def execute_complete(
+        self, context: Context, event: dict[str, Any] | None = None, 
ingestion: dict[str, Any] | None = None
+    ) -> dict[str, Any] | None:
+        validated_event = validate_execute_complete_event(event)
+        if validated_event["status"] != "success":
+            raise RuntimeError(f"Error while running Amazon QuickSight SPICE 
ingestion: {validated_event}")
+        self.log.info("Amazon QuickSight SPICE ingestion `%s` completed.", 
validated_event["ingestion_id"])
+        return ingestion
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/sensors/quicksight.py 
b/providers/amazon/src/airflow/providers/amazon/aws/sensors/quicksight.py
index 4c1f9085033..6852d3a361a 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/sensors/quicksight.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/sensors/quicksight.py
@@ -18,11 +18,13 @@
 from __future__ import annotations
 
 from collections.abc import Sequence
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
 from airflow.providers.amazon.aws.sensors.base_aws import AwsBaseSensor
-from airflow.providers.common.compat.sdk import AirflowException
+from airflow.providers.amazon.aws.triggers.quicksight import 
QuickSightIngestionCompletedTrigger
+from airflow.providers.amazon.aws.utils import validate_execute_complete_event
+from airflow.providers.common.compat.sdk import conf
 
 if TYPE_CHECKING:
     from airflow.sdk import Context
@@ -38,6 +40,10 @@ class QuickSightSensor(AwsBaseSensor[QuickSightHook]):
 
     :param data_set_id:  ID of the dataset used in the ingestion.
     :param ingestion_id: ID for the ingestion.
+    :param max_retries: Number of times before returning the current state. 
(default: 2160)
+    :param deferrable: If True, the sensor will operate in deferrable mode. 
This mode requires
+        aiobotocore module to be installed.
+        (default: False, but can be overridden in config file by setting 
default_deferrable to True)
     :param aws_conn_id: The Airflow connection used for AWS credentials.
         If this is ``None`` or empty then the default boto3 behaviour is used. 
If
         running Airflow in a distributed manner and aws_conn_id is None or
@@ -53,13 +59,48 @@ class QuickSightSensor(AwsBaseSensor[QuickSightHook]):
     aws_hook_class = QuickSightHook
     template_fields: Sequence[str] = ("data_set_id", "ingestion_id", 
"aws_conn_id")
 
-    def __init__(self, *, data_set_id: str, ingestion_id: str, **kwargs):
+    def __init__(
+        self,
+        *,
+        data_set_id: str,
+        ingestion_id: str,
+        max_retries: int = 2160,
+        deferrable: bool = conf.getboolean("operators", "default_deferrable", 
fallback=False),
+        **kwargs,
+    ):
         super().__init__(**kwargs)
         self.data_set_id = data_set_id
         self.ingestion_id = ingestion_id
+        self.max_retries = max_retries
+        self.deferrable = deferrable
         self.success_status = "COMPLETED"
         self.errored_statuses = ("FAILED", "CANCELLED")
 
+    def execute(self, context: Context) -> Any:
+        if self.deferrable:
+            self.defer(
+                trigger=QuickSightIngestionCompletedTrigger(
+                    data_set_id=self.data_set_id,
+                    ingestion_id=self.ingestion_id,
+                    aws_account_id=self.hook.account_id,
+                    waiter_delay=int(self.poke_interval),
+                    waiter_max_attempts=self.max_retries,
+                    aws_conn_id=self.aws_conn_id,
+                    region_name=self.region_name,
+                    verify=self.verify,
+                    botocore_config=self.botocore_config,
+                ),
+                method_name="execute_complete",
+            )
+        else:
+            return super().execute(context=context)
+
+    def execute_complete(self, context: Context, event: dict[str, Any] | None 
= None) -> None:
+        validated_event = validate_execute_complete_event(event)
+        if validated_event["status"] != "success":
+            raise RuntimeError(f"Error while waiting for the Amazon QuickSight 
ingestion: {validated_event}")
+        self.log.info("Amazon QuickSight SPICE ingestion `%s` completed.", 
validated_event["ingestion_id"])
+
     def poke(self, context: Context) -> bool:
         """
         Pokes until the QuickSight Ingestion has successfully finished.
@@ -72,5 +113,5 @@ class QuickSightSensor(AwsBaseSensor[QuickSightHook]):
         self.log.info("QuickSight Status: %s", quicksight_ingestion_state)
         if quicksight_ingestion_state in self.errored_statuses:
             error = self.hook.get_error_info(None, self.data_set_id, 
self.ingestion_id)
-            raise AirflowException(f"The QuickSight Ingestion failed. Error 
info: {error}")
+            raise RuntimeError(f"The QuickSight Ingestion failed. Error info: 
{error}")
         return quicksight_ingestion_state == self.success_status
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/triggers/quicksight.py 
b/providers/amazon/src/airflow/providers/amazon/aws/triggers/quicksight.py
new file mode 100644
index 00000000000..752af4b929e
--- /dev/null
+++ b/providers/amazon/src/airflow/providers/amazon/aws/triggers/quicksight.py
@@ -0,0 +1,87 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from airflow.providers.amazon.aws.hooks.base_aws import AwsGenericHook
+
+from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
+from airflow.providers.amazon.aws.triggers.base import AwsBaseWaiterTrigger
+
+
+class QuickSightIngestionCompletedTrigger(AwsBaseWaiterTrigger):
+    """
+    Trigger when an Amazon QuickSight SPICE ingestion is complete.
+
+    :param data_set_id: ID of the dataset used in the ingestion.
+    :param ingestion_id: ID for the ingestion.
+    :param aws_account_id: The ID of the AWS account that owns the dataset.
+    :param waiter_delay: The amount of time in seconds to wait between 
attempts. (default: 30)
+    :param waiter_max_attempts: The maximum number of attempts to be made. 
(default: 4320)
+    :param aws_conn_id: The Airflow connection used for AWS credentials.
+    :param region_name: AWS region_name. If not specified then the default 
boto3 behaviour is used.
+    :param verify: Whether or not to verify SSL certificates.
+    :param botocore_config: Configuration dictionary (key-values) for botocore 
client.
+    """
+
+    def __init__(
+        self,
+        *,
+        data_set_id: str,
+        ingestion_id: str,
+        aws_account_id: str,
+        waiter_delay: int = 30,
+        waiter_max_attempts: int = 4320,
+        aws_conn_id: str | None = "aws_default",
+        region_name: str | None = None,
+        verify: bool | str | None = None,
+        botocore_config: dict | None = None,
+    ) -> None:
+        super().__init__(
+            serialized_fields={
+                "data_set_id": data_set_id,
+                "ingestion_id": ingestion_id,
+                "aws_account_id": aws_account_id,
+            },
+            waiter_name="ingestion_complete",
+            waiter_args={
+                "AwsAccountId": aws_account_id,
+                "DataSetId": data_set_id,
+                "IngestionId": ingestion_id,
+            },
+            failure_message="Amazon QuickSight SPICE ingestion failed.",
+            status_message="Status of Amazon QuickSight SPICE ingestion is",
+            status_queries=["Ingestion.IngestionStatus", 
"Ingestion.ErrorInfo"],
+            return_key="ingestion_id",
+            return_value=ingestion_id,
+            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,
+        )
+
+    def hook(self) -> AwsGenericHook:
+        return QuickSightHook(
+            aws_conn_id=self.aws_conn_id,
+            region_name=self.region_name,
+            verify=self.verify,
+            config=self.botocore_config,
+        )
diff --git 
a/providers/amazon/src/airflow/providers/amazon/aws/waiters/quicksight.json 
b/providers/amazon/src/airflow/providers/amazon/aws/waiters/quicksight.json
new file mode 100644
index 00000000000..652c3b3d376
--- /dev/null
+++ b/providers/amazon/src/airflow/providers/amazon/aws/waiters/quicksight.json
@@ -0,0 +1,48 @@
+{
+    "version": 2,
+    "waiters": {
+        "ingestion_complete": {
+            "delay": 30,
+            "maxAttempts": 60,
+            "operation": "DescribeIngestion",
+            "acceptors": [
+                {
+                    "matcher": "path",
+                    "argument": "Ingestion.IngestionStatus",
+                    "expected": "INITIALIZED",
+                    "state": "retry"
+                },
+                {
+                    "matcher": "path",
+                    "argument": "Ingestion.IngestionStatus",
+                    "expected": "QUEUED",
+                    "state": "retry"
+                },
+                {
+                    "matcher": "path",
+                    "argument": "Ingestion.IngestionStatus",
+                    "expected": "RUNNING",
+                    "state": "retry"
+                },
+                {
+                    "matcher": "path",
+                    "argument": "Ingestion.IngestionStatus",
+                    "expected": "COMPLETED",
+                    "state": "success"
+                },
+                {
+                    "matcher": "path",
+                    "argument": "Ingestion.IngestionStatus",
+                    "expected": "FAILED",
+                    "state": "failure"
+                },
+                {
+                    "matcher": "path",
+                    "argument": "Ingestion.IngestionStatus",
+                    "expected": "CANCELLED",
+                    "state": "failure"
+                }
+            ]
+        }
+    }
+}
diff --git a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py 
b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
index ea3e1f49437..1038b5d698f 100644
--- a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
+++ b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
@@ -1022,6 +1022,10 @@ def get_provider_info():
                 "integration-name": "AWS Database Migration Service",
                 "python-modules": 
["airflow.providers.amazon.aws.triggers.dms"],
             },
+            {
+                "integration-name": "Amazon QuickSight",
+                "python-modules": 
["airflow.providers.amazon.aws.triggers.quicksight"],
+            },
         ],
         "transfers": [
             {
diff --git a/providers/amazon/tests/unit/amazon/aws/hooks/test_quicksight.py 
b/providers/amazon/tests/unit/amazon/aws/hooks/test_quicksight.py
index 8969eb72fb2..a6d93d81d06 100644
--- a/providers/amazon/tests/unit/amazon/aws/hooks/test_quicksight.py
+++ b/providers/amazon/tests/unit/amazon/aws/hooks/test_quicksight.py
@@ -22,6 +22,7 @@ from unittest import mock
 import pytest
 from botocore.exceptions import ClientError
 
+from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
 from airflow.providers.common.compat.sdk import AirflowException
 
@@ -183,7 +184,10 @@ class TestQuicksight:
     ):
         mocked_get_error_info.return_value = "Something Bad Happen"
         hook = QuickSightHook(aws_conn_id=None, region_name="us-east-1")
-        with pytest.raises(AirflowException, match="Error info: Something Bad 
Happen"):
+        with (
+            pytest.warns(AirflowProviderDeprecationWarning, 
match="wait_for_ingestion"),
+            pytest.raises(AirflowException, match="Error info: Something Bad 
Happen"),
+        ):
             hook.wait_for_state(
                 aws_account_id, "data_set_id", "ingestion_id", 
target_state={"COMPLETED"}, check_interval=0
             )
@@ -193,7 +197,10 @@ class TestQuicksight:
     @mock.patch.object(QuickSightHook, "get_status", return_value="CANCELLED")
     def test_wait_for_state_canceled(self, _):
         hook = QuickSightHook(aws_conn_id=None, region_name="us-east-1")
-        with pytest.raises(AirflowException, match="The Amazon QuickSight 
SPICE ingestion cancelled"):
+        with (
+            pytest.warns(AirflowProviderDeprecationWarning, 
match="wait_for_ingestion"),
+            pytest.raises(AirflowException, match="The Amazon QuickSight SPICE 
ingestion cancelled"),
+        ):
             hook.wait_for_state(
                 "aws_account_id", "data_set_id", "ingestion_id", 
target_state={"COMPLETED"}, check_interval=0
             )
@@ -202,12 +209,11 @@ class TestQuicksight:
     def test_wait_for_state_completed(self, mocked_get_status):
         mocked_get_status.side_effect = ["INITIALIZED", "QUEUED", "RUNNING", 
"COMPLETED"]
         hook = QuickSightHook(aws_conn_id=None, region_name="us-east-1")
-        assert (
-            hook.wait_for_state(
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="wait_for_ingestion"):
+            status = hook.wait_for_state(
                 "aws_account_id", "data_set_id", "ingestion_id", 
target_state={"COMPLETED"}, check_interval=0
             )
-            == "COMPLETED"
-        )
+        assert status == "COMPLETED"
         assert mocked_get_status.call_count == 4
 
     @pytest.mark.parametrize(
@@ -220,7 +226,7 @@ class TestQuicksight:
         mocked_client.create_ingestion.return_value = 
MOCK_CREATE_INGESTION_RESPONSE
 
         hook = QuickSightHook(aws_conn_id=None, region_name="us-east-1")
-        with mock.patch.object(QuickSightHook, "wait_for_state") as 
mocked_wait_for_state:
+        with mock.patch.object(QuickSightHook, "wait_for_ingestion") as 
mocked_wait_for_ingestion:
             assert (
                 hook.create_ingestion(
                     data_set_id="DemoDataSet",
@@ -229,23 +235,24 @@ class TestQuicksight:
                     aws_account_id=aws_account_id,
                     wait_for_completion=wait_for_completion,
                     check_interval=0,
+                    waiter_max_attempts=2,
                 )
                 == MOCK_CREATE_INGESTION_RESPONSE
             )
             if wait_for_completion:
-                mocked_wait_for_state.assert_called_once_with(
-                    aws_account_id=expected_account_id,
+                mocked_wait_for_ingestion.assert_called_once_with(
                     data_set_id="DemoDataSet",
                     ingestion_id="DemoDataSet_Ingestion",
-                    target_state={"COMPLETED"},
-                    check_interval=0,
+                    aws_account_id=expected_account_id,
+                    waiter_delay=0,
+                    waiter_max_attempts=2,
                 )
             else:
-                mocked_wait_for_state.assert_not_called()
+                mocked_wait_for_ingestion.assert_not_called()
 
         
mocked_client.create_ingestion.assert_called_with(AwsAccountId=expected_account_id,
 **MOCK_DATA)
 
-    def test_create_ingestion_exception(self, mocked_account_id, 
mocked_client, caplog):
+    def test_create_ingestion_exception(self, mocked_account_id, 
mocked_client):
         mocked_client.create_ingestion.side_effect = ValueError("Fake Error")
         hook = QuickSightHook(aws_conn_id=None)
         with pytest.raises(ValueError, match="Fake Error"):
@@ -254,4 +261,46 @@ class TestQuicksight:
                 ingestion_id="DemoDataSet_Ingestion",
                 ingestion_type="INCREMENTAL_REFRESH",
             )
-        assert "create_ingestion API, error: Fake Error" in caplog.text
+
+    @pytest.mark.parametrize(("aws_account_id", "expected_account_id"), 
ACCOUNT_TEST_CASES)
+    @mock.patch("airflow.providers.amazon.aws.hooks.quicksight.wait")
+    @mock.patch.object(QuickSightHook, "get_waiter")
+    def test_wait_for_ingestion(
+        self, mocked_get_waiter, mocked_wait, aws_account_id, 
expected_account_id, mocked_account_id
+    ):
+        hook = QuickSightHook(aws_conn_id=None, region_name="us-east-1")
+        hook.wait_for_ingestion(
+            data_set_id="DemoDataSet",
+            ingestion_id="DemoDataSet_Ingestion",
+            aws_account_id=aws_account_id,
+            waiter_delay=1,
+            waiter_max_attempts=2,
+        )
+        mocked_get_waiter.assert_called_once_with("ingestion_complete")
+        mocked_wait.assert_called_once_with(
+            waiter=mocked_get_waiter.return_value,
+            waiter_delay=1,
+            waiter_max_attempts=2,
+            args={
+                "AwsAccountId": expected_account_id,
+                "DataSetId": "DemoDataSet",
+                "IngestionId": "DemoDataSet_Ingestion",
+            },
+            failure_message="Amazon QuickSight SPICE ingestion failed.",
+            status_message="Status of Amazon QuickSight SPICE ingestion is",
+            status_args=["Ingestion.IngestionStatus", "Ingestion.ErrorInfo"],
+        )
+
+    @mock.patch(
+        "airflow.providers.amazon.aws.hooks.quicksight.wait",
+        side_effect=AirflowException("Waiter error: max attempts reached"),
+    )
+    @mock.patch.object(QuickSightHook, "get_waiter")
+    def test_wait_for_ingestion_failure(self, mocked_get_waiter, mocked_wait):
+        hook = QuickSightHook(aws_conn_id=None, region_name="us-east-1")
+        with pytest.raises(RuntimeError, match="max attempts reached"):
+            hook.wait_for_ingestion(
+                data_set_id="DemoDataSet",
+                ingestion_id="DemoDataSet_Ingestion",
+                aws_account_id="123456789012",
+            )
diff --git 
a/providers/amazon/tests/unit/amazon/aws/operators/test_quicksight.py 
b/providers/amazon/tests/unit/amazon/aws/operators/test_quicksight.py
index 5b0edd1ff5b..f3043c05073 100644
--- a/providers/amazon/tests/unit/amazon/aws/operators/test_quicksight.py
+++ b/providers/amazon/tests/unit/amazon/aws/operators/test_quicksight.py
@@ -19,8 +19,13 @@ from __future__ import annotations
 
 from unittest import mock
 
+import pytest
+
+from airflow.exceptions import AirflowProviderDeprecationWarning
 from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
 from airflow.providers.amazon.aws.operators.quicksight import 
QuickSightCreateIngestionOperator
+from airflow.providers.amazon.aws.triggers.quicksight import 
QuickSightIngestionCompletedTrigger
+from airflow.providers.common.compat.sdk import TaskDeferred
 
 from unit.amazon.aws.utils.test_template_fields import validate_template_fields
 
@@ -38,6 +43,13 @@ MOCK_RESPONSE = {
 }
 
 
[email protected]
+def mocked_account_id():
+    with mock.patch.object(QuickSightHook, "account_id", 
new_callable=mock.PropertyMock) as m:
+        m.return_value = AWS_ACCOUNT_ID
+        yield m
+
+
 class TestQuickSightCreateIngestionOperator:
     def setup_method(self):
         self.default_op_kwargs = {
@@ -71,16 +83,94 @@ class TestQuickSightCreateIngestionOperator:
         assert op.hook._region_name is None
         assert op.hook._verify is None
         assert op.hook._config is None
+        assert op.wait_for_completion is True
+        assert op.waiter_delay == 30
+        assert op.waiter_max_attempts == 4320
+        assert op.deferrable is False
 
+    @pytest.mark.parametrize(
+        ("wait_for_completion", "deferrable"),
+        [
+            pytest.param(False, False, id="no_wait"),
+            pytest.param(True, False, id="wait"),
+            pytest.param(False, True, id="defer"),
+        ],
+    )
+    @mock.patch.object(QuickSightCreateIngestionOperator, "defer")
+    @mock.patch.object(QuickSightHook, "wait_for_ingestion")
     @mock.patch.object(QuickSightHook, "create_ingestion", 
return_value=MOCK_RESPONSE)
-    def test_execute(self, mock_create_ingestion):
-        QuickSightCreateIngestionOperator(**self.default_op_kwargs).execute({})
+    def test_execute_wait_combinations(
+        self,
+        mock_create_ingestion,
+        mock_wait_for_ingestion,
+        mock_defer,
+        wait_for_completion,
+        deferrable,
+        mocked_account_id,
+    ):
+        op = QuickSightCreateIngestionOperator(
+            **self.default_op_kwargs, wait_for_completion=wait_for_completion, 
deferrable=deferrable
+        )
+
+        response = op.execute({})
+
+        assert response == MOCK_RESPONSE
         mock_create_ingestion.assert_called_once_with(
             data_set_id=DATA_SET_ID,
             ingestion_id=INGESTION_ID,
-            ingestion_type="FULL_REFRESH",
-            wait_for_completion=True,
-            check_interval=30,
+            ingestion_type=INGESTION_TYPE,
+            wait_for_completion=False,
+        )
+        assert mock_wait_for_ingestion.call_count == wait_for_completion
+        assert mock_defer.call_count == deferrable
+
+    @mock.patch.object(QuickSightHook, "create_ingestion", 
return_value=MOCK_RESPONSE)
+    def test_execute_deferrable(self, mock_create_ingestion, 
mocked_account_id):
+        op = QuickSightCreateIngestionOperator(
+            **self.default_op_kwargs, deferrable=True, waiter_delay=15, 
waiter_max_attempts=3
+        )
+
+        with pytest.raises(TaskDeferred) as defer_exc:
+            op.execute({})
+
+        trigger = defer_exc.value.trigger
+        assert isinstance(trigger, QuickSightIngestionCompletedTrigger)
+        assert defer_exc.value.method_name == "execute_complete"
+        assert defer_exc.value.kwargs == {"ingestion": MOCK_RESPONSE}
+        _, kwargs = trigger.serialize()
+        assert kwargs["data_set_id"] == DATA_SET_ID
+        assert kwargs["ingestion_id"] == INGESTION_ID
+        assert kwargs["aws_account_id"] == AWS_ACCOUNT_ID
+        assert kwargs["waiter_delay"] == 15
+        assert kwargs["waiter_max_attempts"] == 3
+
+    def test_execute_complete(self):
+        op = QuickSightCreateIngestionOperator(**self.default_op_kwargs)
+        event = {"status": "success", "ingestion_id": INGESTION_ID}
+
+        assert op.execute_complete({}, event, ingestion=MOCK_RESPONSE) == 
MOCK_RESPONSE
+
+    def test_execute_complete_failure(self):
+        op = QuickSightCreateIngestionOperator(**self.default_op_kwargs)
+        event = {"status": "error", "message": "test failure", "ingestion_id": 
INGESTION_ID}
+
+        with pytest.raises(RuntimeError, match="Error while running"):
+            op.execute_complete({}, event)
+
+    @pytest.mark.parametrize("check_interval", [10, "10"], ids=["int", 
"templated_str"])
+    @mock.patch.object(QuickSightHook, "wait_for_ingestion")
+    @mock.patch.object(QuickSightHook, "create_ingestion", 
return_value=MOCK_RESPONSE)
+    def test_check_interval_deprecation(self, mock_create_ingestion, 
mock_wait_for_ingestion, check_interval):
+        op = QuickSightCreateIngestionOperator(**self.default_op_kwargs, 
check_interval=check_interval)
+
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="check_interval"):
+            op.execute({})
+
+        mock_wait_for_ingestion.assert_called_once_with(
+            data_set_id=DATA_SET_ID,
+            ingestion_id=INGESTION_ID,
+            waiter_delay=10,
+            waiter_max_attempts=4320,
         )
 
     def test_template_fields(self):
diff --git a/providers/amazon/tests/unit/amazon/aws/sensors/test_quicksight.py 
b/providers/amazon/tests/unit/amazon/aws/sensors/test_quicksight.py
index 277749edd52..b65b8e6234a 100644
--- a/providers/amazon/tests/unit/amazon/aws/sensors/test_quicksight.py
+++ b/providers/amazon/tests/unit/amazon/aws/sensors/test_quicksight.py
@@ -23,10 +23,12 @@ import pytest
 
 from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
 from airflow.providers.amazon.aws.sensors.quicksight import QuickSightSensor
-from airflow.providers.common.compat.sdk import AirflowException
+from airflow.providers.amazon.aws.triggers.quicksight import 
QuickSightIngestionCompletedTrigger
+from airflow.providers.common.compat.sdk import TaskDeferred
 
 DATA_SET_ID = "DemoDataSet"
 INGESTION_ID = "DemoDataSet_Ingestion"
+AWS_ACCOUNT_ID = "123456789012"
 
 
 @pytest.fixture
@@ -41,6 +43,13 @@ def mocked_get_error_info():
         yield m
 
 
[email protected]
+def mocked_account_id():
+    with mock.patch.object(QuickSightHook, "account_id", 
new_callable=mock.PropertyMock) as m:
+        m.return_value = AWS_ACCOUNT_ID
+        yield m
+
+
 class TestQuickSightSensor:
     def setup_method(self):
         self.default_op_kwargs = {
@@ -74,6 +83,8 @@ class TestQuickSightSensor:
         assert sensor.hook._region_name is None
         assert sensor.hook._verify is None
         assert sensor.hook._config is None
+        assert sensor.max_retries == 2160
+        assert sensor.deferrable is False
 
     @pytest.mark.parametrize("status", ["COMPLETED"])
     def test_poke_completed(self, status, mocked_get_status):
@@ -91,7 +102,38 @@ class TestQuickSightSensor:
     def test_poke_terminated_status(self, status, mocked_get_status, 
mocked_get_error_info):
         mocked_get_status.return_value = status
         mocked_get_error_info.return_value = "something bad happen"
-        with pytest.raises(AirflowException, match="Error info: something bad 
happen"):
+        with pytest.raises(RuntimeError, match="Error info: something bad 
happen"):
             QuickSightSensor(**self.default_op_kwargs).poke({})
         mocked_get_status.assert_called_once_with(None, DATA_SET_ID, 
INGESTION_ID)
         mocked_get_error_info.assert_called_once_with(None, DATA_SET_ID, 
INGESTION_ID)
+
+    def test_execute_deferrable(self, mocked_account_id):
+        sensor = QuickSightSensor(
+            **self.default_op_kwargs, deferrable=True, poke_interval=20.5, 
max_retries=3
+        )
+
+        with pytest.raises(TaskDeferred) as defer_exc:
+            sensor.execute({})
+
+        trigger = defer_exc.value.trigger
+        assert isinstance(trigger, QuickSightIngestionCompletedTrigger)
+        assert defer_exc.value.method_name == "execute_complete"
+        _, kwargs = trigger.serialize()
+        assert kwargs["data_set_id"] == DATA_SET_ID
+        assert kwargs["ingestion_id"] == INGESTION_ID
+        assert kwargs["aws_account_id"] == AWS_ACCOUNT_ID
+        assert kwargs["waiter_delay"] == 20
+        assert kwargs["waiter_max_attempts"] == 3
+
+    def test_execute_complete(self):
+        sensor = QuickSightSensor(**self.default_op_kwargs)
+        event = {"status": "success", "ingestion_id": INGESTION_ID}
+
+        assert sensor.execute_complete({}, event) is None
+
+    def test_execute_complete_failure(self):
+        sensor = QuickSightSensor(**self.default_op_kwargs)
+        event = {"status": "error", "message": "Waiter error: max attempts 
reached"}
+
+        with pytest.raises(RuntimeError, match="Error while waiting"):
+            sensor.execute_complete({}, event)
diff --git a/providers/amazon/tests/unit/amazon/aws/triggers/test_quicksight.py 
b/providers/amazon/tests/unit/amazon/aws/triggers/test_quicksight.py
new file mode 100644
index 00000000000..bb4b61e50db
--- /dev/null
+++ b/providers/amazon/tests/unit/amazon/aws/triggers/test_quicksight.py
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+from unittest.mock import AsyncMock
+
+import pytest
+
+from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
+from airflow.providers.amazon.aws.triggers.quicksight import 
QuickSightIngestionCompletedTrigger
+from airflow.triggers.base import TriggerEvent
+
+from unit.amazon.aws.utils.test_waiter import assert_expected_waiter_type
+
+BASE_TRIGGER_CLASSPATH = "airflow.providers.amazon.aws.triggers.quicksight."
+EXPECTED_WAITER_NAME = "ingestion_complete"
+DATA_SET_ID = "DemoDataSet"
+INGESTION_ID = "DemoDataSet_Ingestion"
+AWS_ACCOUNT_ID = "123456789012"
+
+
+class TestQuickSightIngestionCompletedTrigger:
+    def test_serialization(self):
+        """Assert that arguments and classpath are correctly serialized."""
+        trigger = QuickSightIngestionCompletedTrigger(
+            data_set_id=DATA_SET_ID, ingestion_id=INGESTION_ID, 
aws_account_id=AWS_ACCOUNT_ID
+        )
+        classpath, kwargs = trigger.serialize()
+        assert classpath == BASE_TRIGGER_CLASSPATH + 
"QuickSightIngestionCompletedTrigger"
+        assert kwargs.get("data_set_id") == DATA_SET_ID
+        assert kwargs.get("ingestion_id") == INGESTION_ID
+        assert kwargs.get("aws_account_id") == AWS_ACCOUNT_ID
+
+    @pytest.mark.asyncio
+    @mock.patch.object(QuickSightHook, "get_waiter")
+    @mock.patch.object(QuickSightHook, "get_async_conn")
+    async def test_run_success(self, mock_async_conn, mock_get_waiter):
+        mock_async_conn.__aenter__.return_value = mock.MagicMock()
+        mock_get_waiter().wait = AsyncMock()
+        trigger = QuickSightIngestionCompletedTrigger(
+            data_set_id=DATA_SET_ID, ingestion_id=INGESTION_ID, 
aws_account_id=AWS_ACCOUNT_ID
+        )
+
+        generator = trigger.run()
+        response = await generator.asend(None)
+
+        assert response == TriggerEvent({"status": "success", "ingestion_id": 
INGESTION_ID})
+        assert_expected_waiter_type(mock_get_waiter, EXPECTED_WAITER_NAME)
+        mock_get_waiter().wait.assert_called_once()
diff --git a/providers/amazon/tests/unit/amazon/aws/waiters/test_quicksight.py 
b/providers/amazon/tests/unit/amazon/aws/waiters/test_quicksight.py
new file mode 100644
index 00000000000..9ccbe8e0edc
--- /dev/null
+++ b/providers/amazon/tests/unit/amazon/aws/waiters/test_quicksight.py
@@ -0,0 +1,76 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import boto3
+import botocore
+import pytest
+
+from airflow.providers.amazon.aws.hooks.quicksight import QuickSightHook
+
+INGESTION_WAITER_ARGS = {
+    "AwsAccountId": "123456789012",
+    "DataSetId": "DemoDataSet",
+    "IngestionId": "DemoDataSet_Ingestion",
+}
+
+
+class TestQuickSightCustomWaiters:
+    def test_service_waiters(self):
+        assert "ingestion_complete" in QuickSightHook().list_waiters()
+
+
+class TestQuickSightCustomWaitersBase:
+    @pytest.fixture(autouse=True)
+    def mock_conn(self, monkeypatch):
+        self.client = boto3.client("quicksight")
+        monkeypatch.setattr(QuickSightHook, "conn", self.client)
+
+
+class TestQuickSightIngestionCompleteWaiter(TestQuickSightCustomWaitersBase):
+    WAITER_NAME = "ingestion_complete"
+
+    @pytest.fixture
+    def mock_describe_ingestion(self):
+        with mock.patch.object(self.client, "describe_ingestion") as 
mock_getter:
+            yield mock_getter
+
+    def test_ingestion_complete(self, mock_describe_ingestion):
+        mock_describe_ingestion.return_value = {"Ingestion": 
{"IngestionStatus": "COMPLETED"}}
+
+        
QuickSightHook().get_waiter(self.WAITER_NAME).wait(**INGESTION_WAITER_ARGS)
+
+    @pytest.mark.parametrize("state", ["FAILED", "CANCELLED"])
+    def test_ingestion_failed(self, state, mock_describe_ingestion):
+        mock_describe_ingestion.return_value = {"Ingestion": 
{"IngestionStatus": state}}
+
+        with pytest.raises(botocore.exceptions.WaiterError):
+            
QuickSightHook().get_waiter(self.WAITER_NAME).wait(**INGESTION_WAITER_ARGS)
+
+    def test_ingestion_wait(self, mock_describe_ingestion):
+        mock_describe_ingestion.side_effect = [
+            {"Ingestion": {"IngestionStatus": "INITIALIZED"}},
+            {"Ingestion": {"IngestionStatus": "QUEUED"}},
+            {"Ingestion": {"IngestionStatus": "RUNNING"}},
+            {"Ingestion": {"IngestionStatus": "COMPLETED"}},
+        ]
+
+        QuickSightHook().get_waiter(self.WAITER_NAME).wait(
+            **INGESTION_WAITER_ARGS, WaiterConfig={"Delay": 0.01, 
"MaxAttempts": 4}
+        )

Reply via email to