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 a1c27c5d36c Prevent premature validation in Gemini batch job operators 
(#70362)
a1c27c5d36c is described below

commit a1c27c5d36c5429303461b4a71f373307535f8b6
Author: daniel.jin <[email protected]>
AuthorDate: Fri Jul 24 04:08:00 2026 -0400

    Prevent premature validation in Gemini batch job operators (#70362)
---
 .../providers/google/cloud/operators/gen_ai.py     |  39 ++--
 .../unit/google/cloud/operators/test_gen_ai.py     | 210 ++++++++++++---------
 .../ci/prek/validate_operators_init_exemptions.txt |   2 -
 3 files changed, 143 insertions(+), 108 deletions(-)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py 
b/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py
index 71a51c32419..0801bb24a75 100644
--- a/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py
+++ b/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py
@@ -475,15 +475,6 @@ class 
GenAIGeminiCreateBatchJobOperator(GoogleCloudBaseOperator):
         self.results_folder = results_folder
         self.deferrable = deferrable
 
-        if self.retrieve_result and not (self.wait_until_complete or 
self.deferrable):
-            raise AirflowException(
-                "Retrieving results is possible only if wait_until_complete 
set to True or in deferrable mode"
-            )
-        if self.results_folder and not isinstance(self.input_source, str):
-            raise AirflowException("results_folder works only when 
input_source is file name")
-        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
-            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
-
     def _wait_until_complete(self, job, polling_interval: int = 30):
         try:
             while True:
@@ -542,6 +533,17 @@ class 
GenAIGeminiCreateBatchJobOperator(GoogleCloudBaseOperator):
         )
 
     def execute(self, context: Context):
+        if self.retrieve_result and not (self.wait_until_complete or 
self.deferrable):
+            raise AirflowException(
+                "Retrieving results is possible only if wait_until_complete 
set to True or in deferrable mode"
+            )
+
+        if self.results_folder and not isinstance(self.input_source, str):
+            raise AirflowException("results_folder works only when 
input_source is file name")
+
+        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
+            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
+
         if self.deferrable:
             self.defer(
                 trigger=GenAIGeminiCreateBatchJobTrigger(
@@ -918,15 +920,6 @@ class 
GenAIGeminiCreateEmbeddingsBatchJobOperator(GoogleCloudBaseOperator):
         self.results_folder = results_folder
         self.deferrable = deferrable
 
-        if self.retrieve_result and not (self.wait_until_complete or 
self.deferrable):
-            raise AirflowException(
-                "Retrieving results is possible only if wait_until_complete 
set to True or in deferrable mode"
-            )
-        if self.results_folder and not isinstance(self.input_source, str):
-            raise AirflowException("results_folder works only when 
input_source is file name")
-        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
-            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
-
     def _wait_until_complete(self, job, polling_interval: int = 30):
         try:
             while True:
@@ -985,6 +978,16 @@ class 
GenAIGeminiCreateEmbeddingsBatchJobOperator(GoogleCloudBaseOperator):
         )
 
     def execute(self, context: Context):
+        if self.retrieve_result and not (self.wait_until_complete or 
self.deferrable):
+            raise AirflowException(
+                "Retrieving results is possible only if wait_until_complete 
set to True or in deferrable mode"
+            )
+
+        if self.results_folder and not isinstance(self.input_source, str):
+            raise AirflowException("results_folder works only when 
input_source is file name")
+
+        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
+            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
         if self.deferrable:
             self.defer(
                 trigger=GenAIGeminiCreateEmbeddingsBatchJobTrigger(
diff --git a/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py 
b/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py
index cbd2105ef82..3e814f9bd9a 100644
--- a/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py
+++ b/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py
@@ -395,50 +395,67 @@ class TestGenAIGeminiCreateBatchJobOperator:
         
mock_hook.return_value.get_batch_job.assert_called_once_with("test-name")
         mock_job.model_dump.assert_called_once_with(mode="json")
 
-    def 
test_init_retrieve_result_and_not_wait_until_complete_raises_airflow_exception(self):
-        with pytest.raises(AirflowException):
-            GenAIGeminiCreateBatchJobOperator(
-                task_id=TASK_ID,
-                project_id=GCP_PROJECT,
-                location=GCP_LOCATION,
-                model=TEST_GEMINI_MODEL,
-                gcp_conn_id=GCP_CONN_ID,
-                impersonation_chain=IMPERSONATION_CHAIN,
-                input_source=TEST_BATCH_JOB_INLINED_REQUESTS,
-                gemini_api_key=TEST_GEMINI_API_KEY,
-                wait_until_complete=False,
-                retrieve_result=True,
-            )
-
-    def test_init_input_source_not_string_raises_airflow_exception(self):
-        with pytest.raises(AirflowException):
-            GenAIGeminiCreateBatchJobOperator(
-                task_id=TASK_ID,
-                project_id=GCP_PROJECT,
-                location=GCP_LOCATION,
-                model=TEST_GEMINI_MODEL,
-                gcp_conn_id=GCP_CONN_ID,
-                impersonation_chain=IMPERSONATION_CHAIN,
-                input_source=TEST_BATCH_JOB_INLINED_REQUESTS,
-                gemini_api_key=TEST_GEMINI_API_KEY,
-                wait_until_complete=False,
-                results_folder=TEST_FILE_PATH,
-            )
-
-    def test_init_results_folder_not_exists_raises_airflow_exception(self):
-        with pytest.raises(AirflowException):
-            GenAIGeminiCreateBatchJobOperator(
-                task_id=TASK_ID,
-                project_id=GCP_PROJECT,
-                location=GCP_LOCATION,
-                model=TEST_GEMINI_MODEL,
-                gcp_conn_id=GCP_CONN_ID,
-                impersonation_chain=IMPERSONATION_CHAIN,
-                input_source=TEST_FILE_NAME,
-                gemini_api_key=TEST_GEMINI_API_KEY,
-                wait_until_complete=False,
-                results_folder=TEST_FILE_PATH,
-            )
+    def 
test_execute_retrieve_result_and_not_wait_until_complete_raises_airflow_exception(self):
+        op = GenAIGeminiCreateBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            model=TEST_GEMINI_MODEL,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            input_source=TEST_BATCH_JOB_INLINED_REQUESTS,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            wait_until_complete=False,
+            retrieve_result=True,
+        )
+
+        with pytest.raises(
+            AirflowException,
+            match=(
+                "Retrieving results is possible only if wait_until_complete 
set to True or in deferrable mode"
+            ),
+        ):
+            op.execute(context={"ti": mock.MagicMock()})
+
+    def test_execute_input_source_not_string_raises_airflow_exception(self):
+        op = GenAIGeminiCreateBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            model=TEST_GEMINI_MODEL,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            input_source=TEST_BATCH_JOB_INLINED_REQUESTS,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            wait_until_complete=False,
+            results_folder=TEST_FILE_PATH,
+        )
+
+        with pytest.raises(
+            AirflowException,
+            match="results_folder works only when input_source is file name",
+        ):
+            op.execute(context={"ti": mock.MagicMock()})
+
+    def test_execute_results_folder_not_exists_raises_airflow_exception(self):
+        op = GenAIGeminiCreateBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            model=TEST_GEMINI_MODEL,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            input_source=TEST_FILE_NAME,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            wait_until_complete=False,
+            results_folder=TEST_FILE_PATH,
+        )
+
+        with pytest.raises(
+            AirflowException,
+            match="path to results_folder does not exist, please provide 
correct path",
+        ):
+            op.execute(context={"ti": mock.MagicMock()})
 
     @mock.patch(GEN_AI_PATH.format("GenAIGeminiAPIHook"))
     def test__wait_until_complete_exception_raises_airflow_exception(self, 
mock_hook):
@@ -753,50 +770,67 @@ class TestGenAIGeminiCreateEmbeddingsBatchJobOperator:
             create_embeddings_config=None,
         )
 
-    def 
test_init_retrieve_result_and_not_wait_until_complete_raises_airflow_exception(self):
-        with pytest.raises(AirflowException):
-            GenAIGeminiCreateEmbeddingsBatchJobOperator(
-                task_id=TASK_ID,
-                project_id=GCP_PROJECT,
-                location=GCP_LOCATION,
-                input_source=TEST_EMBEDDINGS_JOB_INLINED_REQUESTS,
-                model=EMBEDDING_MODEL,
-                gemini_api_key=TEST_GEMINI_API_KEY,
-                gcp_conn_id=GCP_CONN_ID,
-                impersonation_chain=IMPERSONATION_CHAIN,
-                wait_until_complete=False,
-                retrieve_result=True,
-            )
-
-    def test_init_input_source_not_string_raises_airflow_exception(self):
-        with pytest.raises(AirflowException):
-            GenAIGeminiCreateEmbeddingsBatchJobOperator(
-                task_id=TASK_ID,
-                project_id=GCP_PROJECT,
-                location=GCP_LOCATION,
-                input_source=TEST_EMBEDDINGS_JOB_INLINED_REQUESTS,
-                model=EMBEDDING_MODEL,
-                gemini_api_key=TEST_GEMINI_API_KEY,
-                gcp_conn_id=GCP_CONN_ID,
-                impersonation_chain=IMPERSONATION_CHAIN,
-                wait_until_complete=False,
-                results_folder=TEST_FILE_PATH,
-            )
-
-    def test_init_results_folder_not_exists_raises_airflow_exception(self):
-        with pytest.raises(AirflowException):
-            GenAIGeminiCreateEmbeddingsBatchJobOperator(
-                task_id=TASK_ID,
-                project_id=GCP_PROJECT,
-                location=GCP_LOCATION,
-                input_source=TEST_FILE_NAME,
-                model=EMBEDDING_MODEL,
-                gemini_api_key=TEST_GEMINI_API_KEY,
-                gcp_conn_id=GCP_CONN_ID,
-                impersonation_chain=IMPERSONATION_CHAIN,
-                wait_until_complete=False,
-                results_folder=TEST_FILE_PATH,
-            )
+    def 
test_execute_retrieve_result_and_not_wait_until_complete_raises_airflow_exception(self):
+        op = GenAIGeminiCreateEmbeddingsBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            input_source=TEST_EMBEDDINGS_JOB_INLINED_REQUESTS,
+            model=EMBEDDING_MODEL,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            wait_until_complete=False,
+            retrieve_result=True,
+        )
+
+        with pytest.raises(
+            AirflowException,
+            match=(
+                "Retrieving results is possible only if wait_until_complete 
set to True or in deferrable mode"
+            ),
+        ):
+            op.execute(context={"ti": mock.MagicMock()})
+
+    def test_execute_input_source_not_string_raises_airflow_exception(self):
+        op = GenAIGeminiCreateEmbeddingsBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            input_source=TEST_EMBEDDINGS_JOB_INLINED_REQUESTS,
+            model=EMBEDDING_MODEL,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            wait_until_complete=False,
+            results_folder=TEST_FILE_PATH,
+        )
+
+        with pytest.raises(
+            AirflowException,
+            match="results_folder works only when input_source is file name",
+        ):
+            op.execute(context={"ti": mock.MagicMock()})
+
+    def test_execute_results_folder_not_exists_raises_airflow_exception(self):
+        op = GenAIGeminiCreateEmbeddingsBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            input_source=TEST_FILE_NAME,
+            model=EMBEDDING_MODEL,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            wait_until_complete=False,
+            results_folder=TEST_FILE_PATH,
+        )
+
+        with pytest.raises(
+            AirflowException,
+            match="path to results_folder does not exist, please provide 
correct path",
+        ):
+            op.execute(context={"ti": mock.MagicMock()})
 
     @mock.patch(GEN_AI_PATH.format("GenAIGeminiAPIHook"))
     def test__wait_until_complete_exception_raises_airflow_exception(self, 
mock_hook):
diff --git a/scripts/ci/prek/validate_operators_init_exemptions.txt 
b/scripts/ci/prek/validate_operators_init_exemptions.txt
index 052da40bfaa..2f3a91c6b04 100644
--- a/scripts/ci/prek/validate_operators_init_exemptions.txt
+++ b/scripts/ci/prek/validate_operators_init_exemptions.txt
@@ -53,8 +53,6 @@ 
providers/google/src/airflow/providers/google/cloud/operators/functions.py::Clou
 
providers/google/src/airflow/providers/google/cloud/operators/gcs.py::GCSDeleteObjectsOperator
 
providers/google/src/airflow/providers/google/cloud/operators/gcs.py::GCSFileTransformOperator
 
providers/google/src/airflow/providers/google/cloud/operators/gcs.py::GCSListObjectsOperator
-providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py::GenAIGeminiCreateBatchJobOperator
-providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py::GenAIGeminiCreateEmbeddingsBatchJobOperator
 
providers/google/src/airflow/providers/google/cloud/sensors/bigquery_dts.py::BigQueryDataTransferServiceTransferRunSensor
 
providers/google/src/airflow/providers/google/cloud/sensors/cloud_composer.py::CloudComposerExternalTaskSensor
 
providers/google/src/airflow/providers/google/cloud/transfers/azure_fileshare_to_gcs.py::AzureFileShareToGCSOperator

Reply via email to