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 24f33707582 Add query and report inspection operators for Google Bid 
Manager (#71947)
24f33707582 is described below

commit 24f3370758227ce1bb13e5df251d26a9271ceeee
Author: Gbenga <[email protected]>
AuthorDate: Thu Oct 29 07:44:29 2026 +0100

    Add query and report inspection operators for Google Bid Manager (#71947)
---
 generated/known_airflow_exceptions.txt             |   2 +-
 .../operators/marketing_platform/bid_manager.rst   |  76 ++++++-
 .../google/marketing_platform/hooks/bid_manager.py |  21 +-
 .../marketing_platform/operators/bid_manager.py    | 237 ++++++++++++++++++++-
 .../marketing_platform/sensors/bid_manager.py      |   6 +-
 .../marketing_platform/example_bid_manager.py      |  37 +++-
 .../marketing_platform/hooks/test_bid_manager.py   |  37 +++-
 .../operators/test_bid_manager.py                  | 135 +++++++++++-
 .../marketing_platform/sensors/test_bid_manager.py |  23 +-
 9 files changed, 544 insertions(+), 30 deletions(-)

diff --git a/generated/known_airflow_exceptions.txt 
b/generated/known_airflow_exceptions.txt
index ecf8a7634ce..1484c68ce3f 100644
--- a/generated/known_airflow_exceptions.txt
+++ b/generated/known_airflow_exceptions.txt
@@ -310,7 +310,7 @@ 
providers/google/src/airflow/providers/google/firebase/hooks/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
-providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py::3
+providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py::2
 
providers/google/src/airflow/providers/google/marketing_platform/sensors/display_video.py::1
 providers/google/src/airflow/providers/google/suite/hooks/calendar.py::1
 providers/google/src/airflow/providers/google/suite/hooks/sheets.py::1
diff --git a/providers/google/docs/operators/marketing_platform/bid_manager.rst 
b/providers/google/docs/operators/marketing_platform/bid_manager.rst
index c29e2687ed5..ce45177368f 100644
--- a/providers/google/docs/operators/marketing_platform/bid_manager.rst
+++ b/providers/google/docs/operators/marketing_platform/bid_manager.rst
@@ -45,6 +45,42 @@ parameters which allow you to dynamically determine values. 
You can provide body
 as this operator supports this template extension.
 The result is saved to :ref:`XCom <concepts:xcom>`, which allows the result to 
be used by other operators.
 
+.. _howto/operator:GoogleBidManagerGetQueryOperator:
+
+Getting a Query
+^^^^^^^^^^^^^^^
+
+To retrieve a query using Bid Manager, use
+:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerGetQueryOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/marketing_platform/example_bid_manager.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_google_bid_manager_get_query_operator]
+    :end-before: [END howto_google_bid_manager_get_query_operator]
+
+Use :ref:`Jinja templating <concepts:jinja-templating>` with
+:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerGetQueryOperator`
+parameters which allow you to dynamically determine values.
+
+.. _howto/operator:GoogleBidManagerListQueriesOperator:
+
+Listing Queries
+^^^^^^^^^^^^^^^
+
+To list queries using Bid Manager, use
+:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerListQueriesOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/marketing_platform/example_bid_manager.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_google_bid_manager_list_queries_operator]
+    :end-before: [END howto_google_bid_manager_list_queries_operator]
+
+Use :ref:`Jinja templating <concepts:jinja-templating>` with
+:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerListQueriesOperator`
+parameters which allow you to dynamically determine values.
+
 .. _howto/operator:GoogleBidManagerRunQueryOperator:
 
 Run Query
@@ -64,6 +100,42 @@ You can use :ref:`Jinja templating 
<concepts:jinja-templating>` with
 parameters which allow you to dynamically determine values.
 The result is saved to :ref:`XCom <concepts:xcom>`, which allows the result to 
be used by other operators.
 
+.. _howto/operator:GoogleBidManagerGetReportOperator:
+
+Getting a Report
+^^^^^^^^^^^^^^^^
+
+To retrieve a report using Bid Manager, use
+:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerGetReportOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/marketing_platform/example_bid_manager.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_google_bid_manager_get_report_operator]
+    :end-before: [END howto_google_bid_manager_get_report_operator]
+
+Use :ref:`Jinja templating <concepts:jinja-templating>` with
+:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerGetReportOperator`
+parameters which allow you to dynamically determine values.
+
+.. _howto/operator:GoogleBidManagerListReportsOperator:
+
+Listing Reports
+^^^^^^^^^^^^^^^
+
+To list reports for a query using Bid Manager, use
+:class:`~airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerListReportsOperator`.
+
+.. exampleinclude:: 
/../../google/tests/system/google/marketing_platform/example_bid_manager.py
+    :language: python
+    :dedent: 4
+    :start-after: [START howto_google_bid_manager_list_reports_operator]
+    :end-before: [END howto_google_bid_manager_list_reports_operator]
+
+Use :ref:`Jinja templating <concepts:jinja-templating>` with
+:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerListReportsOperator`
+parameters which allow you to dynamically determine values.
+
 .. _howto/operator:GoogleBidManagerDeleteQueryOperator:
 
 Deleting a Query
@@ -111,8 +183,8 @@ To download a report to GCS bucket use
 .. exampleinclude:: 
/../../google/tests/system/google/marketing_platform/example_bid_manager.py
     :language: python
     :dedent: 4
-    :start-after: [START howto_google_bid_manager_get_report_operator]
-    :end-before: [END howto_google_bid_manager_get_report_operator]
+    :start-after: [START howto_google_bid_manager_download_report_operator]
+    :end-before: [END howto_google_bid_manager_download_report_operator]
 
 Use :ref:`Jinja templating <concepts:jinja-templating>` with
 
:template-fields:`airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerDownloadReportOperator`
diff --git 
a/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py
 
b/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py
index a3d262ab7fb..4d86e67b561 100644
--- 
a/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py
+++ 
b/providers/google/src/airflow/providers/google/marketing_platform/hooks/bid_manager.py
@@ -87,8 +87,14 @@ class GoogleBidManagerHook(GoogleBaseHook):
 
     def list_queries(self) -> list[dict]:
         """Retrieve stored queries."""
-        response = 
self.get_conn().queries().list().execute(num_retries=self.num_retries)
-        return response.get("queries", [])
+        conn = self.get_conn()
+        request = conn.queries().list()
+        queries: list[dict] = []
+        while request is not None:
+            response = request.execute(num_retries=self.num_retries)
+            queries.extend(response.get("queries", []))
+            request = conn.queries().list_next(previous_request=request, 
previous_response=response)
+        return queries
 
     def run_query(self, query_id: str, params: dict[str, Any] | None) -> dict:
         """
@@ -122,6 +128,11 @@ class GoogleBidManagerHook(GoogleBaseHook):
 
         :param query_id: Query ID for which report was generated.
         """
-        return (
-            
self.get_conn().queries().reports().list(queryId=query_id).execute(num_retries=self.num_retries)
-        )
+        conn = self.get_conn()
+        request = conn.queries().reports().list(queryId=query_id)
+        reports: list[dict] = []
+        while request is not None:
+            response = request.execute(num_retries=self.num_retries)
+            reports.extend(response.get("reports", []))
+            request = 
conn.queries().reports().list_next(previous_request=request, 
previous_response=response)
+        return {"reports": reports}
diff --git 
a/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py
 
b/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py
index ea00d7ffc7d..71096c65c33 100644
--- 
a/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py
+++ 
b/providers/google/src/airflow/providers/google/marketing_platform/operators/bid_manager.py
@@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, Any
 from urllib.parse import urlparse, urlsplit
 
 from airflow.exceptions import AirflowException
+from airflow.providers.common.compat.sdk import AirflowFailException
 from airflow.providers.google.cloud.hooks.gcs import GCSHook
 from airflow.providers.google.marketing_platform.hooks.bid_manager import 
GoogleBidManagerHook
 from airflow.providers.google.version_compat import BaseOperator
@@ -105,6 +106,114 @@ class GoogleBidManagerCreateQueryOperator(BaseOperator):
         return response
 
 
+class GoogleBidManagerGetQueryOperator(BaseOperator):
+    """
+    Retrieves a stored query.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the 
guide:
+        :ref:`howto/operator:GoogleBidManagerGetQueryOperator`
+
+    .. seealso::
+        Check also the official API docs:
+        `https://developers.google.com/bid-manager/v2/queries/get`
+
+    :param query_id: Query ID to retrieve.
+    :param api_version: The version of the api that will be requested for 
example 'v2'.
+    :param gcp_conn_id: The connection ID to use when fetching connection info.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term
+        credentials, or chained list of accounts required to get the 
access_token
+        of the last account in the list, which will be impersonated in the 
request.
+        If set as a string, the account must grant the originating account
+        the Service Account Token Creator IAM role.
+        If set as a sequence, the identities from the list must grant
+        Service Account Token Creator IAM role to the directly preceding 
identity, with first
+        account from the list granting this role to the originating account 
(templated).
+    """
+
+    template_fields: Sequence[str] = (
+        "query_id",
+        "impersonation_chain",
+        "gcp_conn_id",
+    )
+
+    def __init__(
+        self,
+        *,
+        query_id: str,
+        api_version: str = "v2",
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.query_id = query_id
+        self.api_version = api_version
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    def execute(self, context: Context) -> dict:
+        hook = GoogleBidManagerHook(
+            gcp_conn_id=self.gcp_conn_id,
+            api_version=self.api_version,
+            impersonation_chain=self.impersonation_chain,
+        )
+        self.log.info("Retrieving Bid Manager API query with id: %s", 
self.query_id)
+        return hook.get_query(query_id=self.query_id)
+
+
+class GoogleBidManagerListQueriesOperator(BaseOperator):
+    """
+    Retrieves stored queries.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the 
guide:
+        :ref:`howto/operator:GoogleBidManagerListQueriesOperator`
+
+    .. seealso::
+        Check also the official API docs:
+        `https://developers.google.com/bid-manager/v2/queries/list`
+
+    :param api_version: The version of the api that will be requested for 
example 'v2'.
+    :param gcp_conn_id: The connection ID to use when fetching connection info.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term
+        credentials, or chained list of accounts required to get the 
access_token
+        of the last account in the list, which will be impersonated in the 
request.
+        If set as a string, the account must grant the originating account
+        the Service Account Token Creator IAM role.
+        If set as a sequence, the identities from the list must grant
+        Service Account Token Creator IAM role to the directly preceding 
identity, with first
+        account from the list granting this role to the originating account 
(templated).
+    """
+
+    template_fields: Sequence[str] = (
+        "impersonation_chain",
+        "gcp_conn_id",
+    )
+
+    def __init__(
+        self,
+        *,
+        api_version: str = "v2",
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.api_version = api_version
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    def execute(self, context: Context) -> list[dict]:
+        hook = GoogleBidManagerHook(
+            gcp_conn_id=self.gcp_conn_id,
+            api_version=self.api_version,
+            impersonation_chain=self.impersonation_chain,
+        )
+        self.log.info("Retrieving list of Bid Manager API queries.")
+        return hook.list_queries()
+
+
 class GoogleBidManagerRunQueryOperator(BaseOperator):
     """
     Runs a stored query to generate a report.
@@ -230,6 +339,126 @@ class GoogleBidManagerDeleteQueryOperator(BaseOperator):
         self.log.info("Report deleted.")
 
 
+class GoogleBidManagerGetReportOperator(BaseOperator):
+    """
+    Retrieves details of a specific report.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the 
guide:
+        :ref:`howto/operator:GoogleBidManagerGetReportOperator`
+
+    .. seealso::
+        Check also the official API docs:
+        `https://developers.google.com/bid-manager/v2/queries.reports/get`
+
+    :param query_id: Query ID for which report was generated.
+    :param report_id: Report ID to retrieve.
+    :param api_version: The version of the api that will be requested for 
example 'v2'.
+    :param gcp_conn_id: The connection ID to use when fetching connection info.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term
+        credentials, or chained list of accounts required to get the 
access_token
+        of the last account in the list, which will be impersonated in the 
request.
+        If set as a string, the account must grant the originating account
+        the Service Account Token Creator IAM role.
+        If set as a sequence, the identities from the list must grant
+        Service Account Token Creator IAM role to the directly preceding 
identity, with first
+        account from the list granting this role to the originating account 
(templated).
+    """
+
+    template_fields: Sequence[str] = (
+        "query_id",
+        "report_id",
+        "impersonation_chain",
+        "gcp_conn_id",
+    )
+
+    def __init__(
+        self,
+        *,
+        query_id: str,
+        report_id: str,
+        api_version: str = "v2",
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.query_id = query_id
+        self.report_id = report_id
+        self.api_version = api_version
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    def execute(self, context: Context) -> dict:
+        hook = GoogleBidManagerHook(
+            gcp_conn_id=self.gcp_conn_id,
+            api_version=self.api_version,
+            impersonation_chain=self.impersonation_chain,
+        )
+        self.log.info(
+            "Retrieving Bid Manager API report with query_id: %s and 
report_id: %s",
+            self.query_id,
+            self.report_id,
+        )
+        return hook.get_report(query_id=self.query_id, 
report_id=self.report_id)
+
+
+class GoogleBidManagerListReportsOperator(BaseOperator):
+    """
+    Retrieves a list of reports for a specific query.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the 
guide:
+        :ref:`howto/operator:GoogleBidManagerListReportsOperator`
+
+    .. seealso::
+        Check also the official API docs:
+        `https://developers.google.com/bid-manager/v2/queries.reports/list`
+
+    :param query_id: Query ID for which reports were generated.
+    :param api_version: The version of the api that will be requested for 
example 'v2'.
+    :param gcp_conn_id: The connection ID to use when fetching connection info.
+    :param impersonation_chain: Optional service account to impersonate using 
short-term
+        credentials, or chained list of accounts required to get the 
access_token
+        of the last account in the list, which will be impersonated in the 
request.
+        If set as a string, the account must grant the originating account
+        the Service Account Token Creator IAM role.
+        If set as a sequence, the identities from the list must grant
+        Service Account Token Creator IAM role to the directly preceding 
identity, with first
+        account from the list granting this role to the originating account 
(templated).
+    """
+
+    template_fields: Sequence[str] = (
+        "query_id",
+        "impersonation_chain",
+        "gcp_conn_id",
+    )
+
+    def __init__(
+        self,
+        *,
+        query_id: str,
+        api_version: str = "v2",
+        gcp_conn_id: str = "google_cloud_default",
+        impersonation_chain: str | Sequence[str] | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.query_id = query_id
+        self.api_version = api_version
+        self.gcp_conn_id = gcp_conn_id
+        self.impersonation_chain = impersonation_chain
+
+    def execute(self, context: Context) -> dict:
+        hook = GoogleBidManagerHook(
+            gcp_conn_id=self.gcp_conn_id,
+            api_version=self.api_version,
+            impersonation_chain=self.impersonation_chain,
+        )
+        self.log.info("Retrieving list of Bid Manager API reports for 
query_id: %s", self.query_id)
+        return hook.list_reports(query_id=self.query_id)
+
+
 class GoogleBidManagerDownloadReportOperator(BaseOperator):
     """
     Retrieves a stored query.
@@ -317,8 +546,12 @@ class GoogleBidManagerDownloadReportOperator(BaseOperator):
 
         resource = hook.get_report(query_id=self.query_id, 
report_id=self.report_id)
         status = resource.get("metadata", {}).get("status", {}).get("state")
-        if resource and status not in ["DONE", "FAILED"]:
-            raise AirflowException(f"Report {self.report_id} for query 
{self.query_id} is still running")
+        if status == "FAILED":
+            raise AirflowFailException(f"Report {self.report_id} for query 
{self.query_id} failed execution.")
+        if status != "DONE":
+            raise ValueError(
+                f"Report {self.report_id} for query {self.query_id} is in 
state '{status}'. Expected 'DONE'."
+            )
 
         # If no custom report_name provided, use Bid Manager name
         file_url = resource["metadata"]["googleCloudStoragePath"]
diff --git 
a/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py
 
b/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py
index ca4c10ab2c4..d4d016cd550 100644
--- 
a/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py
+++ 
b/providers/google/src/airflow/providers/google/marketing_platform/sensors/bid_manager.py
@@ -21,7 +21,7 @@ from __future__ import annotations
 from collections.abc import Sequence
 from typing import TYPE_CHECKING
 
-from airflow.providers.common.compat.sdk import BaseSensorOperator
+from airflow.providers.common.compat.sdk import AirflowFailException, 
BaseSensorOperator
 from airflow.providers.google.marketing_platform.hooks.bid_manager import 
GoogleBidManagerHook
 
 if TYPE_CHECKING:
@@ -84,6 +84,8 @@ class GoogleBidManagerRunQuerySensor(BaseSensorOperator):
         response = hook.get_report(query_id=self.query_id, 
report_id=self.report_id)
         status = response.get("metadata", {}).get("status", {}).get("state")
         self.log.info("STATUS OF THE REPORT %s FOR QUERY %s: %s", 
self.report_id, self.query_id, status)
-        if response and status in ["DONE", "FAILED"]:
+        if status == "FAILED":
+            raise AirflowFailException(f"Report {self.report_id} for query 
{self.query_id} failed execution.")
+        if status == "DONE":
             return True
         return False
diff --git 
a/providers/google/tests/system/google/marketing_platform/example_bid_manager.py
 
b/providers/google/tests/system/google/marketing_platform/example_bid_manager.py
index 2c08a035581..07bc31f0516 100644
--- 
a/providers/google/tests/system/google/marketing_platform/example_bid_manager.py
+++ 
b/providers/google/tests/system/google/marketing_platform/example_bid_manager.py
@@ -33,6 +33,10 @@ from 
airflow.providers.google.marketing_platform.operators.bid_manager import (
     GoogleBidManagerCreateQueryOperator,
     GoogleBidManagerDeleteQueryOperator,
     GoogleBidManagerDownloadReportOperator,
+    GoogleBidManagerGetQueryOperator,
+    GoogleBidManagerGetReportOperator,
+    GoogleBidManagerListQueriesOperator,
+    GoogleBidManagerListReportsOperator,
     GoogleBidManagerRunQueryOperator,
 )
 from airflow.providers.google.marketing_platform.sensors.bid_manager import (
@@ -147,6 +151,14 @@ with DAG(
     query_id = cast("str", XComArg(create_query, key="query_id"))
     # [END howto_google_bid_manager_create_query_operator]
 
+    # [START howto_google_bid_manager_get_query_operator]
+    get_query = GoogleBidManagerGetQueryOperator(query_id=query_id, 
task_id="get_query", gcp_conn_id=CONN_ID)
+    # [END howto_google_bid_manager_get_query_operator]
+
+    # [START howto_google_bid_manager_list_queries_operator]
+    list_queries = GoogleBidManagerListQueriesOperator(task_id="list_queries", 
gcp_conn_id=CONN_ID)
+    # [END howto_google_bid_manager_list_queries_operator]
+
     # [START howto_google_bid_manager_run_query_report_operator]
     run_query = GoogleBidManagerRunQueryOperator(
         query_id=query_id, parameters=PARAMETERS, task_id="run_report", 
gcp_conn_id=CONN_ID
@@ -166,14 +178,32 @@ with DAG(
     # [END howto_google_bid_manager_wait_run_query_sensor]
 
     # [START howto_google_bid_manager_get_report_operator]
-    get_report = GoogleBidManagerDownloadReportOperator(
+    get_report = GoogleBidManagerGetReportOperator(
         query_id=query_id,
         report_id=report_id,
         task_id="get_report",
+        gcp_conn_id=CONN_ID,
+    )
+    # [END howto_google_bid_manager_get_report_operator]
+
+    # [START howto_google_bid_manager_list_reports_operator]
+    list_reports = GoogleBidManagerListReportsOperator(
+        query_id=query_id,
+        task_id="list_reports",
+        gcp_conn_id=CONN_ID,
+    )
+    # [END howto_google_bid_manager_list_reports_operator]
+
+    # [START howto_google_bid_manager_download_report_operator]
+    download_report = GoogleBidManagerDownloadReportOperator(
+        query_id=query_id,
+        report_id=report_id,
+        task_id="download_report",
         bucket_name=BUCKET_NAME,
         report_name="test1.csv",
         gcp_conn_id=CONN_ID,
     )
+    # [END howto_google_bid_manager_download_report_operator]
 
     delete_bucket = GCSDeleteBucketOperator(
         task_id="delete_bucket",
@@ -181,7 +211,6 @@ with DAG(
         gcp_conn_id=CONN_ID,
         trigger_rule=TriggerRule.ALL_DONE,
     )
-    # [END howto_google_bid_manager_get_report_operator]
 
     # [START howto_google_bid_manager_delete_query_operator]
     delete_query = GoogleBidManagerDeleteQueryOperator(
@@ -194,9 +223,13 @@ with DAG(
         >> create_connection_display_video_task  # type: ignore
         >> create_bucket
         >> create_query
+        >> get_query
+        >> list_queries
         >> run_query
         >> wait_for_query
         >> get_report
+        >> list_reports
+        >> download_report
         >> delete_query
         >> delete_bucket
         >> delete_connection_task
diff --git 
a/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py
 
b/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py
index 5f460e0ce7e..38fc3ae94e3 100644
--- 
a/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py
+++ 
b/providers/google/tests/unit/google/marketing_platform/hooks/test_bid_manager.py
@@ -19,6 +19,8 @@ from __future__ import annotations
 
 from unittest import mock
 
+from googleapiclient.http import HttpRequest
+
 from airflow.providers.google.marketing_platform.hooks.bid_manager import 
GoogleBidManagerHook
 
 from unit.google.cloud.utils.base_gcp_mock import 
mock_base_gcp_hook_default_project_id
@@ -93,14 +95,20 @@ class TestGoogleBidManagerHook:
 
     
@mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn")
     def test_list_queries(self, get_conn_mock):
-        queries = ["test"]
-        return_value = {"queries": queries}
-        
get_conn_mock.return_value.queries.return_value.list.return_value.execute.return_value
 = return_value
+        queries_page1 = [{"queryId": "q1"}]
+        queries_page2 = [{"queryId": "q2"}]
+        mock_request = mock.MagicMock(spec=HttpRequest)
+        mock_next_request = mock.MagicMock(spec=HttpRequest)
+        get_conn_mock.return_value.queries.return_value.list.return_value = 
mock_request
+        mock_request.execute.return_value = {"queries": queries_page1}
+        mock_next_request.execute.return_value = {"queries": queries_page2}
+        get_conn_mock.return_value.queries.return_value.list_next.side_effect 
= [mock_next_request, None]
+
         result = self.hook.list_queries()
 
         
get_conn_mock.return_value.queries.return_value.list.assert_called_once_with()
-
-        assert queries == result
+        assert 
get_conn_mock.return_value.queries.return_value.list_next.call_count == 2
+        assert result == queries_page1 + queries_page2
 
     
@mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn")
     def test_run_query(self, get_conn_mock):
@@ -135,15 +143,22 @@ class TestGoogleBidManagerHook:
     
@mock.patch("airflow.providers.google.marketing_platform.hooks.bid_manager.GoogleBidManagerHook.get_conn")
     def test_list_reports(self, get_conn_mock):
         query_id = "QUERY_ID"
-        reports = ["report1", "report2"]
-        return_value = {"reports": reports}
-        (
-            
get_conn_mock.return_value.queries.return_value.reports.return_value.list.return_value.execute.return_value
-        ) = return_value
+        reports_page1 = [{"key": {"reportId": "r1"}}]
+        reports_page2 = [{"key": {"reportId": "r2"}}]
+        mock_request = mock.MagicMock(spec=HttpRequest)
+        mock_next_request = mock.MagicMock(spec=HttpRequest)
+        
get_conn_mock.return_value.queries.return_value.reports.return_value.list.return_value
 = mock_request
+        mock_request.execute.return_value = {"reports": reports_page1}
+        mock_next_request.execute.return_value = {"reports": reports_page2}
+        
get_conn_mock.return_value.queries.return_value.reports.return_value.list_next.side_effect
 = [
+            mock_next_request,
+            None,
+        ]
 
         result = self.hook.list_reports(query_id=query_id)
 
         
get_conn_mock.return_value.queries.return_value.reports.return_value.list.assert_called_once_with(
             queryId=query_id
         )
-        assert reports == result["reports"]
+        assert 
get_conn_mock.return_value.queries.return_value.reports.return_value.list_next.call_count
 == 2
+        assert result == {"reports": reports_page1 + reports_page2}
diff --git 
a/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py
 
b/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py
index 1f7b2928f7d..de9ac36c7ca 100644
--- 
a/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py
+++ 
b/providers/google/tests/unit/google/marketing_platform/operators/test_bid_manager.py
@@ -26,11 +26,15 @@ from sqlalchemy import delete
 
 from airflow.exceptions import AirflowException
 from airflow.models import TaskInstance as TI
-from airflow.providers.common.compat.sdk import timezone
+from airflow.providers.common.compat.sdk import AirflowFailException, timezone
 from airflow.providers.google.marketing_platform.operators.bid_manager import (
     GoogleBidManagerCreateQueryOperator,
     GoogleBidManagerDeleteQueryOperator,
     GoogleBidManagerDownloadReportOperator,
+    GoogleBidManagerGetQueryOperator,
+    GoogleBidManagerGetReportOperator,
+    GoogleBidManagerListQueriesOperator,
+    GoogleBidManagerListReportsOperator,
     GoogleBidManagerRunQueryOperator,
 )
 from airflow.utils.session import create_session
@@ -426,6 +430,31 @@ class TestGoogleBidManagerDownloadReportOperator:
             object_name=REPORT_NAME + ".gz",
         )
 
+    @pytest.mark.parametrize(
+        ("state", "expected_exception", "match"),
+        [
+            ("FAILED", AirflowFailException, "failed execution"),
+            ("RUNNING", ValueError, "is in state 'RUNNING'. Expected 'DONE'"),
+        ],
+    )
+    
@mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook")
+    def test_execute_non_done_status_raises(self, mock_hook, state, 
expected_exception, match):
+        mock_hook.return_value.get_report.return_value = {
+            "metadata": {
+                "status": {"state": state},
+            }
+        }
+        mock_context = {"task_instance": mock.Mock()}
+        op = GoogleBidManagerDownloadReportOperator(
+            query_id=QUERY_ID,
+            report_id=REPORT_ID,
+            bucket_name=BUCKET_NAME,
+            report_name=REPORT_NAME,
+            task_id="test_task",
+        )
+        with pytest.raises(expected_exception, match=match):
+            op.execute(context=mock_context)
+
 
 class TestGoogleBidManagerRunQueryOperator:
     
@mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook")
@@ -483,8 +512,110 @@ class TestGoogleBidManagerCreateQueryOperator:
         with NamedTemporaryFile("w+", suffix=".json") as f:
             f.write(json.dumps(body))
             f.flush()
-            op = GoogleBidManagerCreateQueryOperator(body=body, 
task_id="test_task")
+            op = GoogleBidManagerCreateQueryOperator(
+                body=f.name,
+                api_version=API_VERSION,
+                gcp_conn_id=GCP_CONN_ID,
+                impersonation_chain=IMPERSONATION_CHAIN,
+                task_id="test_task",
+            )
             op.prepare_template()
 
             assert isinstance(op.body, dict)
             assert op.body == body
+
+
+class TestGoogleBidManagerGetQueryOperator:
+    
@mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook")
+    def test_execute(self, hook_mock):
+        return_value = {"queryId": QUERY_ID}
+        hook_mock.return_value.get_query.return_value = return_value
+
+        op = GoogleBidManagerGetQueryOperator(
+            query_id=QUERY_ID,
+            api_version=API_VERSION,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            task_id="test_task",
+        )
+        result = op.execute(context=None)
+
+        hook_mock.assert_called_once_with(
+            gcp_conn_id=GCP_CONN_ID,
+            api_version=API_VERSION,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+        
hook_mock.return_value.get_query.assert_called_once_with(query_id=QUERY_ID)
+        assert result == return_value
+
+
+class TestGoogleBidManagerListQueriesOperator:
+    
@mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook")
+    def test_execute(self, hook_mock):
+        return_value = [{"queryId": QUERY_ID}]
+        hook_mock.return_value.list_queries.return_value = return_value
+
+        op = GoogleBidManagerListQueriesOperator(
+            api_version=API_VERSION,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            task_id="test_task",
+        )
+        result = op.execute(context=None)
+
+        hook_mock.assert_called_once_with(
+            gcp_conn_id=GCP_CONN_ID,
+            api_version=API_VERSION,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+        hook_mock.return_value.list_queries.assert_called_once_with()
+        assert result == return_value
+
+
+class TestGoogleBidManagerGetReportOperator:
+    
@mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook")
+    def test_execute(self, hook_mock):
+        return_value = {"key": {"queryId": QUERY_ID, "reportId": REPORT_ID}}
+        hook_mock.return_value.get_report.return_value = return_value
+
+        op = GoogleBidManagerGetReportOperator(
+            query_id=QUERY_ID,
+            report_id=REPORT_ID,
+            api_version=API_VERSION,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            task_id="test_task",
+        )
+        result = op.execute(context=None)
+
+        hook_mock.assert_called_once_with(
+            gcp_conn_id=GCP_CONN_ID,
+            api_version=API_VERSION,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+        
hook_mock.return_value.get_report.assert_called_once_with(query_id=QUERY_ID, 
report_id=REPORT_ID)
+        assert result == return_value
+
+
+class TestGoogleBidManagerListReportsOperator:
+    
@mock.patch("airflow.providers.google.marketing_platform.operators.bid_manager.GoogleBidManagerHook")
+    def test_execute(self, hook_mock):
+        return_value = {"reports": [{"key": {"queryId": QUERY_ID, "reportId": 
REPORT_ID}}]}
+        hook_mock.return_value.list_reports.return_value = return_value
+
+        op = GoogleBidManagerListReportsOperator(
+            query_id=QUERY_ID,
+            api_version=API_VERSION,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            task_id="test_task",
+        )
+        result = op.execute(context=None)
+
+        hook_mock.assert_called_once_with(
+            gcp_conn_id=GCP_CONN_ID,
+            api_version=API_VERSION,
+            impersonation_chain=IMPERSONATION_CHAIN,
+        )
+        
hook_mock.return_value.list_reports.assert_called_once_with(query_id=QUERY_ID)
+        assert result == return_value
diff --git 
a/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py
 
b/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py
index f48810e60de..aac1c34901e 100644
--- 
a/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py
+++ 
b/providers/google/tests/unit/google/marketing_platform/sensors/test_bid_manager.py
@@ -19,6 +19,9 @@ from __future__ import annotations
 
 from unittest import mock
 
+import pytest
+
+from airflow.providers.common.compat.sdk import AirflowFailException
 from airflow.providers.google.marketing_platform.sensors.bid_manager import (
     GoogleBidManagerRunQuerySensor,
 )
@@ -29,13 +32,27 @@ GCP_CONN_ID = "google_cloud_default"
 
 
 class TestGoogleBidManagerRunQuerySensor:
+    @pytest.mark.parametrize(
+        ("state", "expected_result", "expected_exception"),
+        [
+            ("DONE", True, None),
+            ("RUNNING", False, None),
+            ("FAILED", None, AirflowFailException),
+        ],
+    )
     @mock.patch(f"{MODULE_NAME}.GoogleBidManagerHook")
-    @mock.patch(f"{MODULE_NAME}.BaseSensorOperator")
-    def test_poke(self, mock_base_op, hook_mock):
+    def test_poke(self, hook_mock, state, expected_result, expected_exception):
         query_id = "QUERY_ID"
         report_id = "REPORT_ID"
+        hook_mock.return_value.get_report.return_value = {"metadata": 
{"status": {"state": state}}}
         op = GoogleBidManagerRunQuerySensor(query_id=query_id, 
report_id=report_id, task_id="test_task")
-        op.poke(context=None)
+        if expected_exception:
+            with pytest.raises(expected_exception, match="failed execution"):
+                op.poke(context=None)
+        else:
+            result = op.poke(context=None)
+            assert result is expected_result
+
         hook_mock.assert_called_once_with(
             gcp_conn_id=GCP_CONN_ID,
             api_version="v2",

Reply via email to