Source code for airflow.providers.google.ads.hooks.ads
#
# 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.
"""This module contains Google Ad hook."""
from tempfile import NamedTemporaryFile
from typing import IO, Any, Dict, Generator, List
from cached_property import cached_property
from google.ads.google_ads.client import GoogleAdsClient
from google.ads.google_ads.errors import GoogleAdsException
from google.ads.google_ads.v2.types import GoogleAdsRow
from google.api_core.page_iterator import GRPCIterator
from google.auth.exceptions import GoogleAuthError
from googleapiclient.discovery import Resource
from airflow import AirflowException
from airflow.hooks.base import BaseHook
[docs]class GoogleAdsHook(BaseHook):
"""
Hook for the Google Ads API.
This hook requires two connections:
- gcp_conn_id - provides service account details (like any other GCP connection)
- google_ads_conn_id - which contains information from Google Ads config.yaml file
in the ``extras``. Example of the ``extras``:
.. code-block:: json
{
"google_ads_client": {
"developer_token": "{{ INSERT_TOKEN }}",
"path_to_private_key_file": null,
"delegated_account": "{{ INSERT_DELEGATED_ACCOUNT }}"
}
}
The ``path_to_private_key_file`` is resolved by the hook using credentials from gcp_conn_id.
https://developers.google.com/google-ads/api/docs/client-libs/python/oauth-service
.. seealso::
For more information on how Google Ads authentication flow works take a look at:
https://developers.google.com/google-ads/api/docs/client-libs/python/oauth-service
.. seealso::
For more information on the Google Ads API, take a look at the API docs:
https://developers.google.com/google-ads/api/docs/start
:param gcp_conn_id: The connection ID with the service account details.
:type gcp_conn_id: str
:param google_ads_conn_id: The connection ID with the details of Google Ads config.yaml file.
:type google_ads_conn_id: str
:return: list of Google Ads Row object(s)
:rtype: list[GoogleAdsRow]
"""
def __init__(
self,
gcp_conn_id: str = "google_cloud_default",
google_ads_conn_id: str = "google_ads_default",
api_version: str = "v3",
) -> None:
super().__init__()
self.gcp_conn_id = gcp_conn_id
self.google_ads_conn_id = google_ads_conn_id
self.gcp_conn_id = gcp_conn_id
self.api_version = api_version
self.google_ads_config: Dict[str, Any] = {}
@cached_property
[docs] def _get_service(self) -> Resource:
"""Connects and authenticates with the Google Ads API using a service account"""
with NamedTemporaryFile("w", suffix=".json") as secrets_temp:
self._get_config()
self._update_config_with_secret(secrets_temp)
try:
client = GoogleAdsClient.load_from_dict(self.google_ads_config)
return client.get_service("GoogleAdsService", version=self.api_version)
except GoogleAuthError as e:
self.log.error("Google Auth Error: %s", e)
raise
@cached_property
[docs] def _get_customer_service(self) -> Resource:
"""Connects and authenticates with the Google Ads API using a service account"""
with NamedTemporaryFile("w", suffix=".json") as secrets_temp:
self._get_config()
self._update_config_with_secret(secrets_temp)
try:
client = GoogleAdsClient.load_from_dict(self.google_ads_config)
return client.get_service("CustomerService", version=self.api_version)
except GoogleAuthError as e:
self.log.error("Google Auth Error: %s", e)
raise
[docs] def _get_config(self) -> None:
"""
Gets google ads connection from meta db and sets google_ads_config attribute with returned config
file
"""
conn = self.get_connection(self.google_ads_conn_id)
if "google_ads_client" not in conn.extra_dejson:
raise AirflowException("google_ads_client not found in extra field")
self.google_ads_config = conn.extra_dejson["google_ads_client"]
[docs] def _update_config_with_secret(self, secrets_temp: IO[str]) -> None:
"""
Gets Google Cloud secret from connection and saves the contents to the temp file
Updates google ads config with file path of the temp file containing the secret
Note, the secret must be passed as a file path for Google Ads API
"""
secret_conn = self.get_connection(self.gcp_conn_id)
secret = secret_conn.extra_dejson["extra__google_cloud_platform__keyfile_dict"]
secrets_temp.write(secret)
secrets_temp.flush()
self.google_ads_config["path_to_private_key_file"] = secrets_temp.name
[docs] def search(
self, client_ids: List[str], query: str, page_size: int = 10000, **kwargs
) -> List[GoogleAdsRow]:
"""
Pulls data from the Google Ads API
:param client_ids: Google Ads client ID(s) to query the API for.
:type client_ids: List[str]
:param query: Google Ads Query Language query.
:type query: str
:param page_size: Number of results to return per page. Max 10000.
:type page_size: int
:return: Google Ads API response, converted to Google Ads Row objects
:rtype: list[GoogleAdsRow]
"""
service = self._get_service
iterators = (
service.search(client_id, query=query, page_size=page_size, **kwargs) for client_id in client_ids
)
self.log.info("Fetched Google Ads Iterators")
return self._extract_rows(iterators)
[docs] def list_accessible_customers(self) -> List[str]:
"""
Returns resource names of customers directly accessible by the user authenticating the call.
The resulting list of customers is based on your OAuth credentials. The request returns a list
of all accounts that you are able to act upon directly given your current credentials. This will
not necessarily include all accounts within the account hierarchy; rather, it will only include
accounts where your authenticated user has been added with admin or other rights in the account.
..seealso::
https://developers.google.com/google-ads/api/reference/rpc
:return: List of names of customers
"""
try:
accessible_customers = self._get_customer_service.list_accessible_customers()
return accessible_customers.resource_names
except GoogleAdsException as ex:
for error in ex.failure.errors:
self.log.error('\tError with message "%s".', error.message)
if error.location:
for field_path_element in error.location.field_path_elements:
self.log.error('\t\tOn field: %s', field_path_element.field_name)
raise