Skip to content

Commit 951da24

Browse files
authored
Merge pull request #14 from googleads/release-v0_6-52ce0f551fdc1363b6bf
Support for Google Ads API v0_6
2 parents 14173d9 + fbdad63 commit 951da24

File tree

549 files changed

+36021
-1259
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

549 files changed

+36021
-1259
lines changed

ChangeLog

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
* 0.5.0:
2+
- Google Ads v0_6 release.
3+
- Updating add_campaign_targeting_criteria.py example to add
4+
create proximity operation
5+
- Updating apply_recommendation.py example to pass required parameter
6+
partial_failure=False
7+
- Updating get_geo_target_constant_by_names.py example to add
8+
new required country_code parameter
9+
- Updating client.py to accept a login_customer_id
10+
- Fixing bug in ExceptionInterceptor to improve error logging. Resolves GitHub
11+
issue #8: https://github.com/googleads/google-ads-python/issues/8
12+
113
* 0.4.0:
214
- Google Ads v0_5 release.
315
- Adding remarketing/add_conversion_action.py example.

README.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ Configuration file setup
3232
########################
3333

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

4041
When initializing a `GoogleAdsClient` instance via the `load_from_storage`

examples/v0/recommendations/apply_recommendation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
def main(client, customer_id, recommendation_id):
2929
recommendation_service = client.get_service('RecommendationService')
30+
partial_failure = False
3031

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

3839
try:
3940
recommendation_response = recommendation_service.apply_recommendation(
40-
customer_id, [apply_recommendation_operation])
41+
customer_id,
42+
partial_failure,
43+
[apply_recommendation_operation])
4144
except google.ads.google_ads.errors.GoogleAdsException as ex:
4245
print('Request with ID "%s" failed with status "%s" and includes the '
4346
'following errors:' % (ex.request_id, ex.error.code().name))

examples/v0/targeting/add_campaign_targeting_criteria.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ def main(client, customer_id, campaign_id, keyword, location_id):
2727

2828
operations = [
2929
create_location_op(client, customer_id, campaign_id, location_id),
30-
create_negative_keyword_op(client, customer_id, campaign_id, keyword)
30+
create_negative_keyword_op(client, customer_id, campaign_id, keyword),
31+
create_proximity_op(client, customer_id, campaign_id)
3132
]
3233

3334
try:
@@ -84,6 +85,26 @@ def create_negative_keyword_op(client, customer_id, campaign_id, keyword):
8485
return campaign_criterion_operation
8586

8687

88+
def create_proximity_op(client, customer_id, campaign_id):
89+
campaign_service = client.get_service('CampaignService')
90+
91+
# Create the campaign criterion.
92+
campaign_criterion_operation = client.get_type('CampaignCriterionOperation')
93+
campaign_criterion = campaign_criterion_operation.create
94+
campaign_criterion.campaign.value = campaign_service.campaign_path(
95+
customer_id, campaign_id)
96+
campaign_criterion.proximity.address.street_address.value = '38 avenue de l\'Opera'
97+
campaign_criterion.proximity.address.city_name.value = 'Paris'
98+
campaign_criterion.proximity.address.postal_code.value = '75002'
99+
campaign_criterion.proximity.address.country_code.value = 'FR'
100+
campaign_criterion.proximity.radius.value = 10
101+
# Default is kilometers.
102+
campaign_criterion.proximity.radius_units = client.get_type(
103+
'ProximityRadiusUnitsEnum').MILES
104+
105+
return campaign_criterion_operation
106+
107+
87108
if __name__ == '__main__':
88109
# GoogleAdsClient will read the google-ads.yaml configuration file in the
89110
# home directory if none is specified.

examples/v0/targeting/get_geo_target_constant_by_names.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ def main(client):
3636
locale = client.get_type('StringValue')
3737
locale.value = 'en'
3838

39+
# A list of country codes can be referenced here:
40+
# https://developers.google.com/adwords/api/docs/appendix/geotargeting
41+
country_code = client.get_type('StringValue')
42+
country_code.value = 'FR'
43+
3944
results = gtc_service.suggest_geo_target_constants(
40-
locale, location_names=location_names)
45+
locale, country_code, location_names=location_names)
4146

4247
geo_target_constant_status_enum = client.get_type(
4348
'GeoTargetConstantStatusEnum').GeoTargetConstantStatus

google-ads.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@ developer_token: INSERT_DEVELOPER_TOKEN_HERE
22
client_id: INSERT_OAUTH2_CLIENT_ID_HERE
33
client_secret: INSERT_OAUTH2_CLIENT_SECRET_HERE
44
refresh_token: INSERT_REFRESH_TOKEN_HERE
5+
# Required for manager accounts only: Specify the login customer ID used to
6+
# authenticate API calls. This will be the customer ID of the authenticated
7+
# manager account. It should be set without dashes, for example: 1234567890
8+
# instead of 123-456-7890. You can also specify this later in code if your
9+
# application uses multiple manager account + OAuth pairs.
10+
login_customer_id: INSERT_LOGIN_CUSTOMER_ID_HERE

google/ads/google_ads/client.py

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,14 @@ def _get_client_kwargs_from_yaml(cls, yaml_str):
6464
token_uri=_DEFAULT_TOKEN_URI)
6565
credentials.refresh(google.auth.transport.requests.Request())
6666

67+
login_customer_id = config_data.get('login_customer_id')
68+
login_customer_id = str(
69+
login_customer_id) if login_customer_id else None
70+
6771
return {'credentials': credentials,
6872
'developer_token': config_data['developer_token'],
69-
'endpoint': config_data.get('endpoint')}
73+
'endpoint': config_data.get('endpoint'),
74+
'login_customer_id': login_customer_id}
7075
else:
7176
raise ValueError('A required field in the configuration data was'
7277
'not found. The required fields are: %s'
@@ -142,17 +147,22 @@ def load_from_storage(cls, path=None):
142147

143148
return cls.load_from_string(yaml_str)
144149

145-
def __init__(self, credentials, developer_token, endpoint=None):
150+
def __init__(self, credentials, developer_token, endpoint=None,
151+
login_customer_id=None):
146152
"""Initializer for the GoogleAdsClient.
147153
148154
Args:
149155
credentials: a google.oauth2.credentials.Credentials instance.
150156
developer_token: a str developer token.
151157
endpoint: a str specifying an optional alternative API endpoint.
158+
login_customer_id: a str specifying a login customer ID.
152159
"""
160+
_validate_login_customer_id(login_customer_id)
161+
153162
self.credentials = credentials
154163
self.developer_token = developer_token
155164
self.endpoint = endpoint
165+
self.login_customer_id = login_customer_id
156166

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

193203
channel = grpc.intercept_channel(
194204
channel,
195-
MetadataInterceptor(self.developer_token),
205+
MetadataInterceptor(self.developer_token, self.login_customer_id),
196206
ExceptionInterceptor(),
197207
)
198208

@@ -247,32 +257,64 @@ def _get_request_id(self, trailing_metadata):
247257

248258
return None
249259

260+
def _handle_grpc_exception(self, exception):
261+
"""Handles gRPC exceptions of type RpcError by attempting to
262+
convert them to a more readable GoogleAdsException. Certain types of
263+
exceptions are not converted; if the object's trailing metadata does
264+
not indicate that it is a GoogleAdsException, or if it falls under
265+
a certain category of status code, (INTERNAL or RESOURCE_EXHAUSTED).
266+
See documentation for more information about gRPC status codes:
267+
https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
250268
251-
252-
def intercept_unary_unary(self, continuation, client_call_details, request):
253-
"""Intercepts and wraps exceptions in the rpc response.
254-
255-
Overrides abstract method defined in grpc.UnaryUnaryClientInterceptor.
269+
Args:
270+
exception: an exception of type RpcError.
256271
257272
Raises:
258-
GoogleAdsException: if the Google Ads API response contains an
259-
exception and GoogleAdsFailure is in the trailing metadata.
273+
GoogleAdsException: If the exception's trailing metadata
274+
indicates that it is a GoogleAdsException.
275+
RpcError: If the exception's trailing metadata is empty or is not
276+
indicative of a GoogleAdsException, or if the exception has a
277+
status code of INTERNAL or RESOURCE_EXHAUSTED.
260278
"""
261-
response = continuation(client_call_details, request)
262-
ex = response.exception()
263-
264-
if ex and response._state.code not in self._RETRY_STATUS_CODES:
265-
trailing_metadata = response.trailing_metadata()
266-
google_ads_failure = self._get_google_ads_failure(trailing_metadata)
279+
if exception._state.code not in self._RETRY_STATUS_CODES:
280+
trailing_metadata = exception.trailing_metadata()
281+
google_ads_failure = self._get_google_ads_failure(
282+
trailing_metadata)
267283

268284
if google_ads_failure:
269285
request_id = self._get_request_id(trailing_metadata)
270286

271287
raise google.ads.google_ads.errors.GoogleAdsException(
272-
ex, response, google_ads_failure, request_id)
288+
exception, exception, google_ads_failure, request_id)
273289
else:
274290
# Raise the original exception if not a GoogleAdsFailure.
275-
raise ex
291+
raise exception
292+
else:
293+
# Raise the original exception if error has status code
294+
# INTERNAL or RESOURCE_EXHAUSTED.
295+
raise exception
296+
297+
def intercept_unary_unary(self, continuation, client_call_details, request):
298+
"""Intercepts and wraps exceptions in the rpc response.
299+
300+
Overrides abstract method defined in grpc.UnaryUnaryClientInterceptor.
301+
302+
Raises:
303+
GoogleAdsException: If the exception's trailing metadata
304+
indicates that it is a GoogleAdsException.
305+
RpcError: If the exception's trailing metadata is empty or is not
306+
indicative of a GoogleAdsException, or if the exception has a
307+
status code of INTERNAL or RESOURCE_EXHAUSTED.
308+
"""
309+
try:
310+
response = continuation(client_call_details, request)
311+
except grpc.RpcError as ex:
312+
self._handle_grpc_exception(ex)
313+
314+
if response.exception():
315+
# Any exception raised within the continuation function that is not
316+
# an RpcError will be set on the response object and raised here.
317+
raise response.exception()
276318

277319
return response
278320

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

301-
def __init__(self, developer_token):
343+
def __init__(self, developer_token, login_customer_id):
302344
self.developer_token_meta = ('developer-token', developer_token)
345+
self.login_customer_id_meta = (
346+
('login-customer-id', login_customer_id) if login_customer_id
347+
else None)
303348

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

314359
metadata.append(self.developer_token_meta)
315360

361+
if self.login_customer_id_meta:
362+
metadata.append(self.login_customer_id_meta)
363+
316364
client_call_details = grpc._interceptor._ClientCallDetails(
317365
client_call_details.method, client_call_details.timeout, metadata,
318366
client_call_details.credentials
@@ -336,3 +384,20 @@ def _get_version(name):
336384
raise ValueError('Specified Google Ads API version "%s" does not exist.'
337385
% name)
338386
return version
387+
388+
389+
def _validate_login_customer_id(login_customer_id):
390+
"""Validates a login customer ID.
391+
392+
Args:
393+
login_customer_id: a str from config indicating a login customer ID.
394+
395+
Raises:
396+
ValueError: If the login customer ID is not
397+
an int in the range 0 - 9999999999.
398+
"""
399+
if login_customer_id is not None:
400+
if not login_customer_id.isdigit() or len(login_customer_id) != 10:
401+
raise ValueError('The specified login customer ID is invalid. '
402+
'It must be a ten digit number represented '
403+
'as a string, i.e. "1234567890"')

0 commit comments

Comments
 (0)