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 32168fdf5ef Add deferrable mode to DynamoDBValueSensor (#69799)
32168fdf5ef is described below
commit 32168fdf5ef88c7f0efd0e1ba0c40f426869753e
Author: Harsh Gupta <[email protected]>
AuthorDate: Tue Jul 28 05:28:36 2026 +0530
Add deferrable mode to DynamoDBValueSensor (#69799)
DynamoDBValueSensor previously only supported poke/reschedule modes,
occupying a worker slot while waiting for a DynamoDB attribute value.
This adds the standard deferrable path used across the Amazon provider:
---
providers/amazon/docs/operators/dynamodb.rst | 1 +
providers/amazon/provider.yaml | 3 +
.../providers/amazon/aws/sensors/dynamodb.py | 39 +++++
.../providers/amazon/aws/triggers/dynamodb.py | 168 ++++++++++++++++++++
.../airflow/providers/amazon/get_provider_info.py | 4 +
.../tests/unit/amazon/aws/sensors/test_dynamodb.py | 54 +++++++
.../unit/amazon/aws/triggers/test_dynamodb.py | 175 +++++++++++++++++++++
7 files changed, 444 insertions(+)
diff --git a/providers/amazon/docs/operators/dynamodb.rst
b/providers/amazon/docs/operators/dynamodb.rst
index f802fcdb423..633b311fcf0 100644
--- a/providers/amazon/docs/operators/dynamodb.rst
+++ b/providers/amazon/docs/operators/dynamodb.rst
@@ -45,6 +45,7 @@ Wait on Amazon DynamoDB item attribute value match
Use the
:class:`~airflow.providers.amazon.aws.sensors.dynamodb.DynamoDBValueSensor`
to wait for the presence of a matching DynamoDB item's attribute/value pair.
+This sensor can also be run in deferrable mode by setting ``deferrable`` param
to ``True``.
Wait for a Single Attribute Value Match:
----------------------------------------
diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml
index 0121c5b5ee2..343398db658 100644
--- a/providers/amazon/provider.yaml
+++ b/providers/amazon/provider.yaml
@@ -827,6 +827,9 @@ triggers:
- integration-name: Amazon Comprehend
python-modules:
- airflow.providers.amazon.aws.triggers.comprehend
+ - integration-name: Amazon DynamoDB
+ python-modules:
+ - airflow.providers.amazon.aws.triggers.dynamodb
- integration-name: Amazon EC2
python-modules:
- airflow.providers.amazon.aws.triggers.ec2
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/sensors/dynamodb.py
b/providers/amazon/src/airflow/providers/amazon/aws/sensors/dynamodb.py
index 8bf1a67f56c..a3efa77b04f 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/sensors/dynamodb.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/sensors/dynamodb.py
@@ -17,13 +17,17 @@
from __future__ import annotations
from collections.abc import Iterable, Sequence
+from datetime import timedelta
from typing import TYPE_CHECKING, Any
from botocore.exceptions import ClientError
from airflow.providers.amazon.aws.hooks.dynamodb import DynamoDBHook
from airflow.providers.amazon.aws.sensors.base_aws import AwsBaseSensor
+from airflow.providers.amazon.aws.triggers.dynamodb import
DynamoDBValueSensorTrigger
+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
@@ -44,6 +48,9 @@ class DynamoDBValueSensor(AwsBaseSensor[DynamoDBHook]):
:param attribute_value: DynamoDB attribute value
:param sort_key_name: (optional) DynamoDB sort key name
:param sort_key_value: (optional) DynamoDB sort key value
+ :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
@@ -76,6 +83,7 @@ class DynamoDBValueSensor(AwsBaseSensor[DynamoDBHook]):
attribute_value: str | Iterable[str],
sort_key_name: str | None = None,
sort_key_value: str | None = None,
+ deferrable: bool = conf.getboolean("operators", "default_deferrable",
fallback=False),
**kwargs: Any,
):
super().__init__(**kwargs)
@@ -86,6 +94,37 @@ class DynamoDBValueSensor(AwsBaseSensor[DynamoDBHook]):
self.attribute_value = attribute_value
self.sort_key_name = sort_key_name
self.sort_key_value = sort_key_value
+ self.deferrable = deferrable
+
+ def execute(self, context: Context) -> Any:
+ if self.deferrable:
+ self.defer(
+ trigger=DynamoDBValueSensorTrigger(
+ table_name=self.table_name,
+ partition_key_name=self.partition_key_name,
+ partition_key_value=self.partition_key_value,
+ attribute_name=self.attribute_name,
+ attribute_value=self.attribute_value,
+ sort_key_name=self.sort_key_name,
+ sort_key_value=self.sort_key_value,
+ waiter_delay=int(self.poke_interval),
+ aws_conn_id=self.aws_conn_id,
+ region_name=self.region_name,
+ verify=self.verify,
+ botocore_config=self.botocore_config,
+ ),
+ method_name="execute_complete",
+ timeout=timedelta(seconds=self.timeout),
+ )
+ else:
+ super().execute(context=context)
+
+ def execute_complete(self, context: Context, event: dict | None = None) ->
None:
+ validated_event = validate_execute_complete_event(event)
+
+ if validated_event["status"] != "success":
+ raise RuntimeError(f"Trigger error: event is {validated_event}")
+ self.log.info("DynamoDB attribute value match found; sensor complete.")
def poke(self, context: Context) -> bool:
"""Test DynamoDB item for matching attribute value."""
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/triggers/dynamodb.py
b/providers/amazon/src/airflow/providers/amazon/aws/triggers/dynamodb.py
new file mode 100644
index 00000000000..f1008b79f4d
--- /dev/null
+++ b/providers/amazon/src/airflow/providers/amazon/aws/triggers/dynamodb.py
@@ -0,0 +1,168 @@
+# 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
+
+import asyncio
+from collections.abc import AsyncIterator, Iterable
+from functools import cached_property
+from typing import Any
+
+from boto3.dynamodb.types import TypeDeserializer
+from botocore.exceptions import ClientError
+
+from airflow.providers.amazon.aws.hooks.base_aws import AwsBaseHook
+from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS
+
+if AIRFLOW_V_3_0_PLUS:
+ from airflow.triggers.base import BaseEventTrigger, TriggerEvent
+else:
+ from airflow.triggers.base import ( # type: ignore
+ BaseTrigger as BaseEventTrigger,
+ TriggerEvent,
+ )
+
+
+class DynamoDBValueSensorTrigger(BaseEventTrigger):
+ """
+ Asynchronously poll a DynamoDB item until the given attribute matches one
of the expected values.
+
+ The polling uses the low-level DynamoDB client (aiobotocore exposes
clients only, not the
+ boto3 resource API used by ``DynamoDBHook``), so keys are sent as typed
string attributes,
+ matching the string-typed key parameters of
:class:`~airflow.providers.amazon.aws.sensors.dynamodb.DynamoDBValueSensor`.
+
+ :param table_name: DynamoDB table name
+ :param partition_key_name: DynamoDB partition key name
+ :param partition_key_value: DynamoDB partition key value
+ :param attribute_name: DynamoDB attribute name
+ :param attribute_value: expected DynamoDB attribute value (or values, any
of which matches)
+ :param sort_key_name: (optional) DynamoDB sort key name
+ :param sort_key_value: (optional) DynamoDB sort key value
+ :param waiter_delay: The time in seconds to wait between DynamoDB API calls
+ :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. See:
+
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
+ """
+
+ def __init__(
+ self,
+ table_name: str,
+ partition_key_name: str,
+ partition_key_value: str,
+ attribute_name: str,
+ attribute_value: str | Iterable[str],
+ sort_key_name: str | None = None,
+ sort_key_value: str | None = None,
+ waiter_delay: int = 60,
+ aws_conn_id: str | None = "aws_default",
+ region_name: str | None = None,
+ verify: bool | str | None = None,
+ botocore_config: dict | None = None,
+ ):
+ self.table_name = table_name
+ self.partition_key_name = partition_key_name
+ self.partition_key_value = partition_key_value
+ self.attribute_name = attribute_name
+ self.attribute_value = (
+ [attribute_value] if isinstance(attribute_value, str) else
list(attribute_value)
+ )
+ self.sort_key_name = sort_key_name
+ self.sort_key_value = sort_key_value
+ self.waiter_delay = waiter_delay
+ self.aws_conn_id = aws_conn_id
+ self.region_name = region_name
+ self.verify = verify
+ self.botocore_config = botocore_config
+
+ def serialize(self) -> tuple[str, dict[str, Any]]:
+ return (
+ self.__class__.__module__ + "." + self.__class__.__qualname__,
+ {
+ "table_name": self.table_name,
+ "partition_key_name": self.partition_key_name,
+ "partition_key_value": self.partition_key_value,
+ "attribute_name": self.attribute_name,
+ "attribute_value": self.attribute_value,
+ "sort_key_name": self.sort_key_name,
+ "sort_key_value": self.sort_key_value,
+ "waiter_delay": self.waiter_delay,
+ "aws_conn_id": self.aws_conn_id,
+ "region_name": self.region_name,
+ "verify": self.verify,
+ "botocore_config": self.botocore_config,
+ },
+ )
+
+ @cached_property
+ def hook(self) -> AwsBaseHook:
+ # DynamoDBHook is resource-based, but async connections are only
available for
+ # clients, so the trigger talks to DynamoDB through a client-type hook
instead.
+ return AwsBaseHook(
+ aws_conn_id=self.aws_conn_id,
+ region_name=self.region_name,
+ verify=self.verify,
+ config=self.botocore_config,
+ client_type="dynamodb",
+ )
+
+ @property
+ def key(self) -> dict[str, Any]:
+ key = {self.partition_key_name: {"S": self.partition_key_value}}
+ if self.sort_key_name and self.sort_key_value:
+ key[self.sort_key_name] = {"S": self.sort_key_value}
+ return key
+
+ async def poke(self, client: Any) -> bool:
+ """Test the DynamoDB item for a matching attribute value, mirroring
the sensor's poke."""
+ self.log.info(
+ "Checking table %s for item with key %s, waiting for attribute %s
to be one of %s",
+ self.table_name,
+ self.key,
+ self.attribute_name,
+ self.attribute_value,
+ )
+ try:
+ response = await client.get_item(TableName=self.table_name,
Key=self.key)
+ except ClientError as err:
+ # Same tolerance as the sensor's poke: log and keep trying until
the task times out.
+ self.log.error(
+ "Couldn't get %s from table %s.\nError Code: %s\nError
Message: %s",
+ self.key,
+ self.table_name,
+ err.response["Error"]["Code"],
+ err.response["Error"]["Message"],
+ )
+ return False
+
+ try:
+ typed_attribute_value = response["Item"][self.attribute_name]
+ except KeyError:
+ return False
+ item_attribute_value =
TypeDeserializer().deserialize(typed_attribute_value)
+ self.log.info("Got: %s = %s", self.attribute_name,
item_attribute_value)
+ return item_attribute_value in self.attribute_value
+
+ async def run(self) -> AsyncIterator[TriggerEvent]:
+ while True:
+ # This loop runs until the timeout set in the sensor's self.defer
call is reached.
+ async with await self.hook.get_async_conn() as client:
+ if await self.poke(client=client):
+ yield TriggerEvent({"status": "success"})
+ return
+ await asyncio.sleep(self.waiter_delay)
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 5314709a5df..a3e609aa5a8 100644
--- a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
+++ b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
@@ -926,6 +926,10 @@ def get_provider_info():
"integration-name": "Amazon Comprehend",
"python-modules":
["airflow.providers.amazon.aws.triggers.comprehend"],
},
+ {
+ "integration-name": "Amazon DynamoDB",
+ "python-modules":
["airflow.providers.amazon.aws.triggers.dynamodb"],
+ },
{
"integration-name": "Amazon EC2",
"python-modules":
["airflow.providers.amazon.aws.triggers.ec2"],
diff --git a/providers/amazon/tests/unit/amazon/aws/sensors/test_dynamodb.py
b/providers/amazon/tests/unit/amazon/aws/sensors/test_dynamodb.py
index e8ea324ff8c..46999c4c81a 100644
--- a/providers/amazon/tests/unit/amazon/aws/sensors/test_dynamodb.py
+++ b/providers/amazon/tests/unit/amazon/aws/sensors/test_dynamodb.py
@@ -19,10 +19,13 @@ from __future__ import annotations
from unittest import mock
+import pytest
from moto import mock_aws
from airflow.providers.amazon.aws.hooks.dynamodb import DynamoDBHook
from airflow.providers.amazon.aws.sensors.dynamodb import DynamoDBValueSensor
+from airflow.providers.amazon.aws.triggers.dynamodb import
DynamoDBValueSensorTrigger
+from airflow.providers.common.compat.sdk import TaskDeferred
class TestDynamoDBValueSensor:
@@ -120,6 +123,57 @@ class TestDynamoDBValueSensor:
self.sensor_pk.partition_key_name = "no such key"
assert self.sensor_pk.poke(None) is False
+ def test_sensor_deferrable(self):
+ sensor = DynamoDBValueSensor(
+ task_id="dynamodb_value_sensor_deferrable",
+ table_name=self.table_name,
+ partition_key_name=self.pk_name,
+ partition_key_value=self.pk_value,
+ attribute_name=self.attribute_name,
+ attribute_value=self.attribute_value,
+ sort_key_name=self.sk_name,
+ sort_key_value=self.sk_value,
+ deferrable=True,
+ )
+ with pytest.raises(TaskDeferred) as defer:
+ sensor.execute({})
+ trigger = defer.value.trigger
+ assert isinstance(trigger, DynamoDBValueSensorTrigger)
+ assert trigger.table_name == self.table_name
+ assert trigger.partition_key_name == self.pk_name
+ assert trigger.partition_key_value == self.pk_value
+ assert trigger.attribute_name == self.attribute_name
+ assert trigger.attribute_value == [self.attribute_value]
+ assert trigger.sort_key_name == self.sk_name
+ assert trigger.sort_key_value == self.sk_value
+
+ def test_execute_complete(self):
+ sensor = DynamoDBValueSensor(
+ task_id="dynamodb_value_sensor_deferrable",
+ table_name=self.table_name,
+ partition_key_name=self.pk_name,
+ partition_key_value=self.pk_value,
+ attribute_name=self.attribute_name,
+ attribute_value=self.attribute_value,
+ deferrable=True,
+ )
+ sensor.execute_complete(context={}, event={"status": "success"})
+
+ def test_fail_execute_complete(self):
+ sensor = DynamoDBValueSensor(
+ task_id="dynamodb_value_sensor_deferrable",
+ table_name=self.table_name,
+ partition_key_name=self.pk_name,
+ partition_key_value=self.pk_value,
+ attribute_name=self.attribute_name,
+ attribute_value=self.attribute_value,
+ deferrable=True,
+ )
+ event = {"status": "failed"}
+ message = f"Trigger error: event is {event}"
+ with pytest.raises(RuntimeError, match=message):
+ sensor.execute_complete(context={}, event=event)
+
class TestDynamoDBMultipleValuesSensor:
def setup_method(self):
diff --git a/providers/amazon/tests/unit/amazon/aws/triggers/test_dynamodb.py
b/providers/amazon/tests/unit/amazon/aws/triggers/test_dynamodb.py
new file mode 100644
index 00000000000..afcd1991384
--- /dev/null
+++ b/providers/amazon/tests/unit/amazon/aws/triggers/test_dynamodb.py
@@ -0,0 +1,175 @@
+# 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 botocore.exceptions import ClientError
+
+from airflow.providers.amazon.aws.triggers.dynamodb import
DynamoDBValueSensorTrigger
+from airflow.triggers.base import TriggerEvent
+
+TEST_TABLE_NAME = "test-table"
+TEST_PK_NAME = "PK"
+TEST_PK_VALUE = "PKTest"
+TEST_SK_NAME = "SK"
+TEST_SK_VALUE = "SKTest"
+TEST_ATTRIBUTE_NAME = "Foo"
+TEST_ATTRIBUTE_VALUE = "Bar"
+TEST_WAITER_DELAY = 1
+TEST_CONN_ID = "test-conn-id"
+TEST_REGION_NAME = "eu-central-1"
+
+
+def _build_trigger(**kwargs) -> DynamoDBValueSensorTrigger:
+ params = {
+ "table_name": TEST_TABLE_NAME,
+ "partition_key_name": TEST_PK_NAME,
+ "partition_key_value": TEST_PK_VALUE,
+ "attribute_name": TEST_ATTRIBUTE_NAME,
+ "attribute_value": TEST_ATTRIBUTE_VALUE,
+ "waiter_delay": TEST_WAITER_DELAY,
+ "aws_conn_id": TEST_CONN_ID,
+ "region_name": TEST_REGION_NAME,
+ **kwargs,
+ }
+ return DynamoDBValueSensorTrigger(**params)
+
+
+class TestDynamoDBValueSensorTrigger:
+ def test_serialize(self):
+ trigger = _build_trigger(sort_key_name=TEST_SK_NAME,
sort_key_value=TEST_SK_VALUE)
+
+ class_path, args = trigger.serialize()
+ assert class_path ==
"airflow.providers.amazon.aws.triggers.dynamodb.DynamoDBValueSensorTrigger"
+ assert args["table_name"] == TEST_TABLE_NAME
+ assert args["partition_key_name"] == TEST_PK_NAME
+ assert args["partition_key_value"] == TEST_PK_VALUE
+ assert args["attribute_name"] == TEST_ATTRIBUTE_NAME
+ # a single string value is normalized to a list so serialization
round-trips
+ assert args["attribute_value"] == [TEST_ATTRIBUTE_VALUE]
+ assert args["sort_key_name"] == TEST_SK_NAME
+ assert args["sort_key_value"] == TEST_SK_VALUE
+ assert args["waiter_delay"] == TEST_WAITER_DELAY
+ assert args["aws_conn_id"] == TEST_CONN_ID
+ assert args["region_name"] == TEST_REGION_NAME
+ assert args["verify"] is None
+ assert args["botocore_config"] is None
+
+ def test_serialize_generic_hook_params(self):
+ trigger = _build_trigger(verify=False,
botocore_config={"read_timeout": 99})
+ _, args = trigger.serialize()
+ assert args["verify"] is False
+ assert args["botocore_config"] == {"read_timeout": 99}
+
+ hook = trigger.hook
+ assert hook.client_type == "dynamodb"
+ assert hook.aws_conn_id == TEST_CONN_ID
+ assert hook._region_name == TEST_REGION_NAME
+ assert hook._verify is False
+ assert hook._config.read_timeout == 99
+
+ def test_key_with_and_without_sort_key(self):
+ trigger = _build_trigger()
+ assert trigger.key == {TEST_PK_NAME: {"S": TEST_PK_VALUE}}
+
+ trigger = _build_trigger(sort_key_name=TEST_SK_NAME,
sort_key_value=TEST_SK_VALUE)
+ assert trigger.key == {
+ TEST_PK_NAME: {"S": TEST_PK_VALUE},
+ TEST_SK_NAME: {"S": TEST_SK_VALUE},
+ }
+
+ @pytest.mark.asyncio
+
@mock.patch("airflow.providers.amazon.aws.hooks.base_aws.AwsBaseHook.get_async_conn")
+ async def test_run_success_when_value_matches(self, mock_async_conn):
+ client = AsyncMock()
+ mock_async_conn.return_value.__aenter__.return_value = client
+ client.get_item.return_value = {
+ "Item": {
+ TEST_PK_NAME: {"S": TEST_PK_VALUE},
+ TEST_ATTRIBUTE_NAME: {"S": TEST_ATTRIBUTE_VALUE},
+ }
+ }
+
+ trigger = _build_trigger()
+ generator = trigger.run()
+ response = await generator.asend(None)
+
+ client.get_item.assert_called_once_with(
+ TableName=TEST_TABLE_NAME, Key={TEST_PK_NAME: {"S": TEST_PK_VALUE}}
+ )
+ assert response == TriggerEvent({"status": "success"})
+
+ @pytest.mark.asyncio
+ @mock.patch("asyncio.sleep")
+
@mock.patch("airflow.providers.amazon.aws.hooks.base_aws.AwsBaseHook.get_async_conn")
+ async def test_run_keeps_polling_until_value_matches(self,
mock_async_conn, mock_sleep):
+ mock_sleep.return_value = True
+ client = AsyncMock()
+ mock_async_conn.return_value.__aenter__.return_value = client
+ client.get_item.side_effect = [
+ {}, # item does not exist yet
+ {"Item": {TEST_PK_NAME: {"S": TEST_PK_VALUE}, TEST_ATTRIBUTE_NAME:
{"S": "wrong"}}},
+ {"Item": {TEST_PK_NAME: {"S": TEST_PK_VALUE}, TEST_ATTRIBUTE_NAME:
{"S": TEST_ATTRIBUTE_VALUE}}},
+ ]
+
+ trigger = _build_trigger()
+ generator = trigger.run()
+ response = await generator.asend(None)
+
+ assert client.get_item.call_count == 3
+ assert response == TriggerEvent({"status": "success"})
+
+ @pytest.mark.asyncio
+ @mock.patch("asyncio.sleep")
+
@mock.patch("airflow.providers.amazon.aws.hooks.base_aws.AwsBaseHook.get_async_conn")
+ async def test_run_tolerates_client_error(self, mock_async_conn,
mock_sleep):
+ # Same tolerance as the sensor's poke: a ClientError means "not there
yet", not failure.
+ mock_sleep.return_value = True
+ client = AsyncMock()
+ mock_async_conn.return_value.__aenter__.return_value = client
+ client.get_item.side_effect = [
+ ClientError(
+ error_response={"Error": {"Code": "ResourceNotFoundException",
"Message": "no table"}},
+ operation_name="GetItem",
+ ),
+ {"Item": {TEST_PK_NAME: {"S": TEST_PK_VALUE}, TEST_ATTRIBUTE_NAME:
{"S": TEST_ATTRIBUTE_VALUE}}},
+ ]
+
+ trigger = _build_trigger()
+ generator = trigger.run()
+ response = await generator.asend(None)
+
+ assert client.get_item.call_count == 2
+ assert response == TriggerEvent({"status": "success"})
+
+ @pytest.mark.asyncio
+
@mock.patch("airflow.providers.amazon.aws.hooks.base_aws.AwsBaseHook.get_async_conn")
+ async def test_run_success_with_multiple_expected_values(self,
mock_async_conn):
+ client = AsyncMock()
+ mock_async_conn.return_value.__aenter__.return_value = client
+ client.get_item.return_value = {
+ "Item": {TEST_PK_NAME: {"S": TEST_PK_VALUE}, TEST_ATTRIBUTE_NAME:
{"S": "Bar2"}}
+ }
+
+ trigger = _build_trigger(attribute_value=["Bar1", "Bar2", "Bar3"])
+ generator = trigger.run()
+ response = await generator.asend(None)
+
+ assert response == TriggerEvent({"status": "success"})