1313# limitations under the License.
1414"""A set of functions to help load configuration from various locations."""
1515
16- import json
1716import functools
17+ import json
18+ import logging .config
1819import os
1920import yaml
2021
22+
23+ _logger = logging .getLogger (__name__ )
24+
2125_ENV_PREFIX = "GOOGLE_ADS_"
2226_REQUIRED_KEYS = ("developer_token" ,)
23- _OPTIONAL_KEYS = ("login_customer_id" , "endpoint" , "logging" )
27+ _OPTIONAL_KEYS = (
28+ "login_customer_id" ,
29+ "endpoint" ,
30+ "logging" ,
31+ "linked_customer_id" ,
32+ )
33+ _CONFIG_FILE_PATH_KEY = ("configuration_file_path" ,)
2434_OAUTH2_INSTALLED_APP_KEYS = ("client_id" , "client_secret" , "refresh_token" )
25- _OAUTH2_SERVICE_ACCOUNT_KEYS = ("path_to_private_key_file" , "delegated_account" )
35+ _OAUTH2_SERVICE_ACCOUNT_KEYS = ("json_key_file_path" , "impersonated_email" )
36+ # These keys are deprecated environment variables that can be used in place of
37+ # the primary OAuth2 service account keys for backwards compatibility. They will
38+ # be removed in favor of the primary keys at some point.
39+ _SECONDARY_OAUTH2_SERVICE_ACCOUNT_KEYS = (
40+ "path_to_private_key_file" ,
41+ "delegated_account" ,
42+ )
2643_KEYS_ENV_VARIABLES_MAP = {
2744 key : _ENV_PREFIX + key .upper ()
28- for key in list (_REQUIRED_KEYS )
29- + list (_OPTIONAL_KEYS )
30- + list (_OAUTH2_INSTALLED_APP_KEYS )
31- + list (_OAUTH2_SERVICE_ACCOUNT_KEYS )
45+ for key in _REQUIRED_KEYS
46+ + _OPTIONAL_KEYS
47+ + _OAUTH2_INSTALLED_APP_KEYS
48+ + _CONFIG_FILE_PATH_KEY
49+ + _OAUTH2_SERVICE_ACCOUNT_KEYS
50+ + _SECONDARY_OAUTH2_SERVICE_ACCOUNT_KEYS
3251}
3352
3453
@@ -63,6 +82,61 @@ def _config_parser_decorator(func):
6382 def parser_wrapper (* args , ** kwargs ):
6483 config_dict = func (* args , ** kwargs )
6584 parsed_config = convert_login_customer_id_to_str (config_dict )
85+ parsed_config = convert_linked_customer_id_to_str (parsed_config )
86+
87+ config_keys = parsed_config .keys ()
88+
89+ if "logging" in config_keys :
90+ logging_config = parsed_config ["logging" ]
91+ # If the logging config is a dict then it is already in the format
92+ # that needs to be returned by this method.
93+ if type (logging_config ) is not dict :
94+ try :
95+ parsed_config ["logging" ] = json .loads (logging_config )
96+ # The logger is configured here in case deprecation warnings
97+ # need to be logged further down in this method. The logger
98+ # is otherwise configured by the GoogleAdsClient class.
99+ logging .config .dictConfig (parsed_config ["logging" ])
100+ except json .JSONDecodeError :
101+ raise ValueError (
102+ "Could not configure the client because the logging "
103+ "configuration defined in the 'logging' key or "
104+ "'GOOGLE_ADS_LOGGING' environment variable is invalid. "
105+ "The configuration value should be a valid JSON string."
106+ )
107+
108+ if "path_to_private_key_file" in config_keys :
109+ _logger .warning (
110+ "The 'path_to_private_key_file' configuration key and "
111+ "'GOOGLE_ADS_PATH_TO_PRIVATE_KEY_FILE' environment variable "
112+ "are deprecated and support will be removed at some point in "
113+ "the future. Please use 'json_key_file_path' configuration key "
114+ "or 'GOOGLE_ADS_JSON_KEY_FILE_PATH' environment variable "
115+ "instead."
116+ )
117+ if "json_key_file_path" not in config_keys :
118+ parsed_config ["json_key_file_path" ] = parsed_config [
119+ "path_to_private_key_file"
120+ ]
121+
122+ del parsed_config ["path_to_private_key_file" ]
123+
124+ if "delegated_account" in config_keys :
125+ _logger .warning (
126+ "The 'delegated_account' configuration key and "
127+ "'GOOGLE_ADS_DELEGATED_PATH' environment variable are "
128+ "deprecated and support will be removed at some point in "
129+ "the future. Please use 'impersonated_email' configuration key "
130+ "or 'GOOGLE_ADS_IMPERSONATED_EMAIL' environment variable "
131+ "instead."
132+ )
133+ if "impersonated_email" not in config_keys :
134+ parsed_config ["impersonated_email" ] = parsed_config [
135+ "delegated_account"
136+ ]
137+
138+ del parsed_config ["delegated_account" ]
139+
66140 return parsed_config
67141
68142 return parser_wrapper
@@ -74,6 +148,7 @@ def validate_dict(config_data):
74148 Validations that are performed include:
75149 1. Ensuring all required keys are present.
76150 2. If a login_customer_id is present ensure it's valid
151+ 3. If a linked_customer_id is present ensure it's valid
77152
78153 Args:
79154 config_data: a dict with configuration data.
@@ -90,26 +165,57 @@ def validate_dict(config_data):
90165 if "login_customer_id" in config_data :
91166 validate_login_customer_id (config_data ["login_customer_id" ])
92167
168+ if "linked_customer_id" in config_data :
169+ validate_linked_customer_id (config_data ["linked_customer_id" ])
93170
94- def validate_login_customer_id (login_customer_id ):
95- """Validates a login customer ID.
171+
172+ def _validate_customer_id (customer_id , id_type ):
173+ """Validates a customer ID.
96174
97175 Args:
98- login_customer_id: a str from config indicating a login customer ID.
176+ customer_id: a str from config indicating a login customer ID or
177+ linked customer ID.
178+ id_type: a str of the type of customer ID, either "login" or "linked".
99179
100180 Raises:
101- ValueError: If the login customer ID is not an int in the
181+ ValueError: If the customer ID is not an int in the
102182 range 0 - 9999999999.
103183 """
104- if login_customer_id is not None :
105- if not login_customer_id .isdigit () or len (login_customer_id ) != 10 :
184+ if customer_id is not None :
185+ if not customer_id .isdigit () or len (customer_id ) != 10 :
106186 raise ValueError (
107- "The specified login customer ID is invalid. "
187+ f "The specified { id_type } customer ID is invalid. "
108188 "It must be a ten digit number represented "
109189 'as a string, i.e. "1234567890"'
110190 )
111191
112192
193+ def validate_login_customer_id (login_customer_id ):
194+ """Validates a login customer ID.
195+
196+ Args:
197+ login_customer_id: a str from config indicating a login customer ID.
198+
199+ Raises:
200+ ValueError: If the login customer ID is not an int in the
201+ range 0 - 9999999999.
202+ """
203+ _validate_customer_id (login_customer_id , "login" )
204+
205+
206+ def validate_linked_customer_id (linked_customer_id ):
207+ """Validates a linked customer ID.
208+
209+ Args:
210+ linked_customer_id: a str from config indicating a linked customer ID.
211+
212+ Raises:
213+ ValueError: If the linked customer ID is not an int in the
214+ range 0 - 9999999999.
215+ """
216+ _validate_customer_id (linked_customer_id , "linked" )
217+
218+
113219@_config_validation_decorator
114220@_config_parser_decorator
115221def load_from_yaml_file (path = None ):
@@ -127,7 +233,17 @@ def load_from_yaml_file(path=None):
127233 IOError: If the configuration file can't be loaded.
128234 """
129235 if path is None :
130- path = os .path .join (os .path .expanduser ("~" ), "google-ads.yaml" )
236+ # If no path is specified then we check for the environment variable
237+ # that may define the path. If that is not defined then we use the
238+ # default path.
239+ path_from_env_var = os .environ .get (
240+ _ENV_PREFIX + _CONFIG_FILE_PATH_KEY [0 ].upper ()
241+ )
242+ path = (
243+ path_from_env_var
244+ if path_from_env_var
245+ else os .path .join (os .path .expanduser ("~" ), "google-ads.yaml" )
246+ )
131247
132248 if not os .path .isabs (path ):
133249 path = os .path .expanduser (path )
@@ -192,17 +308,15 @@ def load_from_env():
192308 ValueError: If the configuration
193309 """
194310 config_data = {
195- key : os .environ [ env_variable ]
311+ key : os .environ . get ( env_variable )
196312 for key , env_variable in _KEYS_ENV_VARIABLES_MAP .items ()
197313 if env_variable in os .environ
198314 }
199- if "logging" in config_data .keys ():
200- try :
201- config_data ["logging" ] = json .loads (config_data ["logging" ])
202- except json .JSONDecodeError :
203- raise ValueError (
204- "GOOGLE_ADS_LOGGING env variable should be in JSON format."
205- )
315+
316+ # If configuration_file_path is set by the environment then configuration
317+ # is retrieved from the yaml file specified in the given path.
318+ if "configuration_file_path" in config_data .keys ():
319+ return load_from_yaml_file (config_data ["configuration_file_path" ])
206320
207321 return config_data
208322
@@ -244,3 +358,24 @@ def convert_login_customer_id_to_str(config_data):
244358 config_data ["login_customer_id" ] = str (login_customer_id )
245359
246360 return config_data
361+
362+
363+ def convert_linked_customer_id_to_str (config_data ):
364+ """Parses a config dict's linked_customer_id attr value to a str.
365+
366+ Like many values from YAML it's possible for linked_customer_id to
367+ either be a str or an int. Since we actually run validations on this
368+ value before making requests it's important to parse it to a str.
369+
370+ Args:
371+ config_data: A config dict object.
372+
373+ Returns:
374+ The same config dict object with a mutated linked_customer_id attr.
375+ """
376+ linked_customer_id = config_data .get ("linked_customer_id" )
377+
378+ if linked_customer_id :
379+ config_data ["linked_customer_id" ] = str (linked_customer_id )
380+
381+ return config_data
0 commit comments