diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples.pyproj b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples.pyproj index e7fccf89..aa1db70a 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples.pyproj +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples.pyproj @@ -34,6 +34,9 @@ + + Code + diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py new file mode 100644 index 00000000..cd914f8f --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py @@ -0,0 +1,537 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 import * +from bingads.v10.bulk import * + +import sys +import webbrowser +from time import gmtime, strftime +from suds import WebFault + +# Optionally you can include logging to output traffic, for example the SOAP request and response. + +#import logging +#logging.basicConfig(level=logging.INFO) +#logging.getLogger('suds.client').setLevel(logging.DEBUG) + +if __name__ == '__main__': + print("Python loads the web service proxies at runtime, so you will observe " \ + "a performance delay between program launch and main execution...\n") + + DEVELOPER_TOKEN='DeveloperTokenGoesHere' + ENVIRONMENT='production' + + # If you are using OAuth in production, CLIENT_ID is required and CLIENT_STATE is recommended. + CLIENT_ID='ClientIdGoesHere' + CLIENT_STATE='ClientStateGoesHere' + + # The directory for the bulk files. + FILE_DIRECTORY='c:/bulk/' + + # The name of the bulk download file. + DOWNLOAD_FILE_NAME='download.csv' + + #The name of the bulk upload file. + UPLOAD_FILE_NAME='upload.csv' + + # The name of the bulk upload result file. + RESULT_FILE_NAME='result.csv' + + # The bulk file extension type. + FILE_FORMAT = DownloadFileType.csv + + # The bulk file extension type as a string. + FILE_TYPE = 'Csv' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + # Take advantage of the BulkServiceManager class to efficiently manage ads and keywords for all campaigns in an account. + # The client library provides classes to accelerate productivity for downloading and uploading entities. + # For example the upload_entities method of the BulkServiceManager class submits your upload request to the bulk service, + # polls the service until the upload completed, downloads the result file to a temporary directory, and exposes BulkEntity-derived objects + # that contain close representations of the corresponding Campaign Management data objects and value sets. + + # Poll for downloads at reasonable intervals. You know your data better than anyone. + # If you download an account that is well less than one million keywords, consider polling + # at 15 to 20 second intervals. If the account contains about one million keywords, consider polling + # at one minute intervals after waiting five minutes. For accounts with about four million keywords, + # consider polling at one minute intervals after waiting 10 minutes. + + bulk_service=BulkServiceManager( + authorization_data=authorization_data, + poll_interval_in_milliseconds=5000, + environment=ENVIRONMENT, + ) + + campaign_service=ServiceClient( + service='CampaignManagementService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=10, + ) + + customer_service=ServiceClient( + 'CustomerManagementService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=9, + ) + +def authenticate_with_username(): + ''' + Sets the authentication property of the global AuthorizationData instance with PasswordAuthentication. + ''' + global authorization_data + authentication=PasswordAuthentication( + user_name='UserNameGoesHere', + password='PasswordGoesHere' + ) + + # Assign this authentication instance to the global authorization_data. + authorization_data.authentication=authentication + +def authenticate_with_oauth(): + ''' + Sets the authentication property of the global AuthorizationData instance with OAuthDesktopMobileAuthCodeGrant. + ''' + global authorization_data + + authentication=OAuthDesktopMobileAuthCodeGrant( + client_id=CLIENT_ID + ) + + # It is recommended that you specify a non guessable 'state' request parameter to help prevent + # cross site request forgery (CSRF). + authentication.state=CLIENT_STATE + + # Assign this authentication instance to the global authorization_data. + authorization_data.authentication=authentication + + # Register the callback function to automatically save the refresh token anytime it is refreshed. + # Uncomment this line if you want to store your refresh token. Be sure to save your refresh token securely. + authorization_data.authentication.token_refreshed_callback=save_refresh_token + + refresh_token=get_refresh_token() + + try: + # If we have a refresh token let's refresh it + if refresh_token is not None: + authorization_data.authentication.request_oauth_tokens_by_refresh_token(refresh_token) + else: + request_user_consent() + except OAuthTokenRequestException: + # The user could not be authenticated or the grant is expired. + # The user must first sign in and if needed grant the client application access to the requested scope. + request_user_consent() + +def request_user_consent(): + global authorization_data + + webbrowser.open(authorization_data.authentication.get_authorization_endpoint(), new=1) + # For Python 3.x use 'input' instead of 'raw_input' + if(sys.version_info.major >= 3): + response_uri=input( + "You need to provide consent for the application to access your Bing Ads accounts. " \ + "After you have granted consent in the web browser for the application to access your Bing Ads accounts, " \ + "please enter the response URI that includes the authorization 'code' parameter: \n" + ) + else: + response_uri=raw_input( + "You need to provide consent for the application to access your Bing Ads accounts. " \ + "After you have granted consent in the web browser for the application to access your Bing Ads accounts, " \ + "please enter the response URI that includes the authorization 'code' parameter: \n" + ) + + if authorization_data.authentication.state != CLIENT_STATE: + raise Exception("The OAuth response state does not match the client request state.") + + # Request access and refresh tokens using the URI that you provided manually during program execution. + authorization_data.authentication.request_oauth_tokens_by_response_uri(response_uri=response_uri) + +def get_refresh_token(): + ''' + Returns a refresh token if stored locally. + ''' + file=None + try: + file=open("refresh.txt") + line=file.readline() + file.close() + return line if line else None + except IOError: + if file: + file.close() + return None + +def save_refresh_token(oauth_tokens): + ''' + Stores a refresh token locally. Be sure to save your refresh token securely. + ''' + with open("refresh.txt","w+") as file: + file.write(oauth_tokens.refresh_token) + file.close() + return None + +def search_accounts_by_user_id(user_id): + ''' + Search for account details by UserId. + + :param user_id: The Bing Ads user identifier. + :type user_id: long + :return: List of accounts that the user can manage. + :rtype: ArrayOfAccount + ''' + global customer_service + + paging={ + 'Index': 0, + 'Size': 10 + } + + predicates={ + 'Predicate': [ + { + 'Field': 'UserId', + 'Operator': 'Equals', + 'Value': user_id, + }, + ] + } + + search_accounts_request={ + 'PageInfo': paging, + 'Predicates': predicates + } + + return customer_service.SearchAccounts( + PageInfo = paging, + Predicates = predicates + ) + +def print_percent_complete(progress): + output_status_message("Percent Complete: {0}\n".format(progress.percent_complete)) + +def output_status_message(message): + print(message) + +def output_bulk_errors(errors): + for error in errors: + if error.error is not None: + output_status_message("Number: {0}".format(error.error)) + output_status_message("Error: {0}".format(error.number)) + if error.editorial_reason_code is not None: + output_status_message("EditorialTerm: {0}".format(error.editorial_term)) + output_status_message("EditorialReasonCode: {0}".format(error.editorial_reason_code)) + output_status_message("EditorialLocation: {0}".format(error.editorial_location)) + output_status_message("PublisherCountries: {0}".format(error.publisher_countries)) + output_status_message('') + +def output_bing_ads_webfault_error(error): + if hasattr(error, 'ErrorCode'): + output_status_message("ErrorCode: {0}".format(error.ErrorCode)) + if hasattr(error, 'Code'): + output_status_message("Code: {0}".format(error.Code)) + if hasattr(error, 'Message'): + output_status_message("Message: {0}".format(error.Message)) + output_status_message('') + +def output_webfault_errors(ex): + if hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'ApiFault') \ + and hasattr(ex.fault.detail.ApiFault, 'OperationErrors') \ + and hasattr(ex.fault.detail.ApiFault.OperationErrors, 'OperationError'): + api_errors=ex.fault.detail.ApiFault.OperationErrors.OperationError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'AdApiFaultDetail') \ + and hasattr(ex.fault.detail.AdApiFaultDetail, 'Errors') \ + and hasattr(ex.fault.detail.AdApiFaultDetail.Errors, 'AdApiError'): + api_errors=ex.fault.detail.AdApiFaultDetail.Errors.AdApiError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'ApiFaultDetail') \ + and hasattr(ex.fault.detail.ApiFaultDetail, 'BatchErrors') \ + and hasattr(ex.fault.detail.ApiFaultDetail.BatchErrors, 'BatchError'): + api_errors=ex.fault.detail.ApiFaultDetail.BatchErrors.BatchError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'ApiFaultDetail') \ + and hasattr(ex.fault.detail.ApiFaultDetail, 'OperationErrors') \ + and hasattr(ex.fault.detail.ApiFaultDetail.OperationErrors, 'OperationError'): + api_errors=ex.fault.detail.ApiFaultDetail.OperationErrors.OperationError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'EditorialApiFaultDetail') \ + and hasattr(ex.fault.detail.EditorialApiFaultDetail, 'BatchErrors') \ + and hasattr(ex.fault.detail.EditorialApiFaultDetail.BatchErrors, 'BatchError'): + api_errors=ex.fault.detail.EditorialApiFaultDetail.BatchErrors.BatchError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'EditorialApiFaultDetail') \ + and hasattr(ex.fault.detail.EditorialApiFaultDetail, 'EditorialErrors') \ + and hasattr(ex.fault.detail.EditorialApiFaultDetail.EditorialErrors, 'EditorialError'): + api_errors=ex.fault.detail.EditorialApiFaultDetail.EditorialErrors.EditorialError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'EditorialApiFaultDetail') \ + and hasattr(ex.fault.detail.EditorialApiFaultDetail, 'OperationErrors') \ + and hasattr(ex.fault.detail.EditorialApiFaultDetail.OperationErrors, 'OperationError'): + api_errors=ex.fault.detail.EditorialApiFaultDetail.OperationErrors.OperationError + if type(api_errors) == list: + for api_error in api_errors: + output_bing_ads_webfault_error(api_error) + else: + output_bing_ads_webfault_error(api_errors) + # Handle serialization errors e.g. The formatter threw an exception while trying to deserialize the message: + # There was an error while trying to deserialize parameter https://bingads.microsoft.com/CampaignManagement/v10:Entities. + elif hasattr(ex.fault, 'detail') \ + and hasattr(ex.fault.detail, 'ExceptionDetail'): + api_errors=ex.fault.detail.ExceptionDetail + if type(api_errors) == list: + for api_error in api_errors: + output_status_message(api_error.Message) + else: + output_status_message(api_errors.Message) + else: + raise Exception('Unknown WebFault') + +def output_bulk_keywords(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkKeyword: \n") + output_status_message("AdGroup Id: {0}".format(entity.ad_group_id)) + output_status_message("AdGroup Name: {0}".format(entity.ad_group_name)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("ClientId: {0}".format(entity.client_id)) + + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + output_bulk_performance_data(entity.performance_data) + output_bulk_quality_score_data(entity.quality_score_data) + output_bulk_bid_suggestions(entity.bid_suggestions) + + # Output the Campaign Management Keyword Object + output_keyword(entity.keyword) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_keyword(keyword): + if keyword is not None: + output_status_message("Bid.Amount: {0}".format( + keyword.Bid.Amount if keyword.Bid is not None else None) + ) + output_bidding_scheme(keyword.BiddingScheme) + output_status_message("DestinationUrl: {0}".format(keyword.DestinationUrl)) + output_status_message("EditorialStatus: {0}".format(keyword.EditorialStatus)) + output_status_message("FinalMobileUrls: ") + if keyword.FinalMobileUrls is not None: + for final_mobile_url in keyword.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if keyword.FinalUrls is not None: + for final_url in keyword.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("ForwardCompatibilityMap: ") + if keyword.ForwardCompatibilityMap is not None and len(keyword.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in text_ad.ForwardCompatibilityMap['KeyValuePairOfstringstring']: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(keyword.Id)) + output_status_message("MatchType: {0}".format(keyword.MatchType)) + output_status_message("Param1: {0}".format(keyword.Param1)) + output_status_message("Param2: {0}".format(keyword.Param2)) + output_status_message("Param3: {0}".format(keyword.Param3)) + output_status_message("Status: {0}".format(keyword.Status)) + output_status_message("Text: {0}".format(keyword.Text)) + output_status_message("TrackingUrlTemplate: {0}".format(keyword.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if keyword.UrlCustomParameters is not None and keyword.UrlCustomParameters.Parameters is not None: + for custom_parameter in keyword.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_bidding_scheme(bidding_scheme): + if bidding_scheme is None or type(bidding_scheme) == type(campaign_service.factory.create('ns0:BiddingScheme')): + return + elif type(bidding_scheme) == type(campaign_service.factory.create('ns0:EnhancedCpcBiddingScheme')): + output_status_message("BiddingScheme: EnhancedCpc") + elif type(bidding_scheme) == type(campaign_service.factory.create('ns0:InheritFromParentBiddingScheme')): + output_status_message("BiddingScheme: InheritFromParent") + elif type(bidding_scheme) == type(campaign_service.factory.create('ns0:MaxConversionsBiddingScheme')): + output_status_message("BiddingScheme: MaxConversions") + elif type(bidding_scheme) == type(campaign_service.factory.create('ns0:ManualCpcBiddingScheme')): + output_status_message("BiddingScheme: ManualCpc") + elif type(bidding_scheme) == type(campaign_service.factory.create('ns0:TargetCpaBiddingScheme')): + output_status_message("BiddingScheme: TargetCpa") + elif type(bidding_scheme) == type(campaign_service.factory.create('ns0:MaxClicksBiddingScheme')): + output_status_message("BiddingScheme: MaxClicks") + +def output_bulk_performance_data(performance_data): + if performance_data is not None: + output_status_message("AverageCostPerClick: {0}".format(performance_data.average_cost_per_click)) + output_status_message("AverageCostPerThousandImpressions: {0}".format(performance_data.average_cost_per_thousand_impressions)) + output_status_message("AveragePosition: {0}".format(performance_data.average_position)) + output_status_message("Clicks: {0}".format(performance_data.clicks)) + output_status_message("ClickThroughRate: {0}".format(performance_data.click_through_rate)) + output_status_message("Conversions: {0}".format(performance_data.conversions)) + output_status_message("CostPerConversion: {0}".format(performance_data.cost_per_conversion)) + output_status_message("Impressions: {0}".format(performance_data.impressions)) + output_status_message("Spend: {0}".format(performance_data.spend)) + +def output_bulk_quality_score_data(quality_score_data): + if quality_score_data is not None: + output_status_message("KeywordRelevance: {0}".format(quality_score_data.keyword_relevance)) + output_status_message("LandingPageRelevance: {0}".format(quality_score_data.landing_page_relevance)) + output_status_message("LandingPageUserExperience: {0}".format(quality_score_data._landing_page_user_experience)) + output_status_message("QualityScore: {0}".format(quality_score_data.quality_score)) + +def output_bulk_bid_suggestions(bid_suggestions): + if bid_suggestions is not None: + output_status_message("BestPosition: {0}".format(bid_suggestions.best_position)) + output_status_message("MainLine: {0}".format(bid_suggestions.main_line)) + output_status_message("FirstPage: {0}".format(bid_suggestions.first_page)) + +def write_entities_and_upload_file(upload_entities): + # Writes the specified entities to a local file and uploads the file. We could have uploaded directly + # without writing to file. This example writes to file as an exercise so that you can view the structure + # of the bulk records being uploaded as needed. + writer=BulkFileWriter(FILE_DIRECTORY+UPLOAD_FILE_NAME); + for entity in upload_entities: + writer.write_entity(entity) + writer.close() + + file_upload_parameters=FileUploadParameters( + result_file_directory=FILE_DIRECTORY, + compress_upload_file=True, + result_file_name=RESULT_FILE_NAME, + overwrite_result_file=True, + upload_file_path=FILE_DIRECTORY+UPLOAD_FILE_NAME, + response_mode='ErrorsAndResults' + ) + + bulk_file_path=bulk_service.upload_file(file_upload_parameters, progress=print_percent_complete) + + download_entities=[] + entities_generator=read_entities_from_bulk_file(file_path=bulk_file_path, result_file_type=ResultFileType.upload, file_format=FILE_FORMAT) + for entity in entities_generator: + download_entities.append(entity) + + return download_entities + +def download_file(download_parameters): + bulk_file_path=bulk_service.download_file(download_parameters, progress=print_percent_complete) + + download_entities=[] + entities_generator=read_entities_from_bulk_file(file_path=bulk_file_path, result_file_type=ResultFileType.full_download, file_format=FILE_FORMAT) + for entity in entities_generator: + download_entities.append(entity) + + return download_entities + +def read_entities_from_bulk_file(file_path, result_file_type, file_format): + with BulkFileReader(file_path=file_path, result_file_type=result_file_type, file_format=file_format) as reader: + for entity in reader: + yield entity + +# Main execution +if __name__ == '__main__': + + errors=[] + + try: + # You should authenticate for Bing Ads production services with a Microsoft Account, + # instead of providing the Bing Ads username and password set. + # Authentication with a Microsoft Account is currently not supported in Sandbox. + authenticate_with_oauth() + + # Uncomment to run with Bing Ads legacy UserName and Password credentials. + # For example you would use this method to authenticate in sandbox. + #authenticate_with_username() + + # Set to an empty user identifier to get the current authenticated Bing Ads user, + # and then search for all accounts the user may access. + user=customer_service.GetUser(None).User + accounts=search_accounts_by_user_id(user.Id) + + # For this example we'll use the first account. + authorization_data.account_id=accounts['Account'][0].Id + authorization_data.customer_id=accounts['Account'][0].ParentCustomerId + + download_parameters=DownloadParameters( + entities=['Keywords'], + result_file_directory=FILE_DIRECTORY, + result_file_name=DOWNLOAD_FILE_NAME, + overwrite_result_file=True, + last_sync_time_in_utc=None, + #campaign_ids=[OptionalCampaignId1GoesHere, OptionalCampaignId2GoesHere] + ) + + # Download all keywords across all ad groups. + download_entities=download_file(download_parameters) + output_status_message("Downloaded all keywords across all ad groups.\n"); + for entity in download_entities: + if isinstance(entity, BulkKeyword): + output_bulk_keywords([entity]) + + + upload_entities=[] + + # Within the downloaded records, find all keywords that have bids. + for entity in download_entities: + if isinstance(entity, BulkKeyword) \ + and entity.keyword is not None \ + and entity.keyword.Bid is not None: + # Increase all bids by some predetermined amount or percentage. + # Implement your own logic to update bids by varying amounts. + entity.keyword.Bid.Amount += 0.01 + upload_entities.append(entity) + + if len(upload_entities) > 0: + output_status_message("Changed local bid of keywords. Starting upload.\n") + + download_entities=write_entities_and_upload_file(upload_entities) + for entity in download_entities: + if isinstance(entity, BulkKeyword): + output_bulk_keywords([entity]) + else: + output_status_message("No keywords in account.\n") + + output_status_message("Program execution completed") + + except WebFault as ex: + output_webfault_errors(ex) + except Exception as ex: + output_status_message(ex) + diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py index f56e6ddd..7ec498e2 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py @@ -24,7 +24,7 @@ # If you are using OAuth in production, CLIENT_ID is required and CLIENT_STATE is recommended. CLIENT_ID='ClientIdGoesHere' CLIENT_STATE='ClientStateGoesHere' - + authorization_data=AuthorizationData( account_id=None, customer_id=None, @@ -686,8 +686,7 @@ def output_keyword_results(keywords, keyword_ids, keyword_errors): } output_keyword_results(keywords, keyword_ids, keyword_errors) - - + # Here is a simple example that updates the campaign budget. # If the campaign has a shared budget you cannot update the Campaign budget amount, # and you must instead update the amount in the Budget object. If you try to update