Skip to content

Commit

Permalink
Support for Google Ads API v0_6
Browse files Browse the repository at this point in the history
  • Loading branch information
BenRKarl committed Nov 30, 2018
1 parent 14173d9 commit fbdad63
Show file tree
Hide file tree
Showing 549 changed files with 36,021 additions and 1,259 deletions.
12 changes: 12 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
* 0.5.0:
- Google Ads v0_6 release.
- Updating add_campaign_targeting_criteria.py example to add
create proximity operation
- Updating apply_recommendation.py example to pass required parameter
partial_failure=False
- Updating get_geo_target_constant_by_names.py example to add
new required country_code parameter
- Updating client.py to accept a login_customer_id
- Fixing bug in ExceptionInterceptor to improve error logging. Resolves GitHub
issue #8: https://github.com/googleads/google-ads-python/issues/8

* 0.4.0:
- Google Ads v0_5 release.
- Adding remarketing/add_conversion_action.py example.
Expand Down
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ Configuration file setup
########################

To authenticate your API calls, you must specify your **client ID**,
**client secret**, **refresh token**, and **developer token**. If you have not
yet created a client ID, see the `Authorization guide`_ and the
`authentication samples`_ to get started. Likewise, see
**client secret**, **refresh token**, **developer token**, and, if
you are authenticating with a manager account, a **login customer id**.
If you have not yet created a client ID, see the `Authorization guide`_
and the `authentication samples`_ to get started. Likewise, see
`Obtain your developer token`_ if you do not yet have one.

When initializing a `GoogleAdsClient` instance via the `load_from_storage`
Expand Down
5 changes: 4 additions & 1 deletion examples/v0/recommendations/apply_recommendation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

def main(client, customer_id, recommendation_id):
recommendation_service = client.get_service('RecommendationService')
partial_failure = False

apply_recommendation_operation = client.get_type(
'ApplyRecommendationOperation')
Expand All @@ -37,7 +38,9 @@ def main(client, customer_id, recommendation_id):

try:
recommendation_response = recommendation_service.apply_recommendation(
customer_id, [apply_recommendation_operation])
customer_id,
partial_failure,
[apply_recommendation_operation])
except google.ads.google_ads.errors.GoogleAdsException as ex:
print('Request with ID "%s" failed with status "%s" and includes the '
'following errors:' % (ex.request_id, ex.error.code().name))
Expand Down
23 changes: 22 additions & 1 deletion examples/v0/targeting/add_campaign_targeting_criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def main(client, customer_id, campaign_id, keyword, location_id):

operations = [
create_location_op(client, customer_id, campaign_id, location_id),
create_negative_keyword_op(client, customer_id, campaign_id, keyword)
create_negative_keyword_op(client, customer_id, campaign_id, keyword),
create_proximity_op(client, customer_id, campaign_id)
]

try:
Expand Down Expand Up @@ -84,6 +85,26 @@ def create_negative_keyword_op(client, customer_id, campaign_id, keyword):
return campaign_criterion_operation


def create_proximity_op(client, customer_id, campaign_id):
campaign_service = client.get_service('CampaignService')

# Create the campaign criterion.
campaign_criterion_operation = client.get_type('CampaignCriterionOperation')
campaign_criterion = campaign_criterion_operation.create
campaign_criterion.campaign.value = campaign_service.campaign_path(
customer_id, campaign_id)
campaign_criterion.proximity.address.street_address.value = '38 avenue de l\'Opera'
campaign_criterion.proximity.address.city_name.value = 'Paris'
campaign_criterion.proximity.address.postal_code.value = '75002'
campaign_criterion.proximity.address.country_code.value = 'FR'
campaign_criterion.proximity.radius.value = 10
# Default is kilometers.
campaign_criterion.proximity.radius_units = client.get_type(
'ProximityRadiusUnitsEnum').MILES

return campaign_criterion_operation


if __name__ == '__main__':
# GoogleAdsClient will read the google-ads.yaml configuration file in the
# home directory if none is specified.
Expand Down
7 changes: 6 additions & 1 deletion examples/v0/targeting/get_geo_target_constant_by_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ def main(client):
locale = client.get_type('StringValue')
locale.value = 'en'

# A list of country codes can be referenced here:
# https://developers.google.com/adwords/api/docs/appendix/geotargeting
country_code = client.get_type('StringValue')
country_code.value = 'FR'

results = gtc_service.suggest_geo_target_constants(
locale, location_names=location_names)
locale, country_code, location_names=location_names)

geo_target_constant_status_enum = client.get_type(
'GeoTargetConstantStatusEnum').GeoTargetConstantStatus
Expand Down
6 changes: 6 additions & 0 deletions google-ads.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ developer_token: INSERT_DEVELOPER_TOKEN_HERE
client_id: INSERT_OAUTH2_CLIENT_ID_HERE
client_secret: INSERT_OAUTH2_CLIENT_SECRET_HERE
refresh_token: INSERT_REFRESH_TOKEN_HERE
# Required for manager accounts only: Specify the login customer ID used to
# authenticate API calls. This will be the customer ID of the authenticated
# manager account. It should be set without dashes, for example: 1234567890
# instead of 123-456-7890. You can also specify this later in code if your
# application uses multiple manager account + OAuth pairs.
login_customer_id: INSERT_LOGIN_CUSTOMER_ID_HERE
103 changes: 84 additions & 19 deletions google/ads/google_ads/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,14 @@ def _get_client_kwargs_from_yaml(cls, yaml_str):
token_uri=_DEFAULT_TOKEN_URI)
credentials.refresh(google.auth.transport.requests.Request())

login_customer_id = config_data.get('login_customer_id')
login_customer_id = str(
login_customer_id) if login_customer_id else None

return {'credentials': credentials,
'developer_token': config_data['developer_token'],
'endpoint': config_data.get('endpoint')}
'endpoint': config_data.get('endpoint'),
'login_customer_id': login_customer_id}
else:
raise ValueError('A required field in the configuration data was'
'not found. The required fields are: %s'
Expand Down Expand Up @@ -142,17 +147,22 @@ def load_from_storage(cls, path=None):

return cls.load_from_string(yaml_str)

def __init__(self, credentials, developer_token, endpoint=None):
def __init__(self, credentials, developer_token, endpoint=None,
login_customer_id=None):
"""Initializer for the GoogleAdsClient.
Args:
credentials: a google.oauth2.credentials.Credentials instance.
developer_token: a str developer token.
endpoint: a str specifying an optional alternative API endpoint.
login_customer_id: a str specifying a login customer ID.
"""
_validate_login_customer_id(login_customer_id)

self.credentials = credentials
self.developer_token = developer_token
self.endpoint = endpoint
self.login_customer_id = login_customer_id

def get_service(self, name, version=_DEFAULT_VERSION):
"""Returns a service client instance for the specified service_name.
Expand Down Expand Up @@ -192,7 +202,7 @@ def get_service(self, name, version=_DEFAULT_VERSION):

channel = grpc.intercept_channel(
channel,
MetadataInterceptor(self.developer_token),
MetadataInterceptor(self.developer_token, self.login_customer_id),
ExceptionInterceptor(),
)

Expand Down Expand Up @@ -247,32 +257,64 @@ def _get_request_id(self, trailing_metadata):

return None

def _handle_grpc_exception(self, exception):
"""Handles gRPC exceptions of type RpcError by attempting to
convert them to a more readable GoogleAdsException. Certain types of
exceptions are not converted; if the object's trailing metadata does
not indicate that it is a GoogleAdsException, or if it falls under
a certain category of status code, (INTERNAL or RESOURCE_EXHAUSTED).
See documentation for more information about gRPC status codes:
https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

def intercept_unary_unary(self, continuation, client_call_details, request):
"""Intercepts and wraps exceptions in the rpc response.
Overrides abstract method defined in grpc.UnaryUnaryClientInterceptor.
Args:
exception: an exception of type RpcError.
Raises:
GoogleAdsException: if the Google Ads API response contains an
exception and GoogleAdsFailure is in the trailing metadata.
GoogleAdsException: If the exception's trailing metadata
indicates that it is a GoogleAdsException.
RpcError: If the exception's trailing metadata is empty or is not
indicative of a GoogleAdsException, or if the exception has a
status code of INTERNAL or RESOURCE_EXHAUSTED.
"""
response = continuation(client_call_details, request)
ex = response.exception()

if ex and response._state.code not in self._RETRY_STATUS_CODES:
trailing_metadata = response.trailing_metadata()
google_ads_failure = self._get_google_ads_failure(trailing_metadata)
if exception._state.code not in self._RETRY_STATUS_CODES:
trailing_metadata = exception.trailing_metadata()
google_ads_failure = self._get_google_ads_failure(
trailing_metadata)

if google_ads_failure:
request_id = self._get_request_id(trailing_metadata)

raise google.ads.google_ads.errors.GoogleAdsException(
ex, response, google_ads_failure, request_id)
exception, exception, google_ads_failure, request_id)
else:
# Raise the original exception if not a GoogleAdsFailure.
raise ex
raise exception
else:
# Raise the original exception if error has status code
# INTERNAL or RESOURCE_EXHAUSTED.
raise exception

def intercept_unary_unary(self, continuation, client_call_details, request):
"""Intercepts and wraps exceptions in the rpc response.
Overrides abstract method defined in grpc.UnaryUnaryClientInterceptor.
Raises:
GoogleAdsException: If the exception's trailing metadata
indicates that it is a GoogleAdsException.
RpcError: If the exception's trailing metadata is empty or is not
indicative of a GoogleAdsException, or if the exception has a
status code of INTERNAL or RESOURCE_EXHAUSTED.
"""
try:
response = continuation(client_call_details, request)
except grpc.RpcError as ex:
self._handle_grpc_exception(ex)

if response.exception():
# Any exception raised within the continuation function that is not
# an RpcError will be set on the response object and raised here.
raise response.exception()

return response

Expand All @@ -298,8 +340,11 @@ def intercept_unary_unary(self, continuation, client_call_details, request):
class MetadataInterceptor(grpc.UnaryUnaryClientInterceptor):
"""An interceptor that appends custom metadata to requests."""

def __init__(self, developer_token):
def __init__(self, developer_token, login_customer_id):
self.developer_token_meta = ('developer-token', developer_token)
self.login_customer_id_meta = (
('login-customer-id', login_customer_id) if login_customer_id
else None)

def intercept_unary_unary(self, continuation, client_call_details, request):
"""Intercepts and appends custom metadata.
Expand All @@ -313,6 +358,9 @@ def intercept_unary_unary(self, continuation, client_call_details, request):

metadata.append(self.developer_token_meta)

if self.login_customer_id_meta:
metadata.append(self.login_customer_id_meta)

client_call_details = grpc._interceptor._ClientCallDetails(
client_call_details.method, client_call_details.timeout, metadata,
client_call_details.credentials
Expand All @@ -336,3 +384,20 @@ def _get_version(name):
raise ValueError('Specified Google Ads API version "%s" does not exist.'
% name)
return version


def _validate_login_customer_id(login_customer_id):
"""Validates a login customer ID.
Args:
login_customer_id: a str from config indicating a login customer ID.
Raises:
ValueError: If the login customer ID is not
an int in the range 0 - 9999999999.
"""
if login_customer_id is not None:
if not login_customer_id.isdigit() or len(login_customer_id) != 10:
raise ValueError('The specified login customer ID is invalid. '
'It must be a ten digit number represented '
'as a string, i.e. "1234567890"')
Loading

0 comments on commit fbdad63

Please sign in to comment.