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

shahar1 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 5bfc14fcfc2 Validate Google operator templated parameters after 
rendering (#70534)
5bfc14fcfc2 is described below

commit 5bfc14fcfc2020e393ef6c1ec7c62a159c6a62ba
Author: Dr Alex Mitre <[email protected]>
AuthorDate: Tue Sep 15 13:55:09 2026 -0600

    Validate Google operator templated parameters after rendering (#70534)
    
    CloudSpeechToTextRecognizeSpeechOperator, 
CloudTextToSpeechSynthesizeOperator,
    and CloudFirestoreExportDatabaseOperator validate template fields inside
    _validate_inputs() called from __init__, so the checks ran against
    un-rendered Jinja expressions and an empty rendered value was never
    caught. The validation now runs at execute time, and the raises are
    narrowed from AirflowException to ValueError per the ongoing exception
    clean-up.
---
 generated/known_airflow_exceptions.txt                  |  3 ---
 .../providers/google/cloud/operators/speech_to_text.py  |  7 +++----
 .../providers/google/cloud/operators/text_to_speech.py  |  5 ++---
 .../providers/google/firebase/operators/firestore.py    |  5 ++---
 .../unit/google/cloud/operators/test_speech_to_text.py  | 16 ++++++++++++++++
 .../unit/google/cloud/operators/test_text_to_speech.py  |  3 +--
 .../unit/google/firebase/operators/test_firestore.py    | 17 +++++++++++++++++
 7 files changed, 41 insertions(+), 15 deletions(-)

diff --git a/generated/known_airflow_exceptions.txt 
b/generated/known_airflow_exceptions.txt
index c60aa6a73bd..d1b06ce69e2 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -264,8 +264,6 @@ 
providers/google/src/airflow/providers/google/cloud/operators/looker.py::1
 
providers/google/src/airflow/providers/google/cloud/operators/managed_kafka.py::15
 providers/google/src/airflow/providers/google/cloud/operators/pubsub.py::1
 providers/google/src/airflow/providers/google/cloud/operators/spanner.py::19
-providers/google/src/airflow/providers/google/cloud/operators/speech_to_text.py::2
-providers/google/src/airflow/providers/google/cloud/operators/text_to_speech.py::1
 providers/google/src/airflow/providers/google/cloud/operators/translate.py::9
 
providers/google/src/airflow/providers/google/cloud/operators/translate_speech.py::2
 
providers/google/src/airflow/providers/google/cloud/operators/vertex_ai/batch_prediction_job.py::1
@@ -311,7 +309,6 @@ 
providers/google/src/airflow/providers/google/cloud/utils/credentials_provider.p
 providers/google/src/airflow/providers/google/common/hooks/base_google.py::7
 
providers/google/src/airflow/providers/google/common/hooks/operation_helpers.py::2
 providers/google/src/airflow/providers/google/firebase/hooks/firestore.py::1
-providers/google/src/airflow/providers/google/firebase/operators/firestore.py::1
 providers/google/src/airflow/providers/google/leveldb/hooks/leveldb.py::4
 
providers/google/src/airflow/providers/google/marketing_platform/hooks/campaign_manager.py::2
 
providers/google/src/airflow/providers/google/marketing_platform/hooks/search_ads.py::1
diff --git 
a/providers/google/src/airflow/providers/google/cloud/operators/speech_to_text.py
 
b/providers/google/src/airflow/providers/google/cloud/operators/speech_to_text.py
index 5424fba7191..3ac73c54abf 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/operators/speech_to_text.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/operators/speech_to_text.py
@@ -25,7 +25,6 @@ from typing import TYPE_CHECKING
 from google.api_core.gapic_v1.method import DEFAULT, _MethodDefault
 from google.protobuf.json_format import MessageToDict
 
-from airflow.providers.common.compat.sdk import AirflowException
 from airflow.providers.google.cloud.hooks.speech_to_text import 
CloudSpeechToTextHook, RecognitionAudio
 from airflow.providers.google.cloud.operators.cloud_base import 
GoogleCloudBaseOperator
 from airflow.providers.google.common.hooks.base_google import 
PROVIDE_PROJECT_ID
@@ -99,17 +98,17 @@ class 
CloudSpeechToTextRecognizeSpeechOperator(GoogleCloudBaseOperator):
         self.gcp_conn_id = gcp_conn_id
         self.retry = retry
         self.timeout = timeout
-        self._validate_inputs()
         self.impersonation_chain = impersonation_chain
         super().__init__(**kwargs)
 
     def _validate_inputs(self) -> None:
         if self.audio == "":
-            raise AirflowException("The required parameter 'audio' is empty")
+            raise ValueError("The required parameter 'audio' is empty")
         if self.config == "":
-            raise AirflowException("The required parameter 'config' is empty")
+            raise ValueError("The required parameter 'config' is empty")
 
     def execute(self, context: Context):
+        self._validate_inputs()
         hook = CloudSpeechToTextHook(
             gcp_conn_id=self.gcp_conn_id,
             impersonation_chain=self.impersonation_chain,
diff --git 
a/providers/google/src/airflow/providers/google/cloud/operators/text_to_speech.py
 
b/providers/google/src/airflow/providers/google/cloud/operators/text_to_speech.py
index 5deded3ef8b..bfd01409082 100644
--- 
a/providers/google/src/airflow/providers/google/cloud/operators/text_to_speech.py
+++ 
b/providers/google/src/airflow/providers/google/cloud/operators/text_to_speech.py
@@ -25,7 +25,6 @@ from typing import TYPE_CHECKING
 
 from google.api_core.gapic_v1.method import DEFAULT, _MethodDefault
 
-from airflow.providers.common.compat.sdk import AirflowException
 from airflow.providers.google.cloud.hooks.gcs import GCSHook
 from airflow.providers.google.cloud.hooks.text_to_speech import 
CloudTextToSpeechHook
 from airflow.providers.google.cloud.operators.cloud_base import 
GoogleCloudBaseOperator
@@ -112,7 +111,6 @@ class 
CloudTextToSpeechSynthesizeOperator(GoogleCloudBaseOperator):
         self.gcp_conn_id = gcp_conn_id
         self.retry = retry
         self.timeout = timeout
-        self._validate_inputs()
         self.impersonation_chain = impersonation_chain
         super().__init__(**kwargs)
 
@@ -125,9 +123,10 @@ class 
CloudTextToSpeechSynthesizeOperator(GoogleCloudBaseOperator):
             "target_filename",
         ]:
             if getattr(self, parameter) == "":
-                raise AirflowException(f"The required parameter '{parameter}' 
is empty")
+                raise ValueError(f"The required parameter '{parameter}' is 
empty")
 
     def execute(self, context: Context) -> None:
+        self._validate_inputs()
         hook = CloudTextToSpeechHook(
             gcp_conn_id=self.gcp_conn_id,
             impersonation_chain=self.impersonation_chain,
diff --git 
a/providers/google/src/airflow/providers/google/firebase/operators/firestore.py 
b/providers/google/src/airflow/providers/google/firebase/operators/firestore.py
index 0c35f572923..d39f4bf879b 100644
--- 
a/providers/google/src/airflow/providers/google/firebase/operators/firestore.py
+++ 
b/providers/google/src/airflow/providers/google/firebase/operators/firestore.py
@@ -19,7 +19,6 @@ from __future__ import annotations
 from collections.abc import Sequence
 from typing import TYPE_CHECKING
 
-from airflow.providers.common.compat.sdk import AirflowException
 from airflow.providers.google.common.hooks.base_google import 
PROVIDE_PROJECT_ID
 from airflow.providers.google.firebase.hooks.firestore import 
CloudFirestoreHook
 from airflow.providers.google.version_compat import BaseOperator
@@ -78,14 +77,14 @@ class CloudFirestoreExportDatabaseOperator(BaseOperator):
         self.project_id = project_id
         self.gcp_conn_id = gcp_conn_id
         self.api_version = api_version
-        self._validate_inputs()
         self.impersonation_chain = impersonation_chain
 
     def _validate_inputs(self) -> None:
         if not self.body:
-            raise AirflowException("The required parameter 'body' is missing")
+            raise ValueError("The required parameter 'body' is missing")
 
     def execute(self, context: Context):
+        self._validate_inputs()
         hook = CloudFirestoreHook(
             gcp_conn_id=self.gcp_conn_id,
             api_version=self.api_version,
diff --git 
a/providers/google/tests/unit/google/cloud/operators/test_speech_to_text.py 
b/providers/google/tests/unit/google/cloud/operators/test_speech_to_text.py
index 6e515a3d45d..c2e0ad737cd 100644
--- a/providers/google/tests/unit/google/cloud/operators/test_speech_to_text.py
+++ b/providers/google/tests/unit/google/cloud/operators/test_speech_to_text.py
@@ -55,6 +55,22 @@ class TestCloudSpeechToTextRecognizeSpeechOperator:
             config=CONFIG, audio=AUDIO, retry=DEFAULT, timeout=None
         )
 
+    
@patch("airflow.providers.google.cloud.operators.speech_to_text.CloudSpeechToTextHook")
+    def test_empty_audio_fails_at_execute_time(self, mock_hook):
+        op = CloudSpeechToTextRecognizeSpeechOperator(
+            project_id=PROJECT_ID,
+            gcp_conn_id=GCP_CONN_ID,
+            audio="{{ var.value.audio }}",
+            config=CONFIG,
+            task_id="id",
+        )
+        # Template rendering replaces the Jinja expression with the resolved 
value before execute.
+        op.audio = ""
+
+        with pytest.raises(ValueError, match="The required parameter 'audio' 
is empty"):
+            op.execute(context={"task_instance": Mock()})
+        mock_hook.assert_not_called()
+
     
@patch("airflow.providers.google.cloud.operators.speech_to_text.CloudSpeechToTextHook")
     def test_missing_config(self, mock_hook):
         mock_hook.return_value.recognize_speech.return_value = True
diff --git 
a/providers/google/tests/unit/google/cloud/operators/test_text_to_speech.py 
b/providers/google/tests/unit/google/cloud/operators/test_text_to_speech.py
index d4c2a597870..76c53c5c3f4 100644
--- a/providers/google/tests/unit/google/cloud/operators/test_text_to_speech.py
+++ b/providers/google/tests/unit/google/cloud/operators/test_text_to_speech.py
@@ -22,7 +22,6 @@ from unittest.mock import ANY, MagicMock, Mock, PropertyMock, 
patch
 import pytest
 from google.api_core.gapic_v1.method import DEFAULT
 
-from airflow.providers.common.compat.sdk import AirflowException
 from airflow.providers.google.cloud.operators.text_to_speech import 
CloudTextToSpeechSynthesizeOperator
 
 PROJECT_ID = "project-id"
@@ -98,7 +97,7 @@ class TestGcpTextToSpeech:
     ):
         mocked_context = Mock()
 
-        with pytest.raises(AirflowException) as ctx:
+        with pytest.raises(ValueError, match="is empty") as ctx:
             CloudTextToSpeechSynthesizeOperator(
                 project_id="project-id",
                 input_data=input_data,
diff --git 
a/providers/google/tests/unit/google/firebase/operators/test_firestore.py 
b/providers/google/tests/unit/google/firebase/operators/test_firestore.py
index 340058ef6eb..e9fd938b017 100644
--- a/providers/google/tests/unit/google/firebase/operators/test_firestore.py
+++ b/providers/google/tests/unit/google/firebase/operators/test_firestore.py
@@ -18,6 +18,8 @@ from __future__ import annotations
 
 from unittest import mock
 
+import pytest
+
 from airflow.providers.google.firebase.operators.firestore import 
CloudFirestoreExportDatabaseOperator
 
 TEST_OUTPUT_URI_PREFIX: str = "gs://example-bucket/path"
@@ -42,3 +44,18 @@ class TestCloudFirestoreExportDatabaseOperator:
         
mock_firestore_hook.return_value.export_documents.assert_called_once_with(
             body=EXPORT_DOCUMENT_BODY, database_id="(default)", 
project_id=TEST_PROJECT_ID
         )
+
+    
@mock.patch("airflow.providers.google.firebase.operators.firestore.CloudFirestoreHook")
+    def test_empty_body_fails_at_execute_time(self, mock_firestore_hook):
+        op = CloudFirestoreExportDatabaseOperator(
+            task_id="test-task",
+            body="{{ var.value.export_body }}",
+            gcp_conn_id="google_cloud_default",
+            project_id=TEST_PROJECT_ID,
+        )
+        # Template rendering replaces the Jinja expression with the resolved 
value before execute.
+        op.body = None
+
+        with pytest.raises(ValueError, match="The required parameter 'body' is 
missing"):
+            op.execute(mock.MagicMock())
+        mock_firestore_hook.return_value.export_documents.assert_not_called()

Reply via email to