diff --git a/bingads/authorization.py b/bingads/authorization.py index 013e8937..4f3ef547 100644 --- a/bingads/authorization.py +++ b/bingads/authorization.py @@ -1,8 +1,8 @@ try: from urllib.parse import parse_qs, urlparse, quote_plus except ImportError: - from urlparse import parse_qs, urlparse - from urllib import quote_plus + from urllib.parse import parse_qs, urlparse + from urllib.parse import quote_plus import json import requests diff --git a/bingads/authorization.py.bak b/bingads/authorization.py.bak new file mode 100644 index 00000000..013e8937 --- /dev/null +++ b/bingads/authorization.py.bak @@ -0,0 +1,617 @@ +try: + from urllib.parse import parse_qs, urlparse, quote_plus +except ImportError: + from urlparse import parse_qs, urlparse + from urllib import quote_plus +import json + +import requests + +from .exceptions import OAuthTokenRequestException + + +class AuthorizationData: + """ Represents a user who intends to access the corresponding customer and account. + + An instance of this class is required to authenticate with Bing Ads if you are using either + :class:`.ServiceClient` or :class:`.BulkServiceManager`. + """ + + def __init__(self, + account_id=None, + customer_id=None, + developer_token=None, + authentication=None): + """ Initialize an instance of this class. + + :param account_id: The identifier of the account that owns the entities in the request. + Used as the CustomerAccountId header and the AccountId body elements + in calls to the Bing Ads web services. + :type account_id: int + :param customer_id: The identifier of the customer that owns the account. + Used as the CustomerId header element in calls to the Bing Ads web services. + :type customer_id: int + :param developer_token: The Bing Ads developer access token. + Used as the DeveloperToken header element in calls to the Bing Ads web services. + :type developer_token: str + :param authentication: An object representing the authentication method that should be used in calls + to the Bing Ads web services. + :type authentication: Authentication + """ + + self._account_id = account_id + self._customer_id = customer_id + self._developer_token = developer_token + self._authentication = authentication + + @property + def account_id(self): + """ The identifier of the account that owns the entities in the request. + + Used as the CustomerAccountId header and the AccountId body elements in calls to the Bing Ads web services. + + :rtype: int + """ + + return self._account_id + + @property + def customer_id(self): + """ The identifier of the customer that owns the account. + + Used as the CustomerId header element in calls to the Bing Ads web services. + + :rtype: int + """ + + return self._customer_id + + @property + def developer_token(self): + """ The Bing Ads developer access token. + + Used as the DeveloperToken header element in calls to the Bing Ads web services. + + :rtype: str + """ + + return self._developer_token + + @property + def authentication(self): + """ An object representing the authentication method that should be used in calls to the Bing Ads web services. + + *See also:* + + * :class:`.OAuthDesktopMobileAuthCodeGrant` + * :class:`.OAuthDesktopMobileImplicitGrant` + * :class:`.OAuthWebAuthCodeGrant` + * :class:`.PasswordAuthentication` + + :rtype: Authentication + """ + + return self._authentication + + @account_id.setter + def account_id(self, account_id): + self._account_id = account_id + + @customer_id.setter + def customer_id(self, customer_id): + self._customer_id = customer_id + + @developer_token.setter + def developer_token(self, developer_token): + self._developer_token = developer_token + + @authentication.setter + def authentication(self, authentication): + self._authentication = authentication + + +class Authentication(object): + """ The base class for all authentication classes. + + *See also:* + + * :class:`.ServiceClient` + * :class:`.BulkServiceManager` + * :class:`.AuthorizationData` + """ + + def enrich_headers(self, headers): + """ Sets the required header elements for the corresponding Bing Ads service or bulk file upload operation. + + The header elements that the method sets will differ depending on the type of authentication. + For example if you use one of the OAuth classes, the AuthenticationToken header will be set by this method, + whereas the UserName and Password headers will remain empty. + + :param headers: Bing Ads service or bulk file upload operation headers. + :type headers: dict + :rtype: None + """ + + raise NotImplementedError() + + +class PasswordAuthentication(Authentication): + """ Represents a legacy Bing Ads authentication method using user name and password. + + You can use an instance of this class as the authentication property of a :class:`.AuthorizationData` object to + authenticate with Bing Ads services. + Existing users with legacy Bing Ads credentials may continue to specify the UserName and Password header elements. + In future versions of the API, Bing Ads will transition exclusively to Microsoft Account authentication. + New customers are required to sign up for Bing Ads with a Microsoft Account, and to manage those accounts you must + use OAuth. + For example instead of using this :class:`.PasswordAuthentication` class, you would authenticate with an instance + of either :class:`.OAuthDesktopMobileAuthCodeGrant`, :class:`.OAuthDesktopMobileImplicitGrant`, + or :class:`.OAuthWebAuthCodeGrant`. + """ + + def __init__(self, user_name, password): + """ Initializes a new instance of this class using the specified user name and password. + + :param user_name: The Bing Ads user's sign-in user name. You may not set this element to a Microsoft account. + :type user_name: str + :param password: The Bing Ads user's sign-in password. + :type password: str + """ + + self._user_name = user_name + self._password = password + + @property + def user_name(self): + """ The Bing Ads user's sign-in user name. You may not set this element to a Microsoft account. + + :rtype: str + """ + + return self._user_name + + @property + def password(self): + """ The Bing Ads user's sign-in password. + + :rtype: str + """ + + return self._password + + def enrich_headers(self, headers): + """ Sets the user name and password as headers elements for Bing Ads service or bulk file upload operation. """ + + headers['UserName'] = self.user_name + headers['Password'] = self.password + + +class OAuthTokens: + """ Contains information about OAuth access tokens received from the Microsoft Account authorization service. + + You can get OAuthTokens using the RequestAccessAndRefreshTokens method of RequestAccessAndRefreshTokens method of + either the :class:`.OAuthDesktopMobileAuthCodeGrant` or :class:`.OAuthWebAuthCodeGrant` classes. + """ + + def __init__(self, access_token=None, access_token_expires_in_seconds=None, refresh_token=None): + """ Initialize an instance of this class. + + :param access_token: OAuth access token that will be used for authorization in the Bing Ads services. + :type access_token: (optional) str or None + :param access_token_expires_in_seconds: (optional) The access token expiration time in seconds. + :type access_token_expires_in_seconds: int or None + :param refresh_token: (optional) OAuth refresh token that can be user to refresh an access token. + :type refresh_token: str or None + """ + + self._access_token = access_token + self._access_token_expires_in_seconds = access_token_expires_in_seconds + self._refresh_token = refresh_token + + @property + def access_token(self): + """ OAuth access token that will be used for authorization in the Bing Ads services. + + :rtype: str + """ + + return self._access_token + + @property + def access_token_expires_in_seconds(self): + """ Expiration time for the corresponding access token in seconds. + + :rtype: int + """ + + return self._access_token_expires_in_seconds + + @property + def refresh_token(self): + """ OAuth refresh token that can be user to refresh an access token. + + :rtype: str + """ + + return self._refresh_token + + +class OAuthAuthorization(Authentication): + """ The abstract base class for all OAuth authentication classes. + + You can use this class to dynamically instantiate a derived OAuth authentication class at run time. + This class cannot be instantiated, and instead you should use either :class:`.OAuthDesktopMobileAuthCodeGrant`, + :class:`.OAuthDesktopMobileImplicitGrant`, :class:`.OAuthWebAuthCodeGrant`, which extend this class. + + *See also:* + + * :class:`.OAuthDesktopMobileAuthCodeGrant` + * :class:`.OAuthDesktopMobileImplicitGrant` + * :class:`.OAuthWebAuthCodeGrant` + """ + + def __init__(self, client_id, oauth_tokens=None): + """ Initializes a new instance of the OAuthAuthorization class. + + :param client_id: The client identifier corresponding to your registered application. + :type client_id: str + :param oauth_tokens: Contains information about OAuth access tokens received from the Microsoft Account authorization service + :type oauth_tokens: OAuthTokens + :rtype: str + """ + + if client_id is None: + raise ValueError('Client id cannot be None.') + self._client_id = client_id + self._oauth_tokens = oauth_tokens + self._state = None + + @property + def client_id(self): + """ The client identifier corresponding to your registered application. + + For more information about using a client identifier for authentication, see the + Client Password Authentication section of the OAuth 2.0 spec at + http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-3.1 + + :rtype: str + """ + + return self._client_id + + @property + def oauth_tokens(self): + """ Contains information about OAuth access tokens received from the Microsoft Account authorization service. + + :rtype: OAuthTokens + """ + + return self._oauth_tokens + + @property + def state(self): + """ An opaque value used by the client to maintain state between the request and callback + :rtype: str + """ + + return self._state + + @state.setter + def state(self, value): + """ An opaque value used by the client to maintain state between the request and callback + :rtype: str + """ + self._state = value + + @property + def redirection_uri(self): + """ The URI to which the user of the app will be redirected after receiving user consent. + + :rtype: str + """ + + raise NotImplementedError() + + def get_authorization_endpoint(self): + """ Gets the Microsoft Account authorization endpoint where the user should be navigated to give his or her consent. + + :return: The Microsoft Account authorization endpoint. + :rtype: str + """ + + raise NotImplementedError() + + def enrich_headers(self, headers): + """ Sets the AuthenticationToken headers elements for Bing Ads service or bulk file upload operation. """ + + if self.oauth_tokens is None: + raise NotImplementedError("OAuth access token hasn't been requested.") + headers['AuthenticationToken'] = self.oauth_tokens.access_token + + +class OAuthWithAuthorizationCode(OAuthAuthorization): + """ Represents a proxy to the Microsoft account authorization service. + + Implement an extension of this class in compliance with the authorization code grant flow for Managing User + Authentication with OAuth documented at http://go.microsoft.com/fwlink/?LinkID=511609. This is a standard OAuth 2.0 + flow and is defined in detail in the Authorization Code Grant section of the OAuth 2.0 spec at + http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1. + For more information about registering a Bing Ads application, see http://go.microsoft.com/fwlink/?LinkID=511607. + """ + + def __init__(self, client_id, client_secret, redirection_uri, token_refreshed_callback=None, oauth_tokens=None): + """ Initialize a new instance of this class. + + :param client_id: The client identifier corresponding to your registered application. + :type client_id: str + :param client_secret: The client secret corresponding to your registered application, or None if your app is a + desktop or mobile app. + :type client_secret: str or None + :param redirection_uri: The URI to which the user of the app will be redirected after receiving user consent. + :type redirection_uri: str + :param token_refreshed_callback: (optional) Call back function when oauth_tokens be refreshed. + :type token_refreshed_callback: (OAuthTokens)->None or None + :param oauth_tokens: Contains information about OAuth access tokens received from the Microsoft Account authorization service + :type oauth_tokens: OAuthTokens + :return: + """ + + super(OAuthWithAuthorizationCode, self).__init__(client_id, oauth_tokens=oauth_tokens) + self._client_secret = client_secret + self._redirection_uri = redirection_uri + self._token_refreshed_callback = token_refreshed_callback + + def get_authorization_endpoint(self): + """ Gets the Microsoft Account authorization endpoint where the user should be navigated to give his or her consent. + + :return: The Microsoft Account authorization endpoint. + :rtype: str + """ + endpoint = str.format( + 'https://login.live.com/oauth20_authorize.srf?client_id={0}&scope=bingads.manage&response_type={1}&redirect_uri={2}', + self._client_id, + 'code', + quote_plus(self._redirection_uri) + ) + return endpoint if self.state is None else endpoint + '&state=' + self.state + + def request_oauth_tokens_by_response_uri(self, response_uri): + """ Retrieves OAuth access and refresh tokens from the Microsoft Account authorization service. + + Using the specified authorization response redirection uri. + For more information, see the Authorization Response section in the OAuth 2.0 spec + at http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1.2. + + :param response_uri: The response redirection uri. + :type response_uri: str + :return: OAuth tokens + :rtype: OAuthTokens + """ + + parameters = parse_qs(urlparse(response_uri).query) + if 'code' not in parameters or len(parameters['code']) == 0: + raise ValueError( + "Uri passed doesn't contain code param. " + "Please make sure the uri has a code in it, for example http://myurl.com?code=123" + ) + code = parameters['code'][0] + + self._oauth_tokens = _LiveComOAuthService.get_access_token( + client_id=self.client_id, + client_secret=self.client_secret, + redirect_uri=self.redirection_uri, + grant_type='authorization_code', + code=code, + ) + if self.token_refreshed_callback is not None: + self.token_refreshed_callback(self.oauth_tokens) # invoke the callback when token refreshed. + return self.oauth_tokens + + def request_oauth_tokens_by_refresh_token(self, refresh_token): + """ Retrieves OAuth access and refresh tokens from the Microsoft Account authorization service. + + Using the specified refresh token. + For more information, see the Refreshing an Access Token section in the OAuth 2.0 spec + at http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-6. + + :param refresh_token: The refresh token used to request new access and refresh tokens. + :type refresh_token: str + :return: OAuth tokens + :rtype: OAuthTokens + """ + + self._oauth_tokens = _LiveComOAuthService.get_access_token( + client_id=self.client_id, + client_secret=self.client_secret, + redirect_uri=self.redirection_uri, + grant_type='refresh_token', + refresh_token=refresh_token, + ) + if self.token_refreshed_callback is not None: + self.token_refreshed_callback(self.oauth_tokens) # invoke the callback when token refreshed. + + return self.oauth_tokens + + @property + def client_secret(self): + """ The client secret corresponding to your registered application, or None if your app is a desktop or mobile app. + + :rtype: str + """ + + return self._client_secret + + @property + def redirection_uri(self): + """ The URI to which your client browser will be redirected after receiving user consent. + + :rtype: str + """ + + return self._redirection_uri + + @property + def token_refreshed_callback(self): + """ The callback function registered, will be invoked after oauth tokens has been refreshed. + + :rtype: OAuthTokens->None + """ + + return self._token_refreshed_callback + + @client_secret.setter + def client_secret(self, client_secret): + self._client_secret = client_secret + + @redirection_uri.setter + def redirection_uri(self, redirection_uri): + self._redirection_uri = redirection_uri + + @token_refreshed_callback.setter + def token_refreshed_callback(self, token_refreshed_callback): + self._token_refreshed_callback = token_refreshed_callback + + +class OAuthDesktopMobileAuthCodeGrant(OAuthWithAuthorizationCode): + """ Represents an OAuth authorization object implementing the authorization code grant flow for use in a desktop + or mobile application. + + You can use an instance of this class as the AuthorizationData.Authentication property + of an :class:`.AuthorizationData` object to authenticate with Bing Ads services. + In this case the AuthenticationToken request header will be set to the corresponding OAuthTokens.AccessToken value. + + This class implements the authorization code grant flow for Managing User Authentication with OAuth + documented at http://go.microsoft.com/fwlink/?LinkID=511609. This is a standard OAuth 2.0 flow and is defined in detail in the + Authorization Code Grant section of the OAuth 2.0 spec at http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1. + For more information about registering a Bing Ads application, see http://go.microsoft.com/fwlink/?LinkID=511607. + """ + + def __init__(self, client_id, oauth_tokens=None): + """ Initializes a new instance of the this class with the specified client id. + + :param client_id: The client identifier corresponding to your registered application. + :type client_id: str + :param oauth_tokens: Contains information about OAuth access tokens received from the Microsoft Account authorization service + :type oauth_tokens: OAuthTokens + """ + + super(OAuthDesktopMobileAuthCodeGrant, self).__init__( + client_id, + None, + _LiveComOAuthService.DESKTOP_REDIRECTION_URI, + oauth_tokens=oauth_tokens, + ) + + +class OAuthWebAuthCodeGrant(OAuthWithAuthorizationCode): + """ Represents an OAuth authorization object implementing the authorization code grant flow for use in a web application. + + You can use an instance of this class as the AuthorizationData.Authentication property + of an :class:`.AuthorizationData` object to authenticate with Bing Ads services. + In this case the AuthenticationToken request header will be set to the corresponding OAuthTokens.AccessToken value. + + This class implements the authorization code grant flow for Managing User Authentication with OAuth + documented at http://go.microsoft.com/fwlink/?LinkID=511609. This is a standard OAuth 2.0 flow and is defined in detail in the + Authorization Code Grant section of the OAuth 2.0 spec at http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1. + For more information about registering a Bing Ads application, see http://go.microsoft.com/fwlink/?LinkID=511607. + """ + + pass + + +class OAuthDesktopMobileImplicitGrant(OAuthAuthorization): + """ Represents an OAuth authorization object implementing the implicit grant flow for use in a desktop or mobile application. + + You can use an instance of this class as the AuthorizationData.Authentication property + of an :class:`.AuthorizationData` object to authenticate with Bing Ads services. + In this case the AuthenticationToken request header will be set to the corresponding OAuthTokens.AccessToken value. + + This class implements the implicit grant flow for Managing User Authentication with OAuth + documented at http://go.microsoft.com/fwlink/?LinkID=511608. This is a standard OAuth 2.0 flow and is defined in detail in the + Authorization Code Grant section of the OAuth 2.0 spec at http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1. + For more information about registering a Bing Ads application, see http://go.microsoft.com/fwlink/?LinkID=511607. + """ + + def __init__(self, client_id, oauth_tokens=None): + """ Initializes a new instance of the this class with the specified client id. + + :param client_id: The client identifier corresponding to your registered application. + :type client_id: str + :param oauth_tokens: Contains information about OAuth access tokens received from the Microsoft Account authorization service + :type oauth_tokens: OAuthTokens + """ + + super(OAuthDesktopMobileImplicitGrant, self).__init__(client_id, oauth_tokens=oauth_tokens) + + def get_authorization_endpoint(self): + """ Gets the Microsoft Account authorization endpoint where the user should be navigated to give his or her consent. + + :return: The Microsoft Account authorization endpoint. + :rtype: str + """ + + endpoint = str.format( + 'https://login.live.com/oauth20_authorize.srf?client_id={0}&scope=bingads.manage&response_type={1}&redirect_uri={2}', + self.client_id, + 'token', + _LiveComOAuthService.DESKTOP_REDIRECTION_URI, + ) + return endpoint if self.state is None else endpoint + '&state=' + self.state + + def extract_access_token_from_uri(self, redirection_uri): + """ Extracts the access token from the specified redirect URI. + + :param redirection_uri: The redirect URI that contains an access token. + :type redirection_uri: str + :return: The :class:`.OAuthTokens` object which contains both the access_token and access_token_expires_in_seconds properties. + :rtype: OAuthTokens + """ + + parameters = parse_qs(urlparse(redirection_uri).fragment) + if 'access_token' not in parameters or len(parameters['access_token']) == 0: + raise ValueError(str.format("Input URI: {0} doesn't contain access_token parameter", redirection_uri)) + access_token = parameters['access_token'][0] + if 'expires_in' not in parameters or len(parameters['expires_in']) == 0: + expires_in = None + else: + expires_in = parameters['expires_in'][0] + self._oauth_tokens = OAuthTokens( + access_token, + int(expires_in) if expires_in is not None else None + ) + return self.oauth_tokens + + @property + def redirection_uri(self): + return _LiveComOAuthService.DESKTOP_REDIRECTION_URI + + +class _LiveComOAuthService: + """ Provides method for getting OAuth tokens from the live.com authorization server.""" + + def __init__(self): + pass + + DESKTOP_REDIRECTION_URI = 'https://login.live.com/oauth20_desktop.srf' + + @staticmethod + def get_access_token(**kwargs): + """ Calls live.com authorization server with parameters passed in, deserializes the response and returns back OAuth tokens. + + :param kwargs: OAuth parameters for authorization server call. + :return: OAuth tokens. + :rtype: OAuthTokens + """ + + if 'client_secret' in kwargs and kwargs['client_secret'] is None: + del kwargs['client_secret'] + + r = requests.post('https://login.live.com/oauth20_token.srf', kwargs, verify=True) + try: + r.raise_for_status() + except Exception: + error_json = json.loads(r.text) + raise OAuthTokenRequestException(error_json['error'], error_json['error_description']) + + r_json = json.loads(r.text) + return OAuthTokens(r_json['access_token'], int(r_json['expires_in']), r_json['refresh_token']) diff --git a/bingads/internal/extensions.py b/bingads/internal/extensions.py index ebcad040..fc7592eb 100644 --- a/bingads/internal/extensions.py +++ b/bingads/internal/extensions.py @@ -45,7 +45,7 @@ def bulk_str(value): if isinstance(value, str): return value if PY2: - if isinstance(value, unicode): + if isinstance(value, str): return value return str(value) diff --git a/bingads/internal/extensions.py.bak b/bingads/internal/extensions.py.bak new file mode 100644 index 00000000..ebcad040 --- /dev/null +++ b/bingads/internal/extensions.py.bak @@ -0,0 +1,1237 @@ +from datetime import datetime + +from bingads.v10.internal.bulk.string_table import _StringTable +from six import PY2 +import re +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V10, _CAMPAIGN_MANAGEMENT_SERVICE_V10, _CAMPAIGN_OBJECT_FACTORY_V11, _CAMPAIGN_MANAGEMENT_SERVICE_V11 + + +DELETE_VALUE = "delete_value" +_BULK_DATETIME_FORMAT = '%m/%d/%Y %H:%M:%S' +_BULK_DATETIME_FORMAT_2 = '%m/%d/%Y %H:%M:%S.%f' +_BULK_DATE_FORMAT = "%m/%d/%Y" + +url_splitter = ";\\s*(?=https?://)" +custom_param_splitter = "(? 0: + entity.UrlCustomParameters.Parameters.CustomParameter = params + + +def csv_to_field_Urls(entity, value): + """ + set FinalUrls / FinalMobileUrls string field + :param entity: FinalUrls / FinalMobileUrls + :param value: the content in csv + :return:set field values + """ + if value is None or value == '': + return + splitter = re.compile(url_splitter) + entity.string = splitter.split(value) + + +def field_to_csv_Urls(entity): + """ + parse entity to csv content + :param entity: FinalUrls / FinalMobileUrls + :return: csv content + """ + if entity is None: + return None + if entity.string is None: + return DELETE_VALUE + if len(entity.string) == 0: + return None + return '; '.join(entity.string) + + +def field_to_csv_BidStrategyType(entity): + """ + parse entity to csv content + :param entity: entity which has BiddingScheme attribute + :return: csv content + """ + if entity.BiddingScheme is None or type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:BiddingScheme')): + return None + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:EnhancedCpcBiddingScheme')): + return 'EnhancedCpc' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:InheritFromParentBiddingScheme')): + return 'InheritFromParent' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:MaxConversionsBiddingScheme')): + return 'MaxConversions' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:ManualCpcBiddingScheme')): + return 'ManualCpc' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:TargetCpaBiddingScheme')): + return 'TargetCpa' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:MaxClicksBiddingScheme')): + return 'MaxClicks' + else: + raise TypeError('Unsupported Bid Strategy Type') + + +def csv_to_field_BidStrategyType(entity, value): + """ + set BiddingScheme + :param entity: entity which has BiddingScheme attribute + :param value: the content in csv + :return: + """ + if value is None or value == '': + return + elif value == 'EnhancedCpc': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:EnhancedCpcBiddingScheme') + elif value == 'InheritFromParent': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:InheritFromParentBiddingScheme') + elif value == 'MaxConversions': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:MaxConversionsBiddingScheme') + elif value == 'ManualCpc': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:ManualCpcBiddingScheme') + elif value == 'TargetCpa': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:TargetCpaBiddingScheme') + elif value == 'MaxClicks': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:MaxClicksBiddingScheme') + else: + raise ValueError('Unknown Bid Strategy Type') + entity.BiddingScheme.Type = value + + +def field_to_csv_AdFormatPreference(entity): + """ + convert entity field to csv content + :param entity: entity which has ForwardCompatibilityMap attribute + :return: + """ + if entity.ForwardCompatibilityMap is None or entity.ForwardCompatibilityMap.KeyValuePairOfstringstring is None \ + or len(entity.ForwardCompatibilityMap.KeyValuePairOfstringstring) == 0: + return None + for key_value_pair in entity.ForwardCompatibilityMap.KeyValuePairOfstringstring: + if key_value_pair.key == 'NativePreference': + if key_value_pair.value.lower() == 'true': + return 'Native' + elif key_value_pair.value.lower() == 'false': + return 'All' + else: + raise ValueError('Unknown value for Native Preference: {0}'.format(key_value_pair.value)) + return None + + +def csv_to_field_AdFormatPreference(entity, value): + """ + parse csv content and set entity attribute + :param entity: entity which has ForwardCompatibilityMap attribute + :param value: csv content value + :return: + """ + ad_format_preference = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns2:KeyValuePairOfstringstring') + ad_format_preference.key = 'NativePreference' + if value is None or value == '' or value == 'All': + ad_format_preference.value = 'False' + elif value == 'Native': + ad_format_preference.value = 'True' + else: + raise ValueError('Unknown value for Native Preference: {0}'.format(value)) + entity.ForwardCompatibilityMap = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns2:ArrayOfKeyValuePairOfstringstring') + entity.ForwardCompatibilityMap.KeyValuePairOfstringstring.append(ad_format_preference) + + +def csv_to_field_StructuredSnippetValues(entity, value): + if value is not None and value != '': + entity.Values.string = value.split(';') + +def field_to_csv_StructuredSnippetValues(entity): + if entity.Values is not None and entity.Values.string is not None and len(entity.Values.string) > 0: + return ';'.join(entity.Values.string) + return None + + +def ad_rotation_bulk_str(value): + if value is None: + return None + elif value.Type is None: + return DELETE_VALUE + else: + return bulk_str(value.Type) + + +def parse_ad_rotation(value): + if not value: + return None + ad_rotation = _CAMPAIGN_OBJECT_FACTORY_V10.create('AdRotation') + ad_rotation.Type = None if value == DELETE_VALUE else value + return ad_rotation + + +def parse_ad_group_bid(value): + if not value: + return None + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('Bid') + bid.Amount = float(value) + return bid + + +def ad_group_bid_bulk_str(value): + if value is None or value.Amount is None: + return None + return bulk_str(value.Amount) + + +def keyword_bid_bulk_str(value): + if value is None: + return DELETE_VALUE + if value.Amount is None: + return None + return bulk_str(value.Amount) + + +def parse_keyword_bid(value): + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('Bid') + if not value: + bid.Amount = None + else: + bid.Amount = float(value) + return bid + + +def minute_bulk_str(value): + if value == 'Zero': + return '0' + elif value == 'Fifteen': + return '15' + elif value == 'Thirty': + return '30' + elif value == 'FortyFive': + return '45' + else: + raise ValueError('Unknown minute') + + +def parse_minute(value): + minute_number = int(value) + if minute_number == 0: + return 'Zero' + elif minute_number == 15: + return 'Fifteen' + elif minute_number == 30: + return 'Thirty' + elif minute_number == 45: + return 'FortyFive' + raise ValueError('Unknown minute') + + +def format_Day(value): + Day = _CAMPAIGN_OBJECT_FACTORY_V10.create('Day') + if value.lower() == 'monday': + return Day.Monday + elif value.lower() == 'tuesday': + return Day.Tuesday + elif value.lower() == 'wednesday': + return Day.Wednesday + elif value.lower() == 'thursday': + return Day.Thursday + elif value.lower() == 'friday': + return Day.Friday + elif value.lower() == 'saturday': + return Day.Saturday + elif value.lower() == 'sunday': + return Day.Sunday + raise ValueError('Unable to parse day: {0}'.format(value)) + +def parse_location_target_type(value): + if value == 'Metro Area': + return 'MetroArea' + elif value == 'Postal Code': + return 'PostalCode' + else: + return value + + +def location_target_type_bulk_str(value): + if value == 'MetroArea': + return 'Metro Area' + elif value == 'PostalCode': + return 'Postal Code' + else: + return value + + +def field_to_csv_AdSchedule(entity): + """ + get the bulk string for Scheduling DayTimeRanges + :param entity: Scheduling entity + :return: bulk str + """ + if entity is None: + return None + if entity.DayTimeRanges is None: + return DELETE_VALUE + return ';'.join('({0}[{1:02d}:{2:02d}-{3:02d}:{4:02d}])' + .format(d.Day, d.StartHour, int(minute_bulk_str(d.StartMinute)), d.EndHour, int(minute_bulk_str(d.EndMinute))) + for d in entity.DayTimeRanges.DayTime + ) + + +def csv_to_field_AdSchedule(entity, value): + if value is None or value.strip() == '': + return + daytime_strs = value.split(';') + ad_schedule_pattern = '\((Monday|Tuesday|Wednesday|ThursDay|Friday|Saturday|Sunday)\[(\d\d?):(\d\d)-(\d\d?):(\d\d)\]\)' + pattern = re.compile(ad_schedule_pattern, re.IGNORECASE) + daytimes = [] + for daytime_str in daytime_strs: + match = pattern.match(daytime_str) + if match: + daytime = _CAMPAIGN_OBJECT_FACTORY_V10.create('DayTime') + daytime.Day = format_Day(match.group(1)) + daytime.StartHour = int(match.group(2)) + daytime.StartMinute = parse_minute(match.group(3)) + daytime.EndHour = int(match.group(4)) + daytime.EndMinute = parse_minute(match.group(5)) + daytimes.append(daytime) + else: + raise ValueError('Unable to parse DayTime: {0}'.format(daytime_str)) + entity.DayTimeRanges.DayTime = daytimes + + +def field_to_csv_SchedulingStartDate(entity): + """ + write scheduling StartDate to bulk string + :param entity: Scheduling entity + :return: date bulk string + """ + if entity is None: + return None + elif entity.StartDate is None: + return DELETE_VALUE + # this case is what the suds creates by default. return None instead of a delete value + elif entity.StartDate.Day is None and entity.StartDate.Month is None and entity.StartDate.Year is None: + return None + return '{0!s}/{1!s}/{2!s}'.format(entity.StartDate.Month, entity.StartDate.Day, entity.StartDate.Year) + + +def field_to_csv_SchedulingEndDate(entity): + """ + write scheduling EndDate to bulk string + :param entity: Scheduling entity + :return: date bulk string + """ + if entity is None: + return None + elif entity.EndDate is None: + return DELETE_VALUE + # this case is what the suds creates by default. return None instead of a delete value + elif entity.EndDate.Day is None and entity.EndDate.Month is None and entity.EndDate.Year is None: + return None + return '{0!s}/{1!s}/{2!s}'.format(entity.EndDate.Month, entity.EndDate.Day, entity.EndDate.Year) + + +def field_to_csv_UseSearcherTimeZone(entity): + """ + get Scheduling UseSearcherTimeZone bulk str + :param entity: Scheduling entity + :return: bulk str + """ + if entity is None: + return None + # this case is what suds creates by default, while set it to delete value since there's no other case for delete value + elif entity.UseSearcherTimeZone is None: + return DELETE_VALUE + else: + return str(entity.UseSearcherTimeZone) + + +def csv_to_field_BudgetType(entity, value, version=10): + if value is None or value == '': + entity.BudgetType = None + elif value == 'MonthlyBudgetSpendUntilDepleted' and version == 10: + entity.BudgetType = BudgetLimitType.MonthlyBudgetSpendUntilDepleted + elif value == 'DailyBudgetAccelerated': + entity.BudgetType = BudgetLimitType.DailyBudgetAccelerated + elif value == 'DailyBudgetStandard': + entity.BudgetType = BudgetLimitType.DailyBudgetStandard + else: + raise ValueError('Unable to parse BudgetType: {0}'.format(value)) + + +def csv_to_field_DSAWebsite(entity, value): + """ + Set Campaign settings Domain Name from bulk value if the campaign type is Dynamic Search Campaign + :param entity: campaign entity + :param value: bulk str value + """ + if not entity.CampaignType or len(entity.CampaignType) == 0 or entity.CampaignType[0] != "DynamicSearchAds": + return + if len(entity.Settings.Setting) > 0 and entity.Settings.Setting[0].Type == 'DynamicSearchAdsSetting': + entity.Settings.Setting[0].DomainName = value + else: + setting = _CAMPAIGN_OBJECT_FACTORY_V10.create('DynamicSearchAdsSetting') + setting.DomainName = value + setting.Type = 'DynamicSearchAdsSetting' + entity.Settings.Setting.append(setting) + + +def field_to_csv_DSAWebsite(entity): + """ + convert campaign settings Domain Name to bulk str if the campaign is Dynamic Search Campaign + :param entity: campaign entity + :return: bulk str + """ + if entity.CampaignType is not None and (entity.CampaignType == 'DynamicSearchAds' or ( + len(entity.CampaignType) != 0 and entity.CampaignType[0] == 'DynamicSearchAds')): + if entity.Settings is None or entity.Settings.Setting is None or len(entity.Settings.Setting) == 0: + return None + setting = entity.Settings.Setting[0] + if isinstance(setting, type(DynamicSearchAdsSetting)): + return setting.DomainName + return None + + +def csv_to_field_DSADomainLanguage(entity, value): + """ + Set Campaign settings Language from bulk value if the campaign type is Dynamic Search Campaign + :param entity: campaign entity + :param value: bulk str value + """ + if not entity.CampaignType or len(entity.CampaignType) == 0 or entity.CampaignType[0] != "DynamicSearchAds": + return + if len(entity.Settings.Setting) > 0 and entity.Settings.Setting[0].Type == 'DynamicSearchAdsSetting': + entity.Settings.Setting[0].Language = value + else: + setting = _CAMPAIGN_OBJECT_FACTORY_V10.create('DynamicSearchAdsSetting') + setting.Language = value + setting.Type = 'DynamicSearchAdsSetting' + entity.Settings.Setting.append(setting) + + +def field_to_csv_DSADomainLanguage(entity): + """ + convert campaign settings Language to bulk str if the campaign is Dynamic Search Campaign + :param entity: campaign entity + :return: bulk str + """ + if entity.CampaignType is not None and (entity.CampaignType == 'DynamicSearchAds' or ( + len(entity.CampaignType) != 0 and entity.CampaignType[0] == 'DynamicSearchAds')): + if not entity.Settings or not entity.Settings.Setting or len(entity.Settings.Setting) == 0: + return None + setting = entity.Settings.Setting[0] + if isinstance(setting, type(DynamicSearchAdsSetting)): + return setting.Language + + return None + + +def field_to_csv_WebpageParameter_CriterionName(entity): + if entity.Criterion is None or entity.Criterion.Parameter is None or entity.Criterion.Parameter.CriterionName is None: + return None + if not entity.Criterion.Parameter.CriterionName: + return DELETE_VALUE + return entity.Criterion.Parameter.CriterionName + + +def csv_to_field_WebpageParameter_CriterionName(entity, value): + if value is None or value == '': + return + if entity.Criterion is not None and isinstance(entity.Criterion, type(Webpage)): + entity.Criterion.Parameter.CriterionName = value + else: + webpage = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:Webpage') + webpage.Parameter.CriterionName = value + entity.Criterion = webpage + + +def entity_to_csv_DSAWebpageParameter(entity, row_values): + """ + Set Campaign/AdGroup Criterion (WebpagePage) Web page parameters from bulk values + :param entity: campaign/ad group criterion entity + :param row_values: bulk row values + """ + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion, type(Webpage)) and \ + entity.Criterion.Parameter is not None and entity.Criterion.Parameter.Conditions is not None and \ + entity.Criterion.Parameter.Conditions.WebpageCondition is not None: + condition_prefix = _StringTable.DynamicAdTargetCondition1[:-1] + value_prefix = _StringTable.DynamicAdTargetValue1[:-1] + + conditions = entity.Criterion.Parameter.Conditions.WebpageCondition + for i in range(0, len(conditions)): + row_values[condition_prefix + str(i + 1)] = conditions[i].Operand + row_values[value_prefix + str(i + 1)] = conditions[i].Argument + + +def csv_to_entity_DSAWebpageParameter(row_values, entity): + """ + convert Campaign/Ad Group Criterion (WebpagePage) Web page parameters to bulk row values + :param row_values: bulk row values + :param entity: campaign/ad group criterion entity + """ + MAX_NUMBER_OF_CONDITIONS = 3 + condition_prefix = _StringTable.DynamicAdTargetCondition1[:-1] + value_prefix = _StringTable.DynamicAdTargetValue1[:-1] + + conditions = [] + for i in range(0, MAX_NUMBER_OF_CONDITIONS): + condition_success, webpage_condition = row_values.try_get_value(condition_prefix + str(i + 1)) + value_success, webpage_value = row_values.try_get_value(value_prefix + str(i + 1)) + if condition_success and value_success and webpage_condition is not None and webpage_condition != '': + condition = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:WebpageCondition') + if webpage_condition.lower() == 'url': + condition.Operand = WebpageConditionOperand.Url + elif webpage_condition.lower() == "category": + condition.Operand = WebpageConditionOperand.Category + elif webpage_condition.lower() == 'pagetitle': + condition.Operand = WebpageConditionOperand.PageTitle + elif webpage_condition.lower() == 'pagecontent': + condition.Operand = WebpageConditionOperand.PageContent + else: + # TODO wait bug 54825 to be fixed + if webpage_condition.lower() == 'none': + continue + raise ValueError("Unknown WebpageConditionOperand value: {0}".format(webpage_condition)) + + condition.Argument = webpage_value + conditions.append(condition) + + if len(conditions) > 0: + if entity.Criterion is not None and isinstance(entity.Criterion, type(Webpage)): + entity.Criterion.Parameter.Conditions.WebpageCondition = conditions + else: + webpage = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:Webpage') + webpage.Parameter.Conditions.WebpageCondition = conditions + entity.Criterion = webpage + + +def parse_bool(value): + if value is None or value == '': + return None + elif value.lower() == 'true': + return True + elif value.lower() == 'false': + return False + else: + raise ValueError('Unable to parse bool value: {0}.'.format(value)) + + +def field_to_csv_RemarketingRule(entity): + """ + convert remarketing rule to bulk string + :param entity: remarketing list entity + """ + if entity.Rule == None: + return None + + rule = entity.Rule + if (isinstance(rule, type(PageVisitorsRule))): + return 'PageVisitors{0}'.format(rule_item_groups_str(rule.RuleItemGroups.RuleItemGroup)) + elif (isinstance(rule, type(PageVisitorsWhoVisitedAnotherPageRule))): + return 'PageVisitorsWhoVisitedAnotherPage({0}) and ({1})'.format( + rule_item_groups_str(rule.RuleItemGroups.RuleItemGroup), + rule_item_groups_str(rule.AnotherRuleItemGroups.RuleItemGroup)) + elif (isinstance(rule, type(PageVisitorsWhoDidNotVisitAnotherPageRule))): + return 'PageVisitorsWhoDidNotVisitAnotherPage({0}) and not ({1})'.format( + rule_item_groups_str(rule.IncludeRuleItemGroups.RuleItemGroup), + rule_item_groups_str(rule.ExcludeRuleItemGroups.RuleItemGroup)) + elif (isinstance(rule, type(CustomEventsRule))): + return 'CustomEvents{0}'.format(custom_event_rule_str(rule)) + elif (isinstance(rule, type(RemarketingRule))): + return None + else: + raise ValueError('Unsupported Remarketing Rule type: {0}'.format(type(entity.RemarketingRule))) + + +def rule_item_groups_str(groups): + if groups is None or len(groups) == 0: + raise ValueError('Remarketing RuleItemGroups is None or empty.') + + return ' or '.join(['({0})'.format(rule_items_str(group.Items.RuleItem)) for group in groups]) + + +def rule_items_str(items): + if items is None or len(items) == 0: + raise ValueError('Remarketing RuleItem list is None or empty.') + + return ' and '.join(['({0} {1} {2})'.format(item.Operand, item.Operator, item.Value) for item in items]) + + +def custom_event_rule_str(rule): + rule_items = [] + if rule.ActionOperator is not None and rule.Action is not None: + rule_items.append('Action {0} {1}'.format(rule.ActionOperator, rule.Action)) + if rule.CategoryOperator is not None and rule.Category is not None: + rule_items.append('Category {0} {1}'.format(rule.CategoryOperator, rule.Category)) + if rule.LabelOperator is not None and rule.Label is not None: + rule_items.append('Label {0} {1}'.format(rule.LabelOperator, rule.Label)) + if rule.ValueOperator is not None and rule.Value is not None: + rule_items.append('Value {0} {1}'.format(rule.ValueOperator, rule.Value)) + + if len(rule_items) == 0: + raise ValueError('Remarketing CustomEvents RuleItem list is empty') + + return ' and '.join('({0})'.format(item) for item in rule_items) + + +def csv_to_field_RemarketingRule(entity, value): + """ + parse remarketing rule string and set remarketing rule attribute value + :param entity: remarketing list entity + :param value: bulk string value + """ + if value is None or value == '': + return + + type_end_pos = value.index('(') + if type_end_pos <= 0: + raise ValueError('Invalid Remarketing Rule: {0}'.format(value)) + + rule_type = value[:type_end_pos] + rule = value[type_end_pos:] + + if rule_type.lower() == 'pagevisitors': + entity.Rule = parse_rule_PageVisitors(rule) + elif rule_type.lower() == 'pagevisitorswhovisitedanotherpage': + entity.Rule = parse_rule_PageVisitorsWhoVisitedAnotherPage(rule) + elif rule_type.lower() == 'pagevisitorswhodidnotvisitanotherpage': + entity.Rule = parse_rule_PageVisitorsWhoDidNotVisitAnotherPage(rule) + elif rule_type.lower() == 'customevents': + entity.Rule = parse_rule_CustomEvents(rule) + else: + raise ValueError('Invalid Remarketing Rule Type: {0}'.format(rule_type)) + + +def field_to_csv_CriterionAudienceId(entity): + if entity is None or entity.Criterion is None or entity.Criterion.AudienceId is None: + return None + return bulk_str(entity.Criterion.AudienceId) + + +def csv_to_field_CriterionAudienceId(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion, type(AudienceCriterion)): + entity.Criterion.AudienceId = value + + +def field_to_csv_BidAdjustment(entity): + if entity is None or entity.CriterionBid is None or entity.CriterionBid.Multiplier is None: + return None + return bulk_str(entity.CriterionBid.Multiplier) + + +def csv_to_field_BidAdjustment(entity, value): + if value is None or value == '': + return + if entity is not None and entity.CriterionBid is not None and isinstance(entity.CriterionBid, type(BidMultiplier)): + entity.CriterionBid.Multiplier = value + +def field_to_csv_AgeTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.AgeRange is None: + return None + return entity.Criterion.AgeRange + +def csv_to_field_AgeTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(AgeCriterion)): + setattr(entity.Criterion, "AgeRange", value) + +def field_to_csv_DayTimeTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.Day is None: + return None + return entity.Criterion.Day + +def csv_to_field_DayTimeTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "Day", value) + +def field_to_csv_FromHour(entity): + if entity is None or entity.Criterion is None or entity.Criterion.FromHour is None: + return None + return str(entity.Criterion.FromHour) + +def csv_to_field_FromHour(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "FromHour", value) + +def field_to_csv_FromMinute(entity): + if entity is None or entity.Criterion is None or entity.Criterion.FromMinute is None: + return None + return minute_bulk_str(entity.Criterion.FromMinute) + +def csv_to_field_FromMinute(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "FromMinute", parse_minute(value)) + +def field_to_csv_ToHour(entity): + if entity is None or entity.Criterion is None or entity.Criterion.ToHour is None: + return None + return str(entity.Criterion.ToHour) + +def csv_to_field_ToHour(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "ToHour", value) + +def field_to_csv_ToMinute(entity): + if entity is None or entity.Criterion is None or entity.Criterion.ToMinute is None: + return None + return minute_bulk_str(entity.Criterion.ToMinute) + +def csv_to_field_ToMinute(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "ToMinute", parse_minute(value)) + +def field_to_csv_DeviceTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.DeviceName is None: + return None + return entity.Criterion.DeviceName + +def csv_to_field_DeviceTarget(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DeviceCriterion)): + setattr(entity.Criterion, "DeviceName", value) + +def field_to_csv_OSName(entity): + if entity is None or entity.Criterion is None or entity.Criterion.OSName is None: + return None + return entity.Criterion.OSName + +def csv_to_field_OSName(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DeviceCriterion)): + setattr(entity.Criterion, "OSName", value) + +def field_to_csv_GenderTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.GenderType is None: + return None + return entity.Criterion.GenderType + +def csv_to_field_GenderTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(GenderCriterion)): + setattr(entity.Criterion, "GenderType", value) + +def field_to_csv_LocationTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LocationId is None: + return None + return str(entity.Criterion.LocationId) + +def csv_to_field_LocationTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationCriterion)): + setattr(entity.Criterion, "LocationId", value) + +def field_to_csv_LocationType(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LocationType is None: + return None + return entity.Criterion.LocationType + +def csv_to_field_LocationType(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationCriterion)): + setattr(entity.Criterion, "LocationType", value) + +def field_to_csv_LocationName(entity): + if entity is None or entity.Criterion is None or entity.Criterion.DisplayName is None: + return None + return entity.Criterion.DisplayName + +def csv_to_field_LocationName(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationCriterion)): + setattr(entity.Criterion, "DisplayName", value) + +def field_to_csv_LocationIntentTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.IntentOption is None: + return None + return entity.Criterion.IntentOption + +def csv_to_field_LocationIntentTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationIntentCriterion)): + setattr(entity.Criterion, "IntentOption", value) + +def field_to_csv_RadiusName(entity): + if entity is None or entity.Criterion is None or entity.Criterion.Name is None: + return None + return entity.Criterion.Name + +def csv_to_field_RadiusName(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "Name", value) + +def field_to_csv_Radius(entity): + if entity is None or entity.Criterion is None or entity.Criterion.Radius is None: + return None + return str(entity.Criterion.Radius) + +def csv_to_field_Radius(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "Radius", value) + +def field_to_csv_RadiusUnit(entity): + if entity is None or entity.Criterion is None or entity.Criterion.RadiusUnit is None: + return None + return entity.Criterion.RadiusUnit + +def csv_to_field_RadiusUnit(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "RadiusUnit", value) + +def field_to_csv_LatitudeDegrees(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LatitudeDegrees is None: + return None + return str(entity.Criterion.LatitudeDegrees) + +def csv_to_field_LatitudeDegrees(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "LatitudeDegrees", value) + +def field_to_csv_LongitudeDegrees(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LongitudeDegrees is None: + return None + return str(entity.Criterion.LongitudeDegrees) + +def csv_to_field_LongitudeDegrees(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "LongitudeDegrees", value) + +def parse_rule_PageVisitors(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:PageVisitorsRule') + rule.Type = 'PageVisitors' + rule.RuleItemGroups = parse_rule_groups(rule_str) + return rule + + +def parse_rule_PageVisitorsWhoVisitedAnotherPage(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:PageVisitorsWhoVisitedAnotherPageRule') + rule.Type = 'PageVisitorsWhoVisitedAnotherPage' + + groups_split = '))) and (((' + groups_string_list = rule_str.split(groups_split) + + rule.RuleItemGroups = parse_rule_groups(groups_string_list[0]) + rule.AnotherRuleItemGroups = parse_rule_groups(groups_string_list[1]) + + return rule + + +def parse_rule_PageVisitorsWhoDidNotVisitAnotherPage(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:PageVisitorsWhoDidNotVisitAnotherPageRule') + rule.Type = 'PageVisitorsWhoDidNotVisitAnotherPage' + + groups_split = '))) and not (((' + groups_string_list = rule_str.split(groups_split) + + rule.IncludeRuleItemGroups = parse_rule_groups(groups_string_list[0]) + rule.ExcludeRuleItemGroups = parse_rule_groups(groups_string_list[1]) + + return rule + + +def parse_rule_CustomEvents(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:CustomEventsRule') + rule.Type = 'CustomEvents' + + item_split = ') and (' + pattern_for_operand_str = '^(Category|Action|Label|Value) ([^()]*)$' + pattern_for_operand = re.compile(pattern_for_operand_str) + + pattern_number_item_str = '^(Equals|GreaterThan|LessThan|GreaterThanEqualTo|LessThanEqualTo) ([^()]*)$' + pattern_number_item = re.compile(pattern_number_item_str) + + pattern_string_item_str = '^(Equals|Contains|BeginsWith|EndsWith|NotEquals|DoesNotContain|DoesNotBeginWith|DoesNotEndWith) ([^()]*)$' + pattern_string_item = re.compile(pattern_string_item_str) + + item_string_list = rule_str.split(item_split) + for item_string in item_string_list: + item_string = item_string.strip('(').strip(')') + match_for_operand = pattern_for_operand.match(item_string) + + if not match_for_operand: + raise ValueError('Invalid Custom Event rule item: {0}'.format(item_string)) + + operand = match_for_operand.group(1) + operater_and_value_string = match_for_operand.group(2) + + if operand.lower() == 'value': + match_number_item = pattern_number_item.match(operater_and_value_string) + + if not match_number_item: + raise ValueError('Invalid Custom Event number rule item: {0}'.format(item_string)) + + rule.ValueOperator = parse_number_operator(match_number_item.group(1)) + rule.Value = float(match_number_item.group(2)) + else: + match_string_item = pattern_string_item.match(operater_and_value_string) + + if not match_string_item: + raise ValueError('Invalid Custom Event string rule item: {0}'.format(item_string)) + + if operand.lower() == 'category': + rule.CategoryOperator = parse_string_operator(match_string_item.group(1)) + rule.Category = match_string_item.group(2) + elif operand.lower() == 'label': + rule.LabelOperator = parse_string_operator(match_string_item.group(1)) + rule.Label = match_string_item.group(2) + elif operand.lower() == 'action': + rule.ActionOperator = parse_string_operator(match_string_item.group(1)) + rule.Action = match_string_item.group(2) + else: + raise ValueError('Invalid Custom Event string rule operator: {0}'.format(operand)) + + return rule + + +def parse_rule_groups(groups_str): + group_split = ')) or ((' + group_str_list = groups_str.split(group_split) + + rule_item_groups = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:ArrayOfRuleItemGroup') + for group_str in group_str_list: + item_group = parse_rule_items(group_str) + rule_item_groups.RuleItemGroup.append(item_group) + + return rule_item_groups + + +def parse_rule_items(items_str): + item_split = ') and (' + item_str_list = items_str.split(item_split) + + rule_item_group = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:RuleItemGroup') + for item_str in item_str_list: + item = parse_string_rule_item(item_str) + rule_item_group.Items.RuleItem.append(item) + + return rule_item_group + + +def parse_string_rule_item(item_str): + item_str = item_str.strip('(').strip(')') + pattern_str = '^(Url|ReferrerUrl|None) (Equals|Contains|BeginsWith|EndsWith|NotEquals|DoesNotContain|DoesNotBeginWith|DoesNotEndWith) ([^()]*)$' + pattern = re.compile(pattern_str) + + match = pattern.match(item_str) + + if not match: + ValueError('Invalid Rule Item:{0}'.format(item_str)) + + item = _CAMPAIGN_OBJECT_FACTORY_V10.create('ns0:StringRuleItem') + item.Type = 'String' + item.Operand = match.group(1) + item.Operator = parse_string_operator(match.group(2)) + item.Value = match.group(3) + + return item + + +def parse_number_operator(operator): + oper = operator.lower() + if oper == 'equals': + return NumberOperator.Equals + if oper == 'greaterthan': + return NumberOperator.GreaterThan + if oper == 'lessthan': + return NumberOperator.LessThan + if oper == 'greaterthanequalto': + return NumberOperator.GreaterThanEqualTo + if oper == 'lessthanequalto': + return NumberOperator.LessThanEqualTo + raise ValueError('Invalid Number Rule Item operator:{0}'.format(operator)) + + +def parse_string_operator(operator): + oper = operator.lower() + if oper == 'equals': + return StringOperator.Equals + if oper == 'contains': + return StringOperator.Contains + if oper == 'beginswith': + return StringOperator.BeginsWith + if oper == 'endswith': + return StringOperator.EndsWith + if oper == 'notequals': + return StringOperator.NotEquals + if oper == 'doesnotcontain': + return StringOperator.DoesNotContain + if oper == 'doesnotbeginwith': + return StringOperator.DoesNotBeginWith + if oper == 'doesnotendwith': + return StringOperator.DoesNotEndWith + + raise ValueError('Invalid String Rule Item operator:{0}'.format(operator)) diff --git a/bingads/v10/bulk/entities/bulk_negative_sites.py b/bingads/v10/bulk/entities/bulk_negative_sites.py index 53ed054f..3fad1308 100644 --- a/bingads/v10/bulk/entities/bulk_negative_sites.py +++ b/bingads/v10/bulk/entities/bulk_negative_sites.py @@ -450,10 +450,10 @@ def convert_api_to_bulk_negative_site(website): bulk_ad_group_negative_site._website = website return bulk_ad_group_negative_site - return map(convert_api_to_bulk_negative_site, self._ad_group_negative_sites.NegativeSites.string) + return list(map(convert_api_to_bulk_negative_site, self._ad_group_negative_sites.NegativeSites.string)) def reconstruct_api_objects(self): - self._ad_group_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + self._ad_group_negative_sites.NegativeSites.string = list([x.website for x in self.negative_sites]) def _create_identifier(self): return _BulkAdGroupNegativeSitesIdentifier( @@ -559,10 +559,10 @@ def convert_api_to_bulk_negative_site(website): bulk_campaign_negative_site._website = website return bulk_campaign_negative_site - return map(convert_api_to_bulk_negative_site, self._campaign_negative_sites.NegativeSites.string) + return list(map(convert_api_to_bulk_negative_site, self._campaign_negative_sites.NegativeSites.string)) def reconstruct_api_objects(self): - self._campaign_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + self._campaign_negative_sites.NegativeSites.string = list([x.website for x in self.negative_sites]) def _create_identifier(self): return _BulkCampaignNegativeSitesIdentifier( diff --git a/bingads/v10/bulk/entities/bulk_negative_sites.py.bak b/bingads/v10/bulk/entities/bulk_negative_sites.py.bak new file mode 100644 index 00000000..53ed054f --- /dev/null +++ b/bingads/v10/bulk/entities/bulk_negative_sites.py.bak @@ -0,0 +1,761 @@ +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V10 + +from bingads.v10.internal.bulk.entities.bulk_entity_identifier import _BulkEntityIdentifier +from bingads.v10.internal.bulk.string_table import _StringTable +from bingads.v10.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v10.internal.bulk.mappings import _SimpleBulkMapping, _DynamicColumnNameMapping +from bingads.v10.internal.bulk.entities.multi_record_bulk_entity import _MultiRecordBulkEntity +from bingads.internal.extensions import bulk_str + + +class _BulkNegativeSite(_SingleRecordBulkEntity): + """ This abstract base class for the bulk negative sites that are assigned individually to a campaign or ad group entity. + + *See also:* + + * :class:`.BulkAdGroupNegativeSite` + * :class:`.BulkCampaignNegativeSite` + """ + + def __init__(self, identifier, website=None): + super(_BulkNegativeSite, self).__init__() + + self._identifier = identifier + self._website = website + + @property + def website(self): + """ The URL of a website on which you do not want your ads displayed. + + Corresponds to the 'Website' field in the bulk file. + + :rtype: str + """ + + return self._website + + @website.setter + def website(self, website): + self._website = website + + @property + def status(self): + """ The status of the negative site association. + + :rtype: str + """ + + return self._identifier.status + + @status.setter + def status(self, value): + self._identifier.status = value + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Website, + field_to_csv=lambda c: c.website, + csv_to_field=lambda c, v: setattr(c, 'website', v) + ), + ] + + def process_mappings_from_row_values(self, row_values): + self._identifier.read_from_row_values(row_values) + row_values.convert_to_entity(self, _BulkNegativeSite._MAPPINGS) + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._identifier.write_to_row_values(row_values, exclude_readonly_data) + self.convert_to_values(row_values, _BulkNegativeSite._MAPPINGS) + + @property + def can_enclose_in_multiline_entity(self): + return True + + def enclose_in_multiline_entity(self): + return self.create_negative_sites_with_this_negative_site() + + def create_negative_sites_with_this_negative_site(self): + raise NotImplementedError() + + def read_additional_data(self, stream_reader): + super(_BulkNegativeSite, self).read_additional_data(stream_reader) + + +class BulkAdGroupNegativeSite(_BulkNegativeSite): + """ Represents a negative site that is assigned to an ad group. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Ad Group Negative Site record in a bulk file. + + For more information, see Ad Group Negative Site at http://go.microsoft.com/fwlink/?LinkID=620254. + + One :class:`.BulkAdGroupNegativeSites` exposes a read only list of :class:`.BulkAdGroupNegativeSite`. Each + :class:`.BulkAdGroupNegativeSite` instance corresponds to one Ad Group Negative Site record in the bulk file. If you + upload a :class:`.BulkAdGroupNegativeSites`, then you are effectively replacing any existing negative sites + assigned to the ad group. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + status=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, + website=None): + super(BulkAdGroupNegativeSite, self).__init__( + _BulkAdGroupNegativeSitesIdentifier( + status=status, + ad_group_id=ad_group_id, + ad_group_name=ad_group_name, + campaign_name=campaign_name, + ), + website=website + ) + + @property + def ad_group_id(self): + """ The identifier of the ad group that the negative site is assigned. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.ad_group_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._identifier.ad_group_id = value + + @property + def ad_group_name(self): + """ The name of the ad group that the negative site is assigned. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._identifier.ad_group_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._identifier.ad_group_name = value + + @property + def campaign_name(self): + """ The name of the ad group that the negative site is assigned. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._identifier.campaign_name + + @campaign_name.setter + def campaign_name(self, value): + self._identifier.campaign_name = value + + def create_negative_sites_with_this_negative_site(self): + return BulkAdGroupNegativeSites(site=self) + + +class BulkCampaignNegativeSite(_BulkNegativeSite): + """ Represents a negative site that is assigned to an campaign. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Campaign Negative Site record in a bulk file. + + For more information, see Campaign Negative Site at http://go.microsoft.com/fwlink/?LinkID=620242. + + One :class:`.BulkCampaignNegativeSites` exposes a read only list of :class:`.BulkCampaignNegativeSite`. Each + :class:`.BulkCampaignNegativeSite` instance corresponds to one Campaign Negative Site record in the bulk file. If you + upload a :class:`.BulkCampaignNegativeSites`, then you are effectively replacing any existing negative sites + assigned to the campaign. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + status=None, + campaign_id=None, + campaign_name=None, + website=None): + super(BulkCampaignNegativeSite, self).__init__( + _BulkCampaignNegativeSitesIdentifier( + status=status, + campaign_id=campaign_id, + campaign_name=campaign_name + ), + website=website + ) + + @property + def campaign_id(self): + """ The identifier of the campaign that the negative site is assigned. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.campaign_id + + @campaign_id.setter + def campaign_id(self, value): + self._identifier.campaign_id = value + + @property + def campaign_name(self): + """ The name of the campaign that the negative site is assigned. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + return self._identifier.campaign_name + + @campaign_name.setter + def campaign_name(self, value): + self._identifier.campaign_name = value + + def create_negative_sites_with_this_negative_site(self): + return BulkCampaignNegativeSites(site=self) + + +class _BulkNegativeSites(_MultiRecordBulkEntity): + """ This abstract base class for the bulk negative sites that assigned in sets to a campaign or ad group entity. """ + + def __init__(self, status=None, site=None, identifier=None): + super(_BulkNegativeSites, self).__init__() + + self._bulk_negative_sites = [] + self._first_row_identifier = None + self._has_delete_all_row = None + + self._site = site + self._identifier = identifier + + if self._site and self._identifier: + raise ValueError('Conflicting keyword arguments of site and identifier provided') + + if self._site: + if not isinstance(self._site, self.site_class): + raise ValueError('Negative site object provided is not of type: {0}'.format(self.site_class.__name__)) + self._bulk_negative_sites.append(self._site) + self._identifier = self._site._identifier + + if self._identifier: + if not isinstance(self._identifier, self.identifier_class): + raise ValueError( + 'Negative site object provided is not of type: {0}'.format(self.identifier_class.__name__)) + self._first_row_identifier = self._identifier + self._has_delete_all_row = self._identifier.is_delete_row + + self._status = status + + @property + def status(self): + """ The status of the negative site association. + + :rtype: str + """ + + return self._status + + @status.setter + def status(self, status): + self._status = status + + @property + def negative_sites(self): + return self._bulk_negative_sites + + @property + def child_entities(self): + return self.negative_sites + + def _create_identifier(self): + raise NotImplementedError() + + def _validate_properties_not_null(self): + raise NotImplementedError() + + def write_to_stream(self, row_writer, exclude_readonly_data): + self._validate_properties_not_null() + + delete_row = self._create_identifier() + delete_row._status = 'Deleted' + row_writer.write_object_row(delete_row, exclude_readonly_data) + + if self._status == 'Deleted': + return + + for site in self.convert_api_to_bulk_negative_sites(): + site.write_to_stream(row_writer, exclude_readonly_data) + + def convert_api_to_bulk_negative_sites(self): + raise NotImplementedError() + + def reconstruct_api_objects(self): + raise NotImplementedError() + + @property + def site_class(self): + raise NotImplementedError() + + @property + def identifier_class(self): + raise NotImplementedError() + + def read_related_data_from_stream(self, stream_reader): + has_more_rows = True + while has_more_rows: + site_success, site = stream_reader.try_read( + self.site_class, + lambda x: x._identifier == self._first_row_identifier + ) + if site_success: + self._bulk_negative_sites.append(site) + else: + identifier_success, identifier = stream_reader.try_read( + self.identifier_class, + lambda x: x == self._first_row_identifier + ) + if identifier_success: + if identifier.is_delete_row: + self._has_delete_all_row = True + else: + has_more_rows = False + + self.reconstruct_api_objects() + self._status = 'Active' if self._bulk_negative_sites else 'Deleted' + + @property + def all_children_are_present(self): + return self._has_delete_all_row + + +class BulkAdGroupNegativeSites(_BulkNegativeSites): + """ Represents one or more negative sites that are assigned to an ad group. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Ad Group Negative Site record in a bulk file. + + For more information, see Ad Group Negative Site at http://go.microsoft.com/fwlink/?LinkID=620254. + + One :class:`.BulkAdGroupNegativeSites` has one or more :class:`.BulkAdGroupNegativeSite`. Each :class:`.BulkAdGroupNegativeSite` instance + corresponds to one Ad Group Negative Site record in the bulk file. If you upload a :class:`.BulkAdGroupNegativeSites`, + then you are effectively replacing any existing negative sites assigned to the ad group. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + ad_group_negative_sites=None, + ad_group_name=None, + campaign_name=None, + status=None, + site=None, + identifier=None): + super(BulkAdGroupNegativeSites, self).__init__( + status=status, + site=site, + identifier=identifier, + ) + + self._ad_group_negative_sites = ad_group_negative_sites + self._ad_group_name = ad_group_name + self._campaign_name = campaign_name + + if self._identifier: + self.set_data_from_identifier(self._identifier) + + @property + def ad_group_negative_sites(self): + """ The AdGroupNegativeSites Data Object of the Campaign Management Service. + + subset of AdGroupNegativeSites properties are available in the Ad Group Negative Site record. + For more information, see Ad Group Negative Site at http://go.microsoft.com/fwlink/?LinkID=620254. + """ + + return self._ad_group_negative_sites + + @ad_group_negative_sites.setter + def ad_group_negative_sites(self, ad_group_negative_sites): + self._ad_group_negative_sites = ad_group_negative_sites + + @property + def ad_group_name(self): + """ The name of the ad group that the negative site is assigned. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._ad_group_name + + @ad_group_name.setter + def ad_group_name(self, ad_group_name): + self._ad_group_name = ad_group_name + + @property + def campaign_name(self): + """ The name of the campaign that the negative site is assigned. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def set_data_from_identifier(self, identifier): + self._ad_group_negative_sites = _CAMPAIGN_OBJECT_FACTORY_V10.create('AdGroupNegativeSites') + self._ad_group_negative_sites.AdGroupId = identifier.ad_group_id + self._ad_group_name = identifier.ad_group_name + self._campaign_name = identifier.campaign_name + + def convert_api_to_bulk_negative_sites(self): + self._validate_list_not_null_or_empty( + self._ad_group_negative_sites.NegativeSites, + self._ad_group_negative_sites.NegativeSites.string, + 'ad_group_negative_sites.negative_sites' + ) + + def convert_api_to_bulk_negative_site(website): + bulk_ad_group_negative_site = BulkAdGroupNegativeSite() + bulk_ad_group_negative_site.ad_group_id = self._ad_group_negative_sites.AdGroupId + bulk_ad_group_negative_site.ad_group_name = self._ad_group_name + bulk_ad_group_negative_site.campaign_name = self._campaign_name + bulk_ad_group_negative_site._website = website + return bulk_ad_group_negative_site + + return map(convert_api_to_bulk_negative_site, self._ad_group_negative_sites.NegativeSites.string) + + def reconstruct_api_objects(self): + self._ad_group_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + + def _create_identifier(self): + return _BulkAdGroupNegativeSitesIdentifier( + ad_group_id=self._ad_group_negative_sites.AdGroupId, + ad_group_name=self._ad_group_name, + campaign_name=self.campaign_name + ) + + def _validate_properties_not_null(self): + self._validate_property_not_null(self._ad_group_negative_sites, 'ad_group_negative_sites') + + @property + def identifier_class(self): + return _BulkAdGroupNegativeSitesIdentifier + + @property + def site_class(self): + return BulkAdGroupNegativeSite + + +class BulkCampaignNegativeSites(_BulkNegativeSites): + """ Represents one or more negative sites that are assigned to an campaign. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Campaign Negative Site record in a bulk file. + + For more information, see Campaign Negative Site at http://go.microsoft.com/fwlink/?LinkID=620254. + + One :class:`.BulkCampaignNegativeSites` has one or more :class:`.BulkCampaignNegativeSite`. Each :class:`.BulkCampaignNegativeSite` instance + corresponds to one Campaign Negative Site record in the bulk file. If you upload a :class:`.BulkCampaignNegativeSites`, + then you are effectively replacing any existing negative sites assigned to the campaign. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + campaign_negative_sites=None, + campaign_name=None, + status=None, + site=None, + identifier=None): + super(BulkCampaignNegativeSites, self).__init__( + status=status, + site=site, + identifier=identifier + ) + + self._campaign_negative_sites = campaign_negative_sites + self._campaign_name = campaign_name + + if self._identifier: + self.set_data_from_identifier(self._identifier) + + @property + def campaign_negative_sites(self): + """ The CampaignNegativeSites Data Object of the Campaign Management Service. + + A subset of CampaignNegativeSites properties are available in the Campaign Negative Site record. + For more information, see Campaign Negative Site at http://go.microsoft.com/fwlink/?LinkID=620242. + + """ + + return self._campaign_negative_sites + + @campaign_negative_sites.setter + def campaign_negative_sites(self, campaign_negative_sites): + self._campaign_negative_sites = campaign_negative_sites + + @property + def campaign_name(self): + """ The name of the campaign that the negative site is assigned. + + Corresponds to the 'Campaign' field in the bulk file. + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def set_data_from_identifier(self, identifier): + self._campaign_negative_sites = _CAMPAIGN_OBJECT_FACTORY_V10.create('CampaignNegativeSites') + self.campaign_negative_sites.CampaignId = identifier.campaign_id + self._campaign_name = identifier.campaign_name + + def convert_api_to_bulk_negative_sites(self): + self._validate_list_not_null_or_empty( + self._campaign_negative_sites.NegativeSites, + self._campaign_negative_sites.NegativeSites.string, + 'campaign_negative_sites.negative_sites' + ) + + def convert_api_to_bulk_negative_site(website): + bulk_campaign_negative_site = BulkCampaignNegativeSite() + bulk_campaign_negative_site.campaign_id = self._campaign_negative_sites.CampaignId + bulk_campaign_negative_site.campaign_name = self._campaign_name + bulk_campaign_negative_site._website = website + return bulk_campaign_negative_site + + return map(convert_api_to_bulk_negative_site, self._campaign_negative_sites.NegativeSites.string) + + def reconstruct_api_objects(self): + self._campaign_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + + def _create_identifier(self): + return _BulkCampaignNegativeSitesIdentifier( + campaign_id=self._campaign_negative_sites.CampaignId, + campaign_name=self._campaign_name + ) + + def _validate_properties_not_null(self): + self._validate_property_not_null(self._campaign_negative_sites, 'campaign_negative_sites') + + @property + def identifier_class(self): + return _BulkCampaignNegativeSitesIdentifier + + @property + def site_class(self): + return BulkCampaignNegativeSite + + +class _BulkNegativeSiteIdentifier(_BulkEntityIdentifier): + def __init__(self, status=None, entity_id=None, entity_name=None): + self._status = status + self._entity_id = entity_id + self._entity_name = entity_name + + @property + def status(self): + return self._status + + @property + def entity_id(self): + return self._entity_id + + @property + def entity_name(self): + return self._entity_name + + @property + def _parent_column_name(self): + raise NotImplementedError() + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Status, + field_to_csv=lambda c: bulk_str(c._status), + csv_to_field=lambda c, v: setattr(c, '_status', v if v else None) + ), + _SimpleBulkMapping( + header=_StringTable.ParentId, + field_to_csv=lambda c: None if c._entity_id == 0 else bulk_str(c._entity_id), + csv_to_field=lambda c, v: setattr(c, '_entity_id', int(v) if v else 0) + ), + _DynamicColumnNameMapping( + header_func=lambda c: c._parent_column_name, + field_to_csv=lambda c: c._entity_name, + csv_to_field=lambda c, v: setattr(c, '_entity_name', v) + ) + ] + + @property + def is_delete_row(self): + return self._status == 'Deleted' + + def read_from_row_values(self, row_values): + row_values.convert_to_entity(self, _BulkNegativeSiteIdentifier._MAPPINGS) + + def write_to_row_values(self, row_values, exclude_readonly_data): + self.convert_to_values(row_values, _BulkNegativeSiteIdentifier._MAPPINGS) + + +class _BulkCampaignNegativeSitesIdentifier(_BulkNegativeSiteIdentifier): + def __init__(self, status=None, campaign_id=None, campaign_name=None): + super(_BulkCampaignNegativeSitesIdentifier, self).__init__( + status, + campaign_id, + campaign_name, + ) + + def __eq__(self, other): + is_name_not_empty = ( + self.campaign_name is not None and + len(self.campaign_name) > 0 + ) + return ( + type(self) == type(other) and + ( + self.campaign_id == other.campaign_id or + ( + is_name_not_empty and + self.campaign_name == other.campaign_name + ) + ) + ) + + @property + def campaign_id(self): + return self._entity_id + + @campaign_id.setter + def campaign_id(self, value): + self._entity_id = value + + @property + def campaign_name(self): + return self._entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._entity_name = value + + def _create_entity_with_this_identifier(self): + return BulkCampaignNegativeSites(identifier=self) + + @property + def _parent_column_name(self): + return _StringTable.Campaign + + +class _BulkAdGroupNegativeSitesIdentifier(_BulkNegativeSiteIdentifier): + def __init__(self, + status=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None): + super(_BulkAdGroupNegativeSitesIdentifier, self).__init__( + status, + ad_group_id, + ad_group_name, + ) + self._campaign_name = campaign_name + + def __eq__(self, other): + is_name_not_empty = ( + self.campaign_name is not None and + len(self.campaign_name) > 0 and + self.ad_group_name is not None and + len(self.ad_group_name) > 0 + ) + return ( + type(self) == type(other) and + ( + self.ad_group_id == other.ad_group_id or + ( + is_name_not_empty and + self.campaign_name == other.campaign_name and + self.ad_group_name == other.ad_group_name + ) + ) + ) + + @property + def ad_group_id(self): + return self._entity_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._entity_id = value + + @property + def ad_group_name(self): + return self._entity_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._entity_name = value + + @property + def campaign_name(self): + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + ] + + def read_from_row_values(self, row_values): + super(_BulkAdGroupNegativeSitesIdentifier, self).read_from_row_values(row_values) + row_values.convert_to_entity(self, _BulkAdGroupNegativeSitesIdentifier._MAPPINGS) + + def write_to_row_values(self, row_values, exclude_readonly_data): + super(_BulkAdGroupNegativeSitesIdentifier, self).write_to_row_values(row_values, exclude_readonly_data) + self.convert_to_values(row_values, _BulkAdGroupNegativeSitesIdentifier._MAPPINGS) + + def _create_entity_with_this_identifier(self): + return BulkAdGroupNegativeSites(identifier=self) + + @property + def _parent_column_name(self): + return _StringTable.AdGroup diff --git a/bingads/v10/bulk/entities/targets/bulk_targets.py b/bingads/v10/bulk/entities/targets/bulk_targets.py index 359d0278..35bfa7eb 100644 --- a/bingads/v10/bulk/entities/targets/bulk_targets.py +++ b/bingads/v10/bulk/entities/targets/bulk_targets.py @@ -1731,7 +1731,7 @@ def __init__(self, csv_to_field=lambda c, v: setattr( c.device_os_target_bid.OSNames, 'string', - list(filter(None, v.split(';'))) if v else [], + list([_f for _f in v.split(';') if _f]) if v else [], ), ), _SimpleBulkMapping( diff --git a/bingads/v10/bulk/entities/targets/bulk_targets.py.bak b/bingads/v10/bulk/entities/targets/bulk_targets.py.bak new file mode 100644 index 00000000..359d0278 --- /dev/null +++ b/bingads/v10/bulk/entities/targets/bulk_targets.py.bak @@ -0,0 +1,3441 @@ +from bingads.v10.internal.bulk.entities.multi_record_bulk_entity import _MultiRecordBulkEntity +from bingads.v10.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v10.internal.bulk.entities.bulk_entity_identifier import _BulkEntityIdentifier +from bingads.v10.internal.bulk.mappings import _SimpleBulkMapping, _DynamicColumnNameMapping +from bingads.v10.internal.bulk.string_table import _StringTable +from bingads.internal.extensions import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V10 + + +class _BulkTargetIdentifier(_BulkEntityIdentifier): + def __init__(self, + target_bid_type, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + self._target_bid_type = target_bid_type + self._status = status + self._target_id = target_id + self._entity_id = entity_id + self._entity_name = entity_name + self._parent_entity_name = parent_entity_name + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Status, + field_to_csv=lambda c: bulk_str(c.status), + csv_to_field=lambda c, v: setattr(c, '_status', v if v else None), + ), + _SimpleBulkMapping( + header=_StringTable.Id, + field_to_csv=lambda c: bulk_str(c.target_id), + csv_to_field=lambda c, v: setattr(c, '_target_id', int(v) if v else None), + ), + _SimpleBulkMapping( + header=_StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.entity_id), + csv_to_field=lambda c, v: setattr(c, '_entity_id', int(v) if v else None), + ), + _DynamicColumnNameMapping( + header_func=lambda c: c.entity_column_name, + field_to_csv=lambda c: c.entity_name, + csv_to_field=lambda c, v: setattr(c, '_entity_name', v), + ), + ] + + def write_to_row_values(self, row_values, exclude_readonly_data): + self.convert_to_values(row_values, _BulkTargetIdentifier._MAPPINGS) + + def read_from_row_values(self, row_values): + row_values.convert_to_entity(self, _BulkTargetIdentifier._MAPPINGS) + + @property + def is_delete_row(self): + return self.status == 'Deleted' + + def _create_entity_with_this_identifier(self): + raise NotImplementedError() + + @property + def entity_column_name(self): + raise NotImplementedError() + + @property + def status(self): + return self._status + + @status.setter + def status(self, value): + self._status = value + + @property + def target_id(self): + return self._target_id + + @target_id.setter + def target_id(self, value): + self._target_id = value + + @property + def entity_id(self): + return self._entity_id + + @entity_id.setter + def entity_id(self, value): + self._entity_id = value + + @property + def entity_name(self): + return self._entity_name + + @entity_name.setter + def entity_name(self, value): + self._entity_name = value + + @property + def parent_entity_name(self): + return self._parent_entity_name + + @parent_entity_name.setter + def parent_entity_name(self, value): + self._parent_entity_name = value + + @property + def target_bid_type(self): + return self._target_bid_type + + +class _BulkCampaignTargetIdentifier(_BulkTargetIdentifier): + @property + def entity_column_name(self): + return _StringTable.Campaign + + def _create_entity_with_this_identifier(self): + return BulkCampaignTarget(identifier=self) + + def __eq__(self, other): + is_name_not_empty = ( + self.campaign_name is not None and + len(self.campaign_name) > 0 + ) + return ( + type(self) == type(other) and + ( + self.campaign_id == other.campaign_id or + ( + is_name_not_empty and + self.campaign_name == other.campaign_name + ) + ) + ) + + @property + def campaign_id(self): + return self._entity_id + + @campaign_id.setter + def campaign_id(self, value): + self._entity_id = value + + @property + def campaign_name(self): + return self._entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._entity_name = value + + +class _BulkAdGroupTargetIdentifier(_BulkTargetIdentifier): + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Campaign, + field_to_csv=lambda c: bulk_str(c.parent_entity_name), + csv_to_field=lambda c, v: setattr(c, '_parent_entity_name', v) + ) + ] + + def __eq__(self, other): + is_name_not_empty = ( + self.campaign_name is not None and + len(self.campaign_name) > 0 and + self.ad_group_name is not None and + len(self.ad_group_name) > 0 + ) + return ( + type(self) == type(other) and + ( + self.ad_group_id == other.ad_group_id or + ( + is_name_not_empty and + self.campaign_name == other.campaign_name and + self.ad_group_name == other.ad_group_name + ) + ) + ) + + def write_to_row_values(self, row_values, exclude_readonly_data): + super(_BulkAdGroupTargetIdentifier, self).write_to_row_values(row_values, exclude_readonly_data) + self.convert_to_values(row_values, _BulkAdGroupTargetIdentifier._MAPPINGS) + + def read_from_row_values(self, row_values): + super(_BulkAdGroupTargetIdentifier, self).read_from_row_values(row_values) + row_values.convert_to_entity(self, _BulkAdGroupTargetIdentifier._MAPPINGS) + + @property + def entity_column_name(self): + return _StringTable.AdGroup + + def _create_entity_with_this_identifier(self): + return BulkAdGroupTarget(identifier=self) + + @property + def ad_group_id(self): + return self._entity_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._entity_id = value + + @property + def ad_group_name(self): + return self._entity_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._entity_name = value + + @property + def campaign_name(self): + return self._parent_entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._parent_entity_name = value + + +class _BulkTargetBid(_SingleRecordBulkEntity): + """ This base class provides properties that are shared by all bulk target bid classes. + + For example :class:`.BulkAdGroupDayTimeTargetBid`. + """ + + def __init__(self, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + super(_BulkTargetBid, self).__init__() + self._identifier = self._identifier_type( + target_bid_type=type(self), + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def process_mappings_from_row_values(self, row_values): + self._prepare_process_mapping_from_row_values() + self._identifier.read_from_row_values(row_values) + row_values.convert_to_entity(self, type(self)._MAPPINGS) + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._identifier.write_to_row_values(row_values, exclude_readonly_data) + self.convert_to_values(row_values, type(self)._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(_BulkTargetBid, self).read_additional_data(stream_reader) + + @property + def can_enclose_in_multiline_entity(self): + return True + + def enclose_in_multiline_entity(self): + if isinstance(self, _BulkTargetBidCampaignMixin): + return BulkCampaignTarget(bulk_target_bid=self) + elif isinstance(self, _BulkTargetBidAdGroupMixin): + return BulkAdGroupTarget(bulk_target_bid=self) + else: + raise Exception( + 'BulkTargetBid class: {0}, should be extended with either {1} or {2}'.format( + type(self).__name__, + _BulkTargetBidCampaignMixin.__name__, + _BulkTargetBidAdGroupMixin.__name__ + ) + ) + + def _prepare_process_mapping_from_row_values(self): + raise NotImplementedError() + + @property + def _identifier_type(self): + if isinstance(self, _BulkTargetBidCampaignMixin): + return _BulkCampaignTargetIdentifier + elif isinstance(self, _BulkTargetBidAdGroupMixin): + return _BulkAdGroupTargetIdentifier + else: + raise Exception( + 'BulkTargetBid class: {0}, should be extended with either {1} or {2}'.format( + type(self).__name__, + _BulkTargetBidCampaignMixin.__name__, + _BulkTargetBidAdGroupMixin.__name__ + ) + ) + + @property + def status(self): + """ The status of the target bid. + + The value is Active if the target bid is available in the target. + The value is Deleted if the target bid is deleted from the target, or should be deleted in a subsequent upload operation. + Corresponds to the 'Status' field in the bulk file. + + :rtype: str + """ + + return self._identifier.status + + @status.setter + def status(self, value): + self._identifier.status = value + + @property + def target_id(self): + """ The identifier of the target that contains this target bid. + + Corresponds to the 'Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.target_id + + @target_id.setter + def target_id(self, value): + self._identifier.target_id = value + + @property + def entity_id(self): + return self._identifier.entity_id + + @entity_id.setter + def entity_id(self, value): + self._identifier.entity_id = value + + @property + def entity_name(self): + return self._identifier.entity_name + + @entity_name.setter + def entity_name(self, value): + self._identifier.entity_name = value + + @property + def parent_entity_name(self): + return self._identifier.parent_entity_name + + @parent_entity_name.setter + def parent_entity_name(self, value): + self._identifier.parent_entity_name = value + + +class _BulkTargetBidCampaignMixin(object): + @property + def campaign_id(self): + """ The identifier of the campaign that the target is associated. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.campaign_id + + @campaign_id.setter + def campaign_id(self, value): + self._identifier.campaign_id = value + + @property + def campaign_name(self): + """ The name of the campaign that the target is associated. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + + return self._identifier.campaign_name + + @campaign_name.setter + def campaign_name(self, value): + self._identifier.campaign_name = value + + +class _BulkTargetBidAdGroupMixin(object): + @property + def ad_group_id(self): + """ The identifier of the ad group that the target is associated. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.ad_group_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._identifier.ad_group_id = value + + @property + def ad_group_name(self): + """ The name of the ad group that target is associated. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._identifier.ad_group_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._identifier.ad_group_name = value + + @property + def campaign_name(self): + """ The name of the ad group that target is associated. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + + return self._identifier.campaign_name + + @campaign_name.setter + def campaign_name(self, value): + self._identifier.campaign_name = value + + +class _BulkSubTarget(_MultiRecordBulkEntity): + """ This base class provides properties that are shared by all bulk sub target classes. """ + + def __init__(self, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + super(_BulkSubTarget, self).__init__() + self._target_id = target_id + self._status = status + self._entity_id = entity_id + self._entity_name = entity_name + self._parent_entity_name = parent_entity_name + self._bids = [] + self._identifier = None + self._has_delete_all_row = False + self._is_being_written_as_part_of_parent_target = False + + def set_bids(self, bids): + for bid in bids: + self._bids.append(bid) + self._convert_bulk_entities_to_target() + self.status = 'Active' if self._bids else 'Deleted' + + def set_identifier(self, identifier): + self._identifier = identifier + self._has_delete_all_row = identifier.is_delete_row + self.entity_id = identifier.entity_id + self.target_id = identifier.target_id + self.entity_name = identifier.entity_name + self.parent_entity_name = identifier.parent_entity_name + + def _create_identifier_before_write(self): + identifier = self._create_bid()._identifier + identifier.status = 'Deleted' + identifier.target_id = self.target_id + identifier.entity_id = self.entity_id + identifier.entity_name = self.entity_name + identifier.parent_entity_name = self.parent_entity_name + return identifier + + def write_to_stream(self, row_writer, exclude_readonly_data): + + if (not self.is_being_written_as_part_of_parent_target) and (not self.status == 'Deleted'): + self._validate_properties() + # as suds accepts empty sub targets, we accept it either. + self._validate_bids() + + identifier = self._create_identifier_before_write() + identifier.write_to_stream(row_writer, exclude_readonly_data) + if self.status == 'Deleted': + return + for bulk_entity in self._convert_target_to_bulk_entities(): + bulk_entity.write_to_stream(row_writer, exclude_readonly_data) + + def read_related_data_from_stream(self, stream_reader): + pass + + def _create_and_populate_bid(self): + bid = self._create_bid() + bid.status = self.status + bid.target_id = self.target_id + bid.entity_id = self.entity_id + bid.entity_name = self.entity_name + bid.parent_entity_name = self.parent_entity_name + return bid + + @property + def bids(self): + """ The list of target bids corresponding the this sub target type. + + :rtype: list + """ + + return self._bids + + @property + def child_entities(self): + return self._bids + + @property + def all_children_are_present(self): + return self._has_delete_all_row + + @property + def is_being_written_as_part_of_parent_target(self): + return self._is_being_written_as_part_of_parent_target + + @is_being_written_as_part_of_parent_target.setter + def is_being_written_as_part_of_parent_target(self, value): + self._is_being_written_as_part_of_parent_target = value + + def _create_bid(self): + raise NotImplementedError() + + def _validate_properties(self): + raise NotImplementedError() + + def _validate_bids(self): + raise NotImplementedError() + + def _convert_target_to_bulk_entities(self): + raise NotImplementedError() + + def _convert_bulk_entities_to_target(self): + raise NotImplementedError() + + @property + def status(self): + """ The status of the target. + + The value is Active if the target is available in the customer's shared library. + The value is Deleted if the target is deleted from the customer's shared library, or should be deleted in a subsequent upload operation. + Corresponds to the 'Status' field in the bulk file. + + :rtype: str + """ + + return self._status + + @status.setter + def status(self, value): + self._status = value + + @property + def target_id(self): + """ The identifier of the target. + + Corresponds to the 'Id' field in the bulk file. + + :rtype: int + """ + + return self._target_id + + @target_id.setter + def target_id(self, value): + self._target_id = value + + @property + def entity_id(self): + return self._entity_id + + @entity_id.setter + def entity_id(self, value): + self._entity_id = value + + @property + def entity_name(self): + return self._entity_name + + @entity_name.setter + def entity_name(self, value): + self._entity_name = value + + @property + def parent_entity_name(self): + return self._parent_entity_name + + @parent_entity_name.setter + def parent_entity_name(self, value): + self._parent_entity_name = value + + +class _BulkSubTargetCampaignMixin(object): + @property + def campaign_id(self): + """ The identifier of the campaign that the target is associated. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._entity_id + + @campaign_id.setter + def campaign_id(self, value): + self._entity_id = value + + @property + def campaign_name(self): + """ The name of the campaign that the target is associated. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + + return self._entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._entity_name = value + + +class _BulkSubTargetAdGroupMixin(object): + @property + def ad_group_id(self): + """ The identifier of the ad group that the target is associated. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._entity_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._entity_id = value + + @property + def ad_group_name(self): + """ The name of the ad group that the target is associated. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._entity_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._entity_name = value + + @property + def campaign_name(self): + """ The name of the ad group that target is associated. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + return self._parent_entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._parent_entity_name = value + + +class _BulkTarget(_MultiRecordBulkEntity): + """ This abstract base class provides properties that are shared by all bulk target classes. + + For example :class:`.BulkAdGroupDayTimeTarget`. + """ + + def __init__(self, + target=None, + status=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, + bulk_target_bid=None, + identifier=None, ): + super(_BulkTarget, self).__init__() + self._target = target + self._status = status + self._entity_id = entity_id + self._entity_name = entity_name + self._parent_entity_name = parent_entity_name + + self._bids = [] + self._deleted_rows = [] + self._original_identifier = None + if isinstance(self, BulkCampaignTarget): + self._age_target = BulkCampaignAgeTarget() + self._day_time_target = BulkCampaignDayTimeTarget() + self._device_os_target = BulkCampaignDeviceOsTarget() + self._gender_target = BulkCampaignGenderTarget() + self._radius_target = BulkCampaignRadiusTarget() + self._location_target = BulkCampaignLocationTarget() + self._negative_location_target = BulkCampaignNegativeLocationTarget() + elif isinstance(self, BulkAdGroupTarget): + self._age_target = BulkAdGroupAgeTarget() + self._day_time_target = BulkAdGroupDayTimeTarget() + self._device_os_target = BulkAdGroupDeviceOsTarget() + self._gender_target = BulkAdGroupGenderTarget() + self._radius_target = BulkAdGroupRadiusTarget() + self._location_target = BulkAdGroupLocationTarget() + self._negative_location_target = BulkAdGroupNegativeLocationTarget() + else: + raise ValueError('Can only initialize with BulkAdGroupTarget or BulkCampaignTarget.') + self._sub_targets = [ + self._age_target, + self._day_time_target, + self._device_os_target, + self._gender_target, + self._radius_target, + self._location_target, + self._negative_location_target, + ] + + if bulk_target_bid is not None and identifier is not None: + raise ValueError('cannot provide both bulk_target_bid and identifier') + self._identifier = identifier + if bulk_target_bid is not None: + self._bids.append(bulk_target_bid) + self._identifier = bulk_target_bid._identifier + if self._identifier is not None: + self._original_identifier = self._identifier + self._target = _CAMPAIGN_OBJECT_FACTORY_V10.create('Target') + self._target.Id = self._identifier.target_id + self._entity_id = self._identifier.entity_id + self._entity_name = self._identifier.entity_name + self._parent_entity_name = self._identifier.parent_entity_name + if self._identifier.is_delete_row: + self._deleted_rows.append(self._identifier) + + def write_to_stream(self, row_writer, exclude_readonly_data): + if self.status != 'Deleted': + self._validate_property_not_null(self.target, 'target') + if self.target is not None: + self.age_target.age_target = self.target.Age + self.day_time_target.day_time_target = self.target.DayTime + self.device_os_target.device_os_target = self.target.DeviceOS + self.gender_target.gender_target = self.target.Gender + self.radius_target.location_target = self.target.Location + self.location_target.location_target = self.target.Location + self.negative_location_target.location_target = self.target.Location + + self.set_default_identifier(self.age_target) + self.set_default_identifier(self.day_time_target) + self.set_default_identifier(self.device_os_target) + self.set_default_identifier(self.gender_target) + self.set_default_identifier(self.radius_target) + self.set_default_identifier(self.location_target) + self.set_default_identifier(self.negative_location_target) + + self.age_target.is_being_written_as_part_of_parent_target = True + self.day_time_target.is_being_written_as_part_of_parent_target = True + self.device_os_target.is_being_written_as_part_of_parent_target = True + self.gender_target.is_being_written_as_part_of_parent_target = True + self.radius_target.is_being_written_as_part_of_parent_target = True + self.location_target.is_being_written_as_part_of_parent_target = True + self.negative_location_target.is_being_written_as_part_of_parent_target = True + + for sub_target in self.sub_targets: + sub_target.write_to_stream(row_writer, exclude_readonly_data) + + def read_related_data_from_stream(self, stream_reader): + has_more_rows = True + while has_more_rows: + bulk_target_bid_success, bulk_target_bid = stream_reader.try_read( + _BulkTargetBid, + lambda x: x._identifier == self._original_identifier + ) + if bulk_target_bid_success: + self._bids.append(bulk_target_bid) + else: + identifier_success, identifier = stream_reader.try_read( + _BulkTargetIdentifier, + lambda x: x == self._original_identifier and x.is_delete_row + ) + if identifier_success: + self._deleted_rows.append(identifier) + else: + has_more_rows = False + if self.target.Id is None and bulk_target_bid is not None and bulk_target_bid.target_id is not None: + self.target.Id = bulk_target_bid.target_id + self.status = 'Active' if self._bids else 'Deleted' + + location = _CAMPAIGN_OBJECT_FACTORY_V10.create('LocationTarget') + self.radius_target.location_target = location + self.location_target.location_target = location + self.negative_location_target.location_target = location + + self.populate_child_target_bids(self.age_target) + self.populate_child_target_bids(self.day_time_target) + self.populate_child_target_bids(self.device_os_target) + self.populate_child_target_bids(self.gender_target) + self.populate_child_target_bids(self.radius_target) + self.populate_child_target_bids(self.location_target) + self.populate_child_target_bids(self.negative_location_target) + + self.populate_child_target_identities(self.age_target) + self.populate_child_target_identities(self.day_time_target) + self.populate_child_target_identities(self.device_os_target) + self.populate_child_target_identities(self.gender_target) + self.populate_child_target_identities(self.radius_target) + self.populate_child_target_identities(self.location_target) + self.populate_child_target_identities(self.negative_location_target) + + self.target.Age = self.age_target.age_target + self.target.DayTime = self.day_time_target.day_time_target + self.target.DeviceOS = self.device_os_target.device_os_target + self.target.Gender = self.gender_target.gender_target + self.target.Location = location + + @property + def all_children_are_present(self): + return all(sub_target.all_children_are_present for sub_target in self.sub_targets) + + @property + def child_entities(self): + return self._sub_targets + + def set_default_identifier(self, bulk_sub_target): + bulk_target_bid_type = type(bulk_sub_target._create_bid()) + if isinstance(self, BulkCampaignTarget): + identifier = _BulkCampaignTargetIdentifier(target_bid_type=bulk_target_bid_type) + elif isinstance(self, BulkAdGroupTarget): + identifier = _BulkAdGroupTargetIdentifier(target_bid_type=bulk_target_bid_type) + else: + raise ValueError('Can only initialize with BulkAdGroupTarget or BulkCampaignTarget.') + identifier.entity_id = self.entity_id + if self.target is not None: + identifier.target_id = self.target.Id + identifier.entity_name = self.entity_name + identifier.parent_entity_name = self.parent_entity_name + if self.status == 'Deleted': + bulk_sub_target.status = 'Deleted' + bulk_sub_target.set_identifier(identifier) + + def populate_child_target_bids(self, bulk_sub_target): + bulk_target_bid_type = type(bulk_sub_target._create_bid()) + bids = [bid for bid in self._bids if type(bid) == bulk_target_bid_type] + if bids: + bulk_sub_target.set_bids(bids) + else: + bulk_sub_target.status = 'Deleted' + + def populate_child_target_identities(self, bulk_sub_target): + bulk_target_bid_type = type(bulk_sub_target._create_bid()) + identifiers = [identifier for identifier in self._deleted_rows if + identifier.target_bid_type == bulk_target_bid_type] + if identifiers: + for identifier in identifiers: + bulk_sub_target.set_identifier(identifier) + else: + self.set_default_identifier(bulk_sub_target) + + @property + def target(self): + """ The associated target. + + :rtype: Target + """ + + return self._target + + @target.setter + def target(self, value): + self._target = value + + @property + def status(self): + """ The status of the target. + + The value is Active if the target is available in the customer's shared library. + The value is Deleted if the target is deleted from the customer's shared library, or should be deleted in a subsequent upload operation. + Corresponds to the 'Status' field in the bulk file. + + :rtype: str + """ + + return self._status + + @status.setter + def status(self, value): + self._status = value + + @property + def entity_id(self): + return self._entity_id + + @entity_id.setter + def entity_id(self, value): + self._entity_id = value + + @property + def entity_name(self): + return self._entity_name + + @entity_name.setter + def entity_name(self, value): + self._entity_name = value + + @property + def parent_entity_name(self): + return self._parent_entity_name + + @parent_entity_name.setter + def parent_entity_name(self, value): + self._parent_entity_name = value + + @property + def age_target(self): + """ The :class:`.BulkAgeTarget` contains multiple :class:`.BulkAgeTargetBid`. + + :rtype: _BulkAgeTarget + """ + + return self._age_target + + @property + def day_time_target(self): + """ The :class:`.BulkDayTimeTarget` contains multiple :class:`.BulkDayTimeTargetBid`. + + :rtype: _BulkDayTimeTarget + """ + + return self._day_time_target + + @property + def device_os_target(self): + """ The :class:`.BulkDeviceOsTarget` contains multiple :class:`.BulkDeviceOsTargetBid`. + + :rtype: _BulkDeviceOsTarget + """ + + return self._device_os_target + + @property + def gender_target(self): + """ The :class:`.BulkGenderTarget` contains multiple :class:`.BulkGenderTargetBid`. + + :rtype: _BulkGenderTarget + """ + + return self._gender_target + + @property + def radius_target(self): + """ The :class:`.BulkRadiusTarget` contains multiple :class:`.BulkRadiusTargetBid`. + + :rtype: _BulkRadiusTarget + """ + + return self._radius_target + + @property + def location_target(self): + """ The :class:`.BulkLocationTarget` contains multiple :class:`.BulkLocationTargetBid`. + + :rtype: _BulkLocationTarget + """ + + return self._location_target + + @property + def negative_location_target(self): + """ The :class:`.BulkNegativeLocationTarget` contains multiple :class:`.BulkNegativeLocationTargetBid`. + + :rtype: _BulkNegativeLocationTarget + """ + + return self._negative_location_target + + @property + def sub_targets(self): + """ The list of sub targets. + + The target contains can include + + * :class:`.LocationTarget` + * :class:`.AgeTarget` + * :class:`.GenderTarget` + * :class:`.DayTimeTarget` + * :class:`.DeviceOsTarget` + * :class:`.NegativeLocationTarget` + * :class:`.RadiusTarget` + + :rtype: list[_BulkSubTarget] + """ + + return self._sub_targets + + +class BulkAdGroupTarget(_BulkTarget): + """ Represents a target that is associated with an ad group. + + The target contains one or more sub targets, + including age, gender, day and time, device OS, and location. Each target can be read or written in a bulk file. + + *Remarks:* + + When requesting downloaded entities of type *BulkAdGroupTarget*, the results will include + Ad Group Age Target, Ad Group DayTime Target, Ad Group DeviceOS Target, Ad Group Gender Target, Ad Group Location Target, + Ad Group Negative Location Target, and Ad Group Radius Target records. + For more information, see Bulk File Schema at http://go.microsoft.com/fwlink/?LinkID=620269. + + For upload you must set the *Target* property, which will effectively replace any existing bids for the corresponding target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + target=None, + status=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, + bulk_target_bid=None, + identifier=None, ): + super(BulkAdGroupTarget, self).__init__( + target=target, + status=status, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + bulk_target_bid=bulk_target_bid, + identifier=identifier, + ) + + @property + def ad_group_id(self): + """ The identifier of the ad group that the target is associated. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._entity_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._entity_id = value + + @property + def ad_group_name(self): + """ The name of the ad group that the target is associated. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._entity_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._entity_name = value + + @property + def campaign_name(self): + """ The name of the ad group that target is associated. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + return self._parent_entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._parent_entity_name = value + + +class BulkCampaignTarget(_BulkTarget): + """ Represents a target that is associated with a campaign. + + The target contains one or more sub targets, + including age, gender, day and time, device OS, and location. Each target can be read or written in a bulk file. + + *Remarks:* + + When requesting downloaded entities of type *BulkCampaignTarget*, the results will include + Ad Group Age Target, Ad Group DayTime Target, Ad Group DeviceOS Target, Ad Group Gender Target, Ad Group Location Target, + Ad Group Negative Location Target, and Ad Group Radius Target records. + For more information, see Bulk File Schema at http://go.microsoft.com/fwlink/?LinkID=620269. + + For upload you must set the *Target* property, which will effectively replace any existing bids for the corresponding target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + target=None, + status=None, + campaign_id=None, + campaign_name=None, + bulk_target_bid=None, + identifier=None, ): + super(BulkCampaignTarget, self).__init__( + target=target, + status=status, + entity_id=campaign_id, + entity_name=campaign_name, + bulk_target_bid=bulk_target_bid, + identifier=identifier, + ) + + @property + def campaign_id(self): + """ The identifier of the campaign that the target is associated. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._entity_id + + @campaign_id.setter + def campaign_id(self, value): + self._entity_id = value + + @property + def campaign_name(self): + """ The name of the campaign that the target is associated. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + + return self._entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._entity_name = value + + +# age target + + +class _BulkAgeTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk age target bid classes. """ + + def __init__(self, + age_target_bid=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._age_target_bid = age_target_bid + super(_BulkAgeTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Target, + field_to_csv=lambda c: c.age_target_bid.Age, + csv_to_field=lambda c, v: setattr(c.age_target_bid, 'Age', v) + ), + _SimpleBulkMapping( + header=_StringTable.BidAdjustment, + field_to_csv=lambda c: bulk_str(c.age_target_bid.BidAdjustment), + csv_to_field=lambda c, v: setattr(c.age_target_bid, 'BidAdjustment', int(v)) + ), + ] + + def _prepare_process_mapping_from_row_values(self): + self.age_target_bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('AgeTargetBid') + + @property + def age_target_bid(self): + """ Defines a list of age ranges to target with bid adjustments. + + :rtype: AgeTargetBid + """ + + return self._age_target_bid + + @age_target_bid.setter + def age_target_bid(self, value): + self._age_target_bid = value + + +class BulkAdGroupAgeTargetBid(_BulkAgeTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one age target bid within an age target that is associated with an ad group. + + This class exposes the :attr:`age_target_bid` property that can be read and written as fields of the Ad Group Age Target record in a bulk file. + + For more information, see Ad Group Age Target at http://go.microsoft.com/fwlink/?LinkID=620260. + + *Remarks:* + + One :class:`.BulkAdGroupAgeTarget` exposes a read only list of :class:`.BulkAdGroupAgeTargetBid`. + Each :class:`.BulkAdGroupAgeTargetBid` instance corresponds to one Ad Group Age Target record in the bulk file. + If you upload a :class:`.BulkAdGroupAgeTarget`, then you are effectively replacing any existing bids for the corresponding age target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + age_target_bid=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkAgeTargetBid.__init__( + self, + age_target_bid=age_target_bid, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignAgeTargetBid(_BulkAgeTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one age target bid within an age target that is associated with a campaign. + + This class exposes the :attr:`.age_target_bid` property that can be read and written as fields of the Campaign Age Target record in a bulk file. + For more information, see Campaign Age Target at http://go.microsoft.com/fwlink/?LinkID=620248. + + *Remarks:* + + One :class:`.BulkCampaignAgeTarget` exposes a read only list of :class:`.BulkCampaignAgeTargetBid`. + Each :class:`.BulkCampaignAgeTargetBid` instance corresponds to one Campaign Age Target record in the bulk file. + If you upload a :class:`.BulkCampaignAgeTarget`, then you are effectively replacing any existing bids for the corresponding age target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + age_target_bid=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkAgeTargetBid.__init__( + self, + age_target_bid=age_target_bid, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkAgeTarget(_BulkSubTarget): + """ This base class provides properties that are shared by all bulk age target classes. """ + + def __init__(self, + age_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + self._age_target = age_target + super(_BulkAgeTarget, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def _create_bid(self): + raise NotImplementedError() + + def _validate_properties(self): + self._validate_property_not_null(self.age_target, 'age_target') + + def _validate_bids(self): + if self.age_target is not None: + self._validate_list_not_null_or_empty(self.age_target.Bids, self.age_target.Bids.AgeTargetBid, + 'age_target.Bids') + + def _convert_target_to_bulk_entities(self): + if self.age_target is None or self.age_target.Bids is None: + return + for bid in self.age_target.Bids.AgeTargetBid: + bulk_bid = self._create_and_populate_bid() + bulk_bid.age_target_bid = bid + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.age_target = _CAMPAIGN_OBJECT_FACTORY_V10.create('AgeTarget') + for bid in self.child_entities: + self.age_target.Bids.AgeTargetBid.append(bid.age_target_bid) + + @property + def age_target(self): + """ Defines a list of age ranges to target with bid adjustments. + + :rtype: AgeTarget + """ + + return self._age_target + + @age_target.setter + def age_target(self, value): + self._age_target = value + + +class BulkAdGroupAgeTarget(_BulkAgeTarget, _BulkSubTargetAdGroupMixin): + """ Represents an age target that is associated with an ad group. + + This class exposes the :attr:`AgeTarget` property that can be read and written as fields of the Ad Group Age Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkAdGroupAgeTarget` exposes a read only list of :class:`.BulkAdGroupAgeTargetBid`. + Each :class:`.BulkAdGroupAgeTargetBid` instance corresponds to one Ad Group Age Target record in the bulk file. + If you upload a :class:`.BulkAdGroupAgeTarget`, then you are effectively replacing any existing bids for the corresponding age target. + + For more information, see Ad Group Age Target at http://go.microsoft.com/fwlink/?LinkID=620260. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + age_target=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkAgeTarget.__init__( + self, + age_target=age_target, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupAgeTargetBid() + + +class BulkCampaignAgeTarget(_BulkAgeTarget, _BulkSubTargetCampaignMixin): + """ Represents an age target that is associated with a campaign. + + The age target contains one or more age target bids. + This class exposes the :attr:`AgeTarget` property that can be read and written as fields of the Campaign Age Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkCampaignAgeTarget` exposes a read only list of :class:`.BulkCampaignAgeTargetBid`. + Each :class:`.BulkCampaignAgeTargetBid` instance corresponds to one Campaign Age Target record in the bulk file. + If you upload a :class:`BulkCampaignAgeTarget`, then you are effectively replacing any existing bids for the corresponding age target. + + For more information, see Campaign Age Target at http://go.microsoft.com/fwlink/?LinkID=620248. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + age_target=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkAgeTarget.__init__( + self, + age_target=age_target, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignAgeTargetBid() + + +class _BulkDayTimeTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk day and time target bid classes. """ + + def __init__(self, + day_time_target_bid=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._day_time_target_bid = day_time_target_bid + super(_BulkDayTimeTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Target, + field_to_csv=lambda c: bulk_str(c.day_time_target_bid.Day), + csv_to_field=lambda c, v: setattr(c.day_time_target_bid, 'Day', v), + ), + _SimpleBulkMapping( + header=_StringTable.FromHour, + field_to_csv=lambda c: bulk_str(c.day_time_target_bid.FromHour), + csv_to_field=lambda c, v: setattr(c.day_time_target_bid, 'FromHour', int(v)), + ), + _SimpleBulkMapping( + header=_StringTable.ToHour, + field_to_csv=lambda c: bulk_str(c.day_time_target_bid.ToHour), + csv_to_field=lambda c, v: setattr(c.day_time_target_bid, 'ToHour', int(v)), + ), + _SimpleBulkMapping( + header=_StringTable.FromMinute, + field_to_csv=lambda c: minute_bulk_str(c.day_time_target_bid.FromMinute), + csv_to_field=lambda c, v: setattr(c.day_time_target_bid, 'FromMinute', parse_minute(v)), + ), + _SimpleBulkMapping( + header=_StringTable.ToMinute, + field_to_csv=lambda c: minute_bulk_str(c.day_time_target_bid.ToMinute), + csv_to_field=lambda c, v: setattr(c.day_time_target_bid, 'ToMinute', parse_minute(v)), + ), + _SimpleBulkMapping( + header=_StringTable.BidAdjustment, + field_to_csv=lambda c: bulk_str(c.day_time_target_bid.BidAdjustment), + csv_to_field=lambda c, v: setattr(c.day_time_target_bid, 'BidAdjustment', int(v)) + ) + ] + + @property + def day_time_target_bid(self): + """ Defines a specific day of the week and time range to target. + + :rtype: DayTimeTargetBid + """ + + return self._day_time_target_bid + + @day_time_target_bid.setter + def day_time_target_bid(self, value): + self._day_time_target_bid = value + + def _prepare_process_mapping_from_row_values(self): + self._day_time_target_bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('DayTimeTargetBid') + + +class BulkAdGroupDayTimeTargetBid(_BulkDayTimeTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one day and time target bid within a day and time target that is associated with an ad group. + + This class exposes the :attr:`day_time_target_bid` property that can be read and written as fields of the Ad Group DayTime Target record in a bulk file. + + For more information, see Ad Group DayTime Target at http://go.microsoft.com/fwlink/?LinkID=620278. + + *Remarks* + + One :class:`.BulkAdGroupDayTimeTarget` exposes a read only list of :class:`.BulkAdGroupDayTimeTargetBid`. + Each :class:`.BulkAdGroupDayTimeTargetBid` instance corresponds to one Ad Group DayTime Target record in the bulk file. + If you upload a :class:`.BulkAdGroupDayTimeTarget`, then you are effectively replacing any existing bids for the corresponding day and time target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + day_time_target_bid=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkDayTimeTargetBid.__init__( + self, + day_time_target_bid=day_time_target_bid, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignDayTimeTargetBid(_BulkDayTimeTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one day and time target bid within a day and time target that is associated with an campaign. + + This class exposes the :attr:`day_time_target_bid` property that can be read and written as fields of the Campaign DayTime Target record in a bulk file. + + For more information, see Campaign DayTime Target at http://go.microsoft.com/fwlink/?LinkID=620279. + + *Remarks* + + One :class:`.BulkCampaignDayTimeTarget` exposes a read only list of :class:`.BulkCampaignDayTimeTargetBid`. + Each :class:`.BulkCampaignDayTimeTargetBid` instance corresponds to one Campaign DayTime Target record in the bulk file. + If you upload a :class:`.BulkCampaignDayTimeTarget`, then you are effectively replacing any existing bids for the corresponding day and time target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + day_time_target_bid=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkDayTimeTargetBid.__init__( + self, + day_time_target_bid=day_time_target_bid, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkDayTimeTarget(_BulkSubTarget): + """ This base class provides properties that are shared by all bulk day and time target classes. """ + + def __init__(self, + day_time_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + self._day_time_target = day_time_target + super(_BulkDayTimeTarget, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def _create_bid(self): + raise NotImplementedError() + + def _validate_properties(self): + self._validate_property_not_null(self.day_time_target, 'day_time_target') + + def _validate_bids(self): + if self.day_time_target is not None: + self._validate_list_not_null_or_empty(self.day_time_target.Bids, self.day_time_target.Bids.DayTimeTargetBid, + 'day_time_target.Bids') + + def _convert_target_to_bulk_entities(self): + if self.day_time_target is None or self.day_time_target.Bids is None: + return + for bid in self.day_time_target.Bids.DayTimeTargetBid: + bulk_bid = self._create_and_populate_bid() + bulk_bid.day_time_target_bid = bid + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.day_time_target = _CAMPAIGN_OBJECT_FACTORY_V10.create('DayTimeTarget') + for bid in self.child_entities: + self.day_time_target.Bids.DayTimeTargetBid.append(bid.day_time_target_bid) + + @property + def day_time_target(self): + """ Defines a list of days of the week and time ranges to target with bid adjustments. + + :rtype: DayTimeTarget + """ + + return self._day_time_target + + @day_time_target.setter + def day_time_target(self, value): + self._day_time_target = value + + +class BulkAdGroupDayTimeTarget(_BulkDayTimeTarget, _BulkSubTargetAdGroupMixin): + """ Represents a day and time target that is associated with an ad group. + + This class exposes the :attr:`day_time_target` property that can be read and written as fields of the Ad Group DayTime Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkAdGroupDayTimeTarget` exposes a read only list of :class:`.BulkAdGroupDayTimeTargetBid`. + Each :class:`.BulkAdGroupDayTimeTargetBid` instance corresponds to one Ad Group DayTime Target record in the bulk file. + If you upload a :class:`.BulkAdGroupDayTimeTarget`, then you are effectively replacing any existing bids for the corresponding day and time target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + day_time_target=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkDayTimeTarget.__init__( + self, + day_time_target=day_time_target, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupDayTimeTargetBid() + + +class BulkCampaignDayTimeTarget(_BulkDayTimeTarget, _BulkSubTargetCampaignMixin): + """ Represents a day and time target that is associated with a campaign. + + This class exposes the :attr:`day_time_target` property that can be read and written as fields of the Campaign DayTime Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkCampaignDayTimeTarget` exposes a read only list of :class:`.BulkCampaignDayTimeTargetBid`. + Each :class:`.BulkCampaignDayTimeTargetBid` instance corresponds to one Campaign DayTime Target record in the bulk file. + If you upload a :class:`.BulkCampaignDayTimeTarget`, then you are effectively replacing any existing bids for the corresponding day and time target. + + For more information, see Campaign DayTime Target at http://go.microsoft.com/fwlink/?LinkID=620279. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + day_time_target=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkDayTimeTarget.__init__( + self, + day_time_target=day_time_target, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignDayTimeTargetBid() + + +class _BulkDeviceOsTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk device OS target bid classes. """ + + def __init__(self, + device_os_target_bid=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._device_os_target_bid = device_os_target_bid + super(_BulkDeviceOsTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Target, + field_to_csv=lambda c: bulk_str(c.device_os_target_bid.DeviceName), + csv_to_field=lambda c, v: setattr(c.device_os_target_bid, 'DeviceName', v), + ), + _SimpleBulkMapping( + header=_StringTable.OsNames, + field_to_csv=lambda c: ';'.join( + c.device_os_target_bid.OSNames.string) if c.device_os_target_bid.OSNames.string else None, + csv_to_field=lambda c, v: setattr( + c.device_os_target_bid.OSNames, + 'string', + list(filter(None, v.split(';'))) if v else [], + ), + ), + _SimpleBulkMapping( + header=_StringTable.BidAdjustment, + field_to_csv=lambda c: bulk_str(c.device_os_target_bid.BidAdjustment), + csv_to_field=lambda c, v: setattr(c.device_os_target_bid, 'BidAdjustment', int(v)) + ), + ] + + @property + def device_os_target_bid(self): + """ Defines a specific device to target. + + :rtype: DeviceOSTargetBid + """ + + return self._device_os_target_bid + + @device_os_target_bid.setter + def device_os_target_bid(self, value): + self._device_os_target_bid = value + + def _prepare_process_mapping_from_row_values(self): + self._device_os_target_bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('DeviceOSTargetBid') + + +class BulkAdGroupDeviceOsTargetBid(_BulkDeviceOsTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one device OS target bid within a device OS target that is associated with an ad group. + + This class exposes the :attr:`device_os_target_bid` property that can be read and written as fields of the Ad Group DeviceOS Target record in a bulk file. + + For more information, see Ad Group DeviceOS Target at http://go.microsoft.com/fwlink/?LinkID=620247. + + *Remarks:* + + One :class:`.BulkAdGroupDeviceOsTarget` exposes a read only list of :class:`.BulkAdGroupDeviceOsTargetBid`. + Each :class:`BulkAdGroupDeviceOsTargetBid` instance corresponds to one Ad Group DeviceOS Target record in the bulk file. + If you upload a :class:`BulkAdGroupDeviceOsTarget`, then you are effectively replacing any existing bids for the corresponding device OS target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + device_os_target_bid=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkDeviceOsTargetBid.__init__( + self, + device_os_target_bid=device_os_target_bid, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignDeviceOsTargetBid(_BulkDeviceOsTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one device OS target bid within a device OS target that is associated with a campaign. + + This class exposes the :attr:`device_os_target_bid` property that can be read and written as fields of the Campaign DeviceOS Target record in a bulk file. + + For more information, see Campaign DeviceOS Target at http://go.microsoft.com/fwlink/?LinkID=620247. + + *Remarks:* + + One :class:`.BulkCampaignDeviceOsTarget` exposes a read only list of :class:`.BulkCampaignDeviceOsTargetBid`. + Each :class:`.BulkCampaignDeviceOsTargetBid` instance corresponds to one Campaign DeviceOS Target record in the bulk file. + If you upload a :class:`.BulkCampaignDeviceOsTarget`, then you are effectively replacing any existing bids for the corresponding device OS target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + device_os_target_bid=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkDeviceOsTargetBid.__init__( + self, + device_os_target_bid=device_os_target_bid, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkDeviceOsTarget(_BulkSubTarget): + """ This base class provides properties that are shared by all bulk device OS target classes. """ + + def __init__(self, + device_os_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + self._device_os_target = device_os_target + super(_BulkDeviceOsTarget, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def _create_bid(self): + raise NotImplementedError() + + def _validate_properties(self): + self._validate_property_not_null(self.device_os_target, 'device_os_target') + + def _validate_bids(self): + if self.device_os_target is not None: + self._validate_list_not_null_or_empty( + self.device_os_target.Bids, + self.device_os_target.Bids.DeviceOSTargetBid, + 'device_os_target.Bids' + ) + + def _convert_target_to_bulk_entities(self): + if self.device_os_target is None or self.device_os_target.Bids is None: + return + for bid in self.device_os_target.Bids.DeviceOSTargetBid: + bulk_bid = self._create_and_populate_bid() + bulk_bid.device_os_target_bid = bid + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.device_os_target = _CAMPAIGN_OBJECT_FACTORY_V10.create('DeviceOSTarget') + for bid in self.child_entities: + self.device_os_target.Bids.DeviceOSTargetBid.append(bid.device_os_target_bid) + + @property + def device_os_target(self): + """ Defines a list of devices to target with bid adjustments. + + :rtype: DeviceOSTarget + """ + + return self._device_os_target + + @device_os_target.setter + def device_os_target(self, value): + self._device_os_target = value + + +class BulkAdGroupDeviceOsTarget(_BulkDeviceOsTarget, _BulkSubTargetAdGroupMixin): + """ Represents a device OS target that is associated with an ad group. + + This class exposes the :attr:`device_os_target` property that can be read and written as fields of the Ad Group DeviceOS Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkAdGroupDeviceOsTarget` exposes a read only list of :class`.BulkAdGroupDeviceOsTargetBid`. + Each :class:`.BulkAdGroupDeviceOsTargetBid` instance corresponds to one Ad Group DeviceOS Target record in the bulk file. + If you upload a :class:`.BulkAdGroupDeviceOsTarget`, then you are effectively replacing any existing bids for the corresponding device OS target. + + For more information, see Ad Group DeviceOS Target at http://go.microsoft.com/fwlink/?LinkID=620247. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + device_os_target=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkDeviceOsTarget.__init__( + self, + device_os_target=device_os_target, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupDeviceOsTargetBid() + + +class BulkCampaignDeviceOsTarget(_BulkDeviceOsTarget, _BulkSubTargetCampaignMixin): + """ Represents a device OS target that is associated with a campaign. + + This class exposes the :attr:`device_os_target` property that can be read and written as fields of the Campaign DeviceOS Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkCampaignDeviceOsTarget` exposes a read only list of :class:`.BulkCampaignDeviceOsTargetBid`. + Each :class:`.`BulkCampaignDeviceOsTargetBid` instance corresponds to one Campaign DeviceOS Target record in the bulk file. + If you upload a :class:`.BulkCampaignDeviceOsTarget`, then you are effectively replacing any existing bids for the corresponding device OS target + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter`. + """ + + def __init__(self, + device_os_target=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkDeviceOsTarget.__init__( + self, + device_os_target=device_os_target, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignDeviceOsTargetBid() + + +class _BulkGenderTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk gender target bid classes. """ + + def __init__(self, + gender_target_bid=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._gender_target_bid = gender_target_bid + super(_BulkGenderTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Target, + field_to_csv=lambda c: bulk_str(c.gender_target_bid.Gender), + csv_to_field=lambda c, v: setattr(c.gender_target_bid, 'Gender', v), + ), + _SimpleBulkMapping( + header=_StringTable.BidAdjustment, + field_to_csv=lambda c: bulk_str(c.gender_target_bid.BidAdjustment), + csv_to_field=lambda c, v: setattr(c.gender_target_bid, 'BidAdjustment', int(v)) + ) + ] + + @property + def gender_target_bid(self): + """ Defines a specific gender target. + + :rtype: GenderTargetBid + """ + + return self._gender_target_bid + + @gender_target_bid.setter + def gender_target_bid(self, value): + self._gender_target_bid = value + + def _prepare_process_mapping_from_row_values(self): + self._gender_target_bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('GenderTargetBid') + + +class BulkAdGroupGenderTargetBid(_BulkGenderTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one gender target bid within a gender target that is associated with an ad group. + + This class exposes the :attr:`gender_target_bid` property that can be read and written as fields of the Ad Group Gender Target record in a bulk file. + + For more information, see Ad Group Gender Target at http://go.microsoft.com/fwlink/?LinkID=620258. + + *Remarks:* + + One :class:`.BulkAdGroupGenderTarget` exposes a read only list of :class:`.BulkAdGroupGenderTargetBid`. + Each :class`.BulkAdGroupGenderTargetBid` instance corresponds to one Ad Group Gender Target record in the bulk file. + If you upload a :class:`.BulkAdGroupGenderTarget`, then you are effectively replacing any existing bids for the corresponding gender target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + gender_target_bid=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkGenderTargetBid.__init__( + self, + gender_target_bid=gender_target_bid, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignGenderTargetBid(_BulkGenderTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one gender target bid within a gender target that is associated with a campaign. + + This class exposes the :attr:`gender_target_bid` property that can be read and written as fields of the Campaign Gender Target record in a bulk file. + + For more information, see Campaign Gender Target at http://go.microsoft.com/fwlink/?LinkID=620246. + + *Remarks:* + One :class:`.BulkCampaignGenderTarget` exposes a read only list of :class:`.BulkCampaignGenderTargetBid`. + Each :class:`.BulkCampaignGenderTargetBid` instance corresponds to one Campaign Gender Target record in the bulk file. + If you upload a :class:`.BulkCampaignGenderTarget`, then you are effectively replacing any existing bids for the corresponding gender target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + gender_target_bid=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkGenderTargetBid.__init__( + self, + gender_target_bid=gender_target_bid, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkGenderTarget(_BulkSubTarget): + """ This base class provides properties that are shared by all bulk gender target classes. """ + + def __init__(self, + gender_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None, ): + self._gender_target = gender_target + super(_BulkGenderTarget, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def _create_bid(self): + raise NotImplementedError() + + def _validate_properties(self): + self._validate_property_not_null(self.gender_target, 'gender_target') + + def _validate_bids(self): + if self.gender_target is not None: + self._validate_list_not_null_or_empty( + self.gender_target.Bids, + self.gender_target.Bids.GenderTargetBid, + 'gender_target.Bids' + ) + + def _convert_target_to_bulk_entities(self): + if self.gender_target is None or self.gender_target.Bids is None: + return + for bid in self.gender_target.Bids.GenderTargetBid: + bulk_bid = self._create_and_populate_bid() + bulk_bid.gender_target_bid = bid + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.gender_target = _CAMPAIGN_OBJECT_FACTORY_V10.create('GenderTarget') + for bid in self.child_entities: + self.gender_target.Bids.GenderTargetBid.append(bid.gender_target_bid) + + @property + def gender_target(self): + """ Defines a list of genders to target with bid adjustments. + + :rtype: GenderTarget + """ + + return self._gender_target + + @gender_target.setter + def gender_target(self, value): + self._gender_target = value + + +class BulkAdGroupGenderTarget(_BulkGenderTarget, _BulkSubTargetAdGroupMixin): + """ Represents a gender target that is associated with an ad group. + + This class exposes the :attr:`gender_target` property that can be read and written as fields of the Ad Group Gender Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkAdGroupGenderTarget` exposes a read only list of :class:`.BulkAdGroupGenderTargetBid`. + Each :class:`.BulkAdGroupGenderTargetBid` instance corresponds to one Ad Group Gender Target record in the bulk file. + If you upload a :class:`.BulkAdGroupGenderTarget`, then you are effectively replacing any existing bids for the corresponding gender target. + + For more information, see Ad Group Gender Target at http://go.microsoft.com/fwlink/?LinkID=620258. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + gender_target=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkGenderTarget.__init__( + self, + gender_target=gender_target, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupGenderTargetBid() + + +class BulkCampaignGenderTarget(_BulkGenderTarget, _BulkSubTargetCampaignMixin): + """ Represents a gender target that is associated with an campaign. + + This class exposes the :attr:`gender_target` property that can be read and written as fields of the Campaign Gender Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkCampaignGenderTarget` exposes a read only list of :class:`.BulkCampaignGenderTargetBid`. + Each :class:`.BulkCampaignGenderTargetBid` instance corresponds to one Campaign Gender Target record in the bulk file. + If you upload a :class:`.BulkCampaignGenderTarget`, then you are effectively replacing any existing bids for the corresponding gender target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + gender_target=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkGenderTarget.__init__( + self, + gender_target=gender_target, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignGenderTargetBid() + + +class _BulkRadiusTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk radius target bid classes. """ + + def __init__(self, + radius_target_bid=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._radius_target_bid = radius_target_bid + self._intent_option = None + super(_BulkRadiusTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.RadiusTargetId, + field_to_csv=lambda c: bulk_str(c.radius_target_bid.Id), + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'Id', int(v) if v else None), + ), + _SimpleBulkMapping( + header=_StringTable.Name, + field_to_csv=lambda c: c.radius_target_bid.Name, + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'Name', v), + ), + _SimpleBulkMapping( + header=_StringTable.Radius, + field_to_csv=lambda c: bulk_str(c.radius_target_bid.Radius), + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'Radius', float(v)), + ), + _SimpleBulkMapping( + header=_StringTable.Unit, + field_to_csv=lambda c: c.radius_target_bid.RadiusUnit, + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'RadiusUnit', v), + ), + _SimpleBulkMapping( + header=_StringTable.Latitude, + field_to_csv=lambda c: bulk_str(c.radius_target_bid.LatitudeDegrees), + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'LatitudeDegrees', float(v)), + ), + _SimpleBulkMapping( + header=_StringTable.Longitude, + field_to_csv=lambda c: bulk_str(c.radius_target_bid.LongitudeDegrees), + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'LongitudeDegrees', float(v)), + ), + _SimpleBulkMapping( + header=_StringTable.BidAdjustment, + field_to_csv=lambda c: bulk_str(c.radius_target_bid.BidAdjustment), + csv_to_field=lambda c, v: setattr(c.radius_target_bid, 'BidAdjustment', int(v)), + ), + _SimpleBulkMapping( + header=_StringTable.PhysicalIntent, + field_to_csv=lambda c: bulk_str(c.intent_option), + csv_to_field=lambda c, v: setattr(c, '_intent_option', v if v else None), + ), + ] + + @property + def radius_target_bid(self): + """ Defines a specific geographical radius to target. + + :rtype: RadiusTargetBid + """ + + return self._radius_target_bid + + @radius_target_bid.setter + def radius_target_bid(self, value): + self._radius_target_bid = value + + @property + def intent_option(self): + """ Defines the possible intent options for location targeting. + + :rtype: str + """ + + return self._intent_option + + def _prepare_process_mapping_from_row_values(self): + self._radius_target_bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('RadiusTargetBid') + + +class BulkAdGroupRadiusTargetBid(_BulkRadiusTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one radius target bid within a radius target that is associated with an ad group. + + This class exposes the :attr:`radius_target_bid` property that can be read and written as fields of the Ad Group Radius Target record in a bulk file. + + For more information, see Ad Group Radius Target at http://go.microsoft.com/fwlink/?LinkID=620257. + + *Remarks:* + + One :class:`.BulkAdGroupRadiusTarget` exposes a read only list of :class:`.BulkAdGroupRadiusTargetBid`. + Each :class:`.BulkAdGroupRadiusTargetBid` instance corresponds to one Ad Group Radius Target record in the bulk file. + If you upload a :class:`.BulkAdGroupRadiusTarget`, then you are effectively replacing any existing bids for the corresponding radius target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + radius_target_bid=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkRadiusTargetBid.__init__( + self, + radius_target_bid=radius_target_bid, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignRadiusTargetBid(_BulkRadiusTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one radius target bid within a radius target that is associated with a campaign. + + This class exposes the :attr:`radius_target_bid` property that can be read and written as fields of the Campaign Radius Target record in a bulk file. + + For more information, see Campaign Radius Target at http://go.microsoft.com/fwlink/?LinkID=620245. + + *Remarks:* + + One :class:`.BulkCampaignRadiusTarget` exposes a read only list of :class:`.BulkCampaignRadiusTargetBid`. + Each :class:`.BulkCampaignRadiusTargetBid` instance corresponds to one Campaign Radius Target record in the bulk file. + If you upload a :class:`.BulkCampaignRadiusTarget`, then you are effectively replacing any existing bids for the corresponding radius target. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + radius_target_bid=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkRadiusTargetBid.__init__( + self, + radius_target_bid=radius_target_bid, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkTargetWithLocation(_BulkSubTarget): + """ This base class provides properties that are shared by all bulk entities mapped to the API LocationTarget object. """ + + def __init__(self, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + super(_BulkTargetWithLocation, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + self._location_target = None + + @property + def location_target(self): + return self._location_target + + @location_target.setter + def location_target(self, value): + self._location_target = value + + def ensure_location_target(self): + if self.location_target is None: + self.location_target = _CAMPAIGN_OBJECT_FACTORY_V10.create('LocationTarget') + + +class _BulkRadiusTarget(_BulkTargetWithLocation): + """ This base class provides properties that are shared by all bulk radius target classes. """ + + def __init__(self, + radius_target=None, + intent_option=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + super(_BulkRadiusTarget, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + if radius_target is not None: + self.radius_target = radius_target + if intent_option is not None: + self.intent_option = intent_option + + def _validate_properties(self): + self._validate_property_not_null(self.radius_target, 'radius_target') + + def _validate_bids(self): + if self.radius_target is not None: + self._validate_list_not_null_or_empty( + self.radius_target.Bids, + self.radius_target.Bids.RadiusTargetBid, + 'radius_target.Bids' + ) + + def _convert_target_to_bulk_entities(self): + if self.radius_target is None or self.radius_target.Bids is None: + return + for bid in self.radius_target.Bids.RadiusTargetBid: + bulk_bid = self._create_and_populate_bid() + bulk_bid.radius_target_bid = bid + bulk_bid._intent_option = self.location_target.IntentOption + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.ensure_location_target() + for bid in self.child_entities: + self.radius_target.Bids.RadiusTargetBid.append(bid.radius_target_bid) + if self.child_entities: + bid = self.child_entities[0] + if bid.intent_option is not None: + self.location_target.IntentOption = bid.intent_option + + @property + def radius_target(self): + """ Defines a list of geographical radius targets with bid adjustments. + + :rtype: RadiusTarget + """ + + if self.location_target is None: + return None + return self.location_target.RadiusTarget + + @radius_target.setter + def radius_target(self, value): + self.ensure_location_target() + self.location_target.RadiusTarget = value + + @property + def intent_option(self): + """ Defines the possible intent options for location targeting. + + :rtype: str + """ + + if self.location_target is None: + return None + return self.location_target.IntentOption + + @intent_option.setter + def intent_option(self, value): + self.ensure_location_target() + self.location_target.IntentOption = value + + +class BulkCampaignRadiusTarget(_BulkRadiusTarget, _BulkSubTargetCampaignMixin): + """ Represents a radius target that is associated with a campaign. + + This class exposes the :attr:`radius_target` property that can be read and written as fields of the Campaign Radius Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkCampaignRadiusTarget` exposes a read only list of :class:`.BulkCampaignRadiusTargetBid`. + Each :class:`.BulkCampaignRadiusTargetBid` instance corresponds to one Campaign Radius Target record in the bulk file. + If you upload a :class`.BulkCampaignRadiusTarget`, then you are effectively replacing any existing bids for the corresponding radius target. + + For more information, see Campaign Radius Target at http://go.microsoft.com/fwlink/?LinkID=620245. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + radius_target=None, + intent_option=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkRadiusTarget.__init__( + self, + radius_target=radius_target, + intent_option=intent_option, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignRadiusTargetBid() + + +class BulkAdGroupRadiusTarget(_BulkRadiusTarget, _BulkSubTargetAdGroupMixin): + """ Represents a radius target that is associated with an ad group. + + This class exposes the :attr:`radius_target` property that can be read and written as fields of the Ad Group Radius Target record in a bulk file. + + *Remarks:* + + One :class:`.BulkAdGroupRadiusTarget` exposes a read only list of :class:`.BulkAdGroupRadiusTargetBid`. + Each :class:`.BulkAdGroupRadiusTargetBid` instance corresponds to one Ad Group Radius Target record in the bulk file. + If you upload a :class:`.BulkAdGroupRadiusTarget`, then you are effectively replacing any existing bids for the corresponding radius target. + + For more information, see Ad Group Radius Target at http://go.microsoft.com/fwlink/?LinkID=620257. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + radius_target=None, + intent_option=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkRadiusTarget.__init__( + self, + radius_target=radius_target, + intent_option=intent_option, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupRadiusTargetBid() + + +class _BulkLocationTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk location target bid classes. """ + + def __init__(self, + location=None, + location_type=None, + bid_adjustment=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._location = location + self._location_type = location_type + self._bid_adjustment = bid_adjustment + self._intent_option = None + super(_BulkLocationTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Target, + field_to_csv=lambda c: bulk_str(c.location), + csv_to_field=lambda c, v: setattr(c, '_location', v), + ), + _SimpleBulkMapping( + header=_StringTable.SubType, + field_to_csv=lambda c: location_target_type_bulk_str(c.location_type), + csv_to_field=lambda c, v: setattr(c, '_location_type', parse_location_target_type(v)), + ), + _SimpleBulkMapping( + header=_StringTable.PhysicalIntent, + field_to_csv=lambda c: bulk_str(c.intent_option), + csv_to_field=lambda c, v: setattr(c, '_intent_option', v if v else None), + ), + _SimpleBulkMapping( + header=_StringTable.BidAdjustment, + field_to_csv=lambda c: bulk_str(c.bid_adjustment), + csv_to_field=lambda c, v: setattr(c, '_bid_adjustment', int(v)) + ) + ] + + def _prepare_process_mapping_from_row_values(self): + pass + + @property + def location(self): + return self._location + + @location.setter + def location(self, value): + self._location = value + + @property + def location_type(self): + return self._location_type + + @location_type.setter + def location_type(self, value): + self._location_type = value + + @property + def intent_option(self): + """ Defines the possible intent options for location targeting. + + :rtype: str + """ + + return self._intent_option + + @property + def bid_adjustment(self): + """ The percentage adjustment to the base bid. + + :rtype: int + """ + + return self._bid_adjustment + + @bid_adjustment.setter + def bid_adjustment(self, value): + self._bid_adjustment = value + + +class BulkAdGroupLocationTargetBid(_BulkLocationTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one sub location target bid within a location target that is associated with an ad group. + + This class exposes properties that can be read and written as fields of the Ad Group Location Target record in a bulk file. + + For more information, see Ad Group Location Target at http://go.microsoft.com/fwlink/?LinkID=620255. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + location=None, + location_type=None, + bid_adjustment=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None): + _BulkLocationTargetBid.__init__( + self, + location=location, + location_type=location_type, + bid_adjustment=bid_adjustment, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignLocationTargetBid(_BulkLocationTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one sub location target bid within a location target that is associated with a campaign. + + This class exposes properties that can be read and written as fields of the Campaign Location Target record in a bulk file. + + For more information, see Campaign Location Target at http://go.microsoft.com/fwlink/?LinkID=620243. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + location=None, + location_type=None, + bid_adjustment=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None): + _BulkLocationTargetBid.__init__( + self, + location=location, + location_type=location_type, + bid_adjustment=bid_adjustment, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkLocationTargetWithStringLocation(_BulkTargetWithLocation): + """ This base class provides properties that are shared by all bulk location target classes. """ + + LOCATION_TYPES = ['City', 'MetroArea', 'State', 'Country', 'PostalCode'] + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + super(_BulkLocationTargetWithStringLocation, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + if city_target is not None: + self.city_target = city_target + if metro_area_target is not None: + self.metro_area_target = metro_area_target + if state_target is not None: + self.state_target = state_target + if country_target is not None: + self.country_target = country_target + if postal_code_target is not None: + self.postal_code_target = postal_code_target + + def _validate_properties(self): + self._validate_property_not_null(self.location_target, 'location_target') + + def _validate_bids(self): + if self.location_target is not None: + if not ( + any(self.city_target.Bids.CityTargetBid) or + any(self.metro_area_target.Bids.MetroAreaTargetBid) or + any(self.state_target.Bids.StateTargetBid) or + any(self.country_target.Bids.CountryTargetBid) or + any(self.postal_code_target.Bids.PostalCodeTargetBid) + ): + raise ValueError('Sub target bid in Location Target cannot all be empty') + + @property + def city_target(self): + """ Defines a list of cities to target with bid adjustments. + + :rtype: CityTarget + """ + + if self.location_target is None: + return None + return self.location_target.CityTarget + + @city_target.setter + def city_target(self, value): + self.ensure_location_target() + self.location_target.CityTarget = value + + @property + def metro_area_target(self): + """ Defines a list of metro areas to target with bid adjustments. + + :rtype: MetroAreaTarget + """ + + if self.location_target is None: + return None + return self.location_target.MetroAreaTarget + + @metro_area_target.setter + def metro_area_target(self, value): + self.ensure_location_target() + self.location_target.MetroAreaTarget = value + + @property + def state_target(self): + """ Defines a list of states to target with bid adjustments. + + :rtype: StateTarget + """ + + if self.location_target is None: + return None + return self.location_target.StateTarget + + @state_target.setter + def state_target(self, value): + self.ensure_location_target() + self.location_target.StateTarget = value + + @property + def country_target(self): + """ Defines a list of countries to target with bid adjustments. + + :rtype: CountryTarget + """ + + if self.location_target is None: + return None + return self.location_target.CountryTarget + + @country_target.setter + def country_target(self, value): + self.ensure_location_target() + self.location_target.CountryTarget = value + + @property + def postal_code_target(self): + """ Defines a list of postal codes to target with bid adjustments. + + :rtype: PostalCodeTarget + """ + + if self.location_target is None: + return None + return self.location_target.PostalCodeTarget + + @postal_code_target.setter + def postal_code_target(self, value): + self.ensure_location_target() + self.location_target.PostalCodeTarget = value + + +class _BulkLocationTarget(_BulkLocationTargetWithStringLocation): + """ A base class for all bulk location target classes. """ + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + super(_BulkLocationTarget, self).__init__( + city_target=city_target, + metro_area_target=metro_area_target, + state_target=state_target, + country_target=country_target, + postal_code_target=postal_code_target, + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def _convert_target_to_bulk_entities(self): + for location_type in self.LOCATION_TYPES: + if location_type == 'City': + if self.city_target is None or self.city_target.Bids is None: + continue + for bid in self.city_target.Bids.CityTargetBid: + if bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.City + bulk_bid.location_type = location_type + bulk_bid.bid_adjustment = bid.BidAdjustment + bulk_bid._intent_option = self.location_target.IntentOption + yield bulk_bid + elif location_type == 'MetroArea': + if self.metro_area_target is None or self.metro_area_target.Bids is None: + continue + for bid in self.metro_area_target.Bids.MetroAreaTargetBid: + if bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.MetroArea + bulk_bid.location_type = location_type + bulk_bid.bid_adjustment = bid.BidAdjustment + bulk_bid._intent_option = self.location_target.IntentOption + yield bulk_bid + elif location_type == 'State': + if self.state_target is None or self.state_target.Bids is None: + continue + for bid in self.state_target.Bids.StateTargetBid: + if bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.State + bulk_bid.location_type = location_type + bulk_bid.bid_adjustment = bid.BidAdjustment + bulk_bid._intent_option = self.location_target.IntentOption + yield bulk_bid + elif location_type == 'Country': + if self.country_target is None or self.country_target.Bids is None: + continue + for bid in self.country_target.Bids.CountryTargetBid: + if bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.CountryAndRegion + bulk_bid.location_type = location_type + bulk_bid.bid_adjustment = bid.BidAdjustment + bulk_bid._intent_option = self.location_target.IntentOption + yield bulk_bid + elif location_type == 'PostalCode': + if self.postal_code_target is None or self.postal_code_target.Bids is None: + continue + for bid in self.postal_code_target.Bids.PostalCodeTargetBid: + if bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.PostalCode + bulk_bid.location_type = location_type + bulk_bid.bid_adjustment = bid.BidAdjustment + bulk_bid._intent_option = self.location_target.IntentOption + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.ensure_location_target() + for bulk_bid in self.child_entities: + if bulk_bid.location_type == 'City': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('CityTargetBid') + bid.City = bulk_bid.location + bid.BidAdjustment = bulk_bid.bid_adjustment + bid.IsExcluded = False + self.city_target.Bids.CityTargetBid.append(bid) + elif bulk_bid.location_type == 'MetroArea': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('MetroAreaTargetBid') + bid.MetroArea = bulk_bid.location + bid.BidAdjustment = bulk_bid.bid_adjustment + bid.IsExcluded = False + self.metro_area_target.Bids.MetroAreaTargetBid.append(bid) + elif bulk_bid.location_type == 'State': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('StateTargetBid') + bid.State = bulk_bid.location + bid.BidAdjustment = bulk_bid.bid_adjustment + bid.IsExcluded = False + self.state_target.Bids.StateTargetBid.append(bid) + elif bulk_bid.location_type == 'Country': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('CountryTargetBid') + bid.CountryAndRegion = bulk_bid.location + bid.BidAdjustment = bulk_bid.bid_adjustment + bid.IsExcluded = False + self.country_target.Bids.CountryTargetBid.append(bid) + elif bulk_bid.location_type == 'PostalCode': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('PostalCodeTargetBid') + bid.PostalCode = bulk_bid.location + bid.BidAdjustment = bulk_bid.bid_adjustment + bid.IsExcluded = False + self.postal_code_target.Bids.PostalCodeTargetBid.append(bid) + if self.child_entities: + bid = self.child_entities[0] + if bid.intent_option is not None: + self.location_target.IntentOption = bid.intent_option + + +class BulkCampaignLocationTarget(_BulkLocationTarget, _BulkSubTargetCampaignMixin): + """ Represents a geographical location target that is associated with a campaign. + + This class exposes the :attr:`city_target`, :attr:`metro_area_target`, :attr:`state_target`, + :attr:`country_target`, and :attr:`postal_code_target`. + Each sub type can be read and written as fields of the Campaign Location Target record in a bulk file. + + For more information, see Campaign Location Target at http://go.microsoft.com/fwlink/?LinkID=620243. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkLocationTarget.__init__( + self, + city_target=city_target, + metro_area_target=metro_area_target, + state_target=state_target, + country_target=country_target, + postal_code_target=postal_code_target, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignLocationTargetBid() + + +class BulkAdGroupLocationTarget(_BulkLocationTarget, _BulkSubTargetAdGroupMixin): + """ Represents a geographical location target that is associated with an ad group. + + This class exposes the :attr:`city_target`, :attr:`metro_area_target`, :attr:`state_target`, + :attr:`country_target`, and :attr:`postal_code_target`. + Each sub type can be read and written as fields of the Ad Group Location Target record in a bulk file. + + For more information, see Ad Group Location Target at http://go.microsoft.com/fwlink/?LinkID=620255. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkLocationTarget.__init__( + self, + city_target=city_target, + metro_area_target=metro_area_target, + state_target=state_target, + country_target=country_target, + postal_code_target=postal_code_target, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupLocationTargetBid() + + +class _BulkNegativeLocationTargetBid(_BulkTargetBid): + """ This base class provides properties that are shared by all bulk negative location target bid classes. """ + + def __init__(self, + location=None, + location_type=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + self._location = location + self._location_type = location_type + super(_BulkNegativeLocationTargetBid, self).__init__( + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name + ) + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Target, + field_to_csv=lambda c: bulk_str(c.location), + csv_to_field=lambda c, v: setattr(c, '_location', v), + ), + _SimpleBulkMapping( + header=_StringTable.SubType, + field_to_csv=lambda c: location_target_type_bulk_str(c.location_type), + csv_to_field=lambda c, v: setattr(c, '_location_type', parse_location_target_type(v)), + ), + ] + + def _prepare_process_mapping_from_row_values(self): + pass + + @property + def location(self): + return self._location + + @location.setter + def location(self, value): + self._location = value + + @property + def location_type(self): + return self._location_type + + @location_type.setter + def location_type(self, value): + self._location_type = value + + +class BulkAdGroupNegativeLocationTargetBid(_BulkNegativeLocationTargetBid, _BulkTargetBidAdGroupMixin): + """ Represents one sub location negative target bid within a negative location target that is associated with an ad group. + + This class exposes properties that can be read and written as fields of the Ad Group Negative Location Target record in a bulk file. + + For more information, see Ad Group Negative Location Target at http://go.microsoft.com/fwlink/?LinkID=620256. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + location=None, + location_type=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None): + _BulkNegativeLocationTargetBid.__init__( + self, + location=location, + location_type=location_type, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + +class BulkCampaignNegativeLocationTargetBid(_BulkNegativeLocationTargetBid, _BulkTargetBidCampaignMixin): + """ Represents one sub location negative target bid within a negative location target that is associated with a campaign. + + This class exposes properties that can be read and written as fields of the Campaign Negative Location Target record in a bulk file. + + For more information, see Campaign Negative Location Target at http://go.microsoft.com/fwlink/?LinkID=620244. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + location=None, + location_type=None, + bid_adjustment=None, + intent_option=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None): + _BulkNegativeLocationTargetBid.__init__( + self, + location=location, + location_type=location_type, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + +class _BulkNegativeLocationTarget(_BulkLocationTargetWithStringLocation): + """ A base class for all bulk negative location target classes. + + For example :class:`.BulkAdGroupNegativeLocationTarget`. + """ + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + entity_id=None, + entity_name=None, + parent_entity_name=None): + super(_BulkNegativeLocationTarget, self).__init__( + city_target=city_target, + metro_area_target=metro_area_target, + state_target=state_target, + country_target=country_target, + postal_code_target=postal_code_target, + status=status, + target_id=target_id, + entity_id=entity_id, + entity_name=entity_name, + parent_entity_name=parent_entity_name, + ) + + def _convert_target_to_bulk_entities(self): + for location_type in self.LOCATION_TYPES: + if location_type == 'City': + if self.city_target is None or self.city_target.Bids is None: + continue + for bid in self.city_target.Bids.CityTargetBid: + if not bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.City + bulk_bid.location_type = location_type + yield bulk_bid + elif location_type == 'MetroArea': + if self.metro_area_target is None or self.metro_area_target.Bids is None: + continue + for bid in self.metro_area_target.Bids.MetroAreaTargetBid: + if not bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.MetroArea + bulk_bid.location_type = location_type + yield bulk_bid + elif location_type == 'State': + if self.state_target is None or self.state_target.Bids is None: + continue + for bid in self.state_target.Bids.StateTargetBid: + if not bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.State + bulk_bid.location_type = location_type + yield bulk_bid + elif location_type == 'Country': + if self.country_target is None or self.country_target.Bids is None: + continue + for bid in self.country_target.Bids.CountryTargetBid: + if not bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.CountryAndRegion + bulk_bid.location_type = location_type + yield bulk_bid + elif location_type == 'PostalCode': + if self.postal_code_target is None or self.postal_code_target.Bids is None: + continue + for bid in self.postal_code_target.Bids.PostalCodeTargetBid: + if not bid.IsExcluded: + continue + bulk_bid = self._create_and_populate_bid() + bulk_bid.location = bid.PostalCode + bulk_bid.location_type = location_type + yield bulk_bid + + def _convert_bulk_entities_to_target(self): + self.ensure_location_target() + for bulk_bid in self.child_entities: + if bulk_bid.location_type == 'City': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('CityTargetBid') + bid.City = bulk_bid.location + bid.IsExcluded = True + self.city_target.Bids.CityTargetBid.append(bid) + elif bulk_bid.location_type == 'MetroArea': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('MetroAreaTargetBid') + bid.MetroArea = bulk_bid.location + bid.IsExcluded = True + self.metro_area_target.Bids.MetroAreaTargetBid.append(bid) + elif bulk_bid.location_type == 'State': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('StateTargetBid') + bid.State = bulk_bid.location + bid.IsExcluded = True + self.state_target.Bids.StateTargetBid.append(bid) + elif bulk_bid.location_type == 'Country': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('CountryTargetBid') + bid.CountryAndRegion = bulk_bid.location + bid.IsExcluded = True + self.country_target.Bids.CountryTargetBid.append(bid) + elif bulk_bid.location_type == 'PostalCode': + bid = _CAMPAIGN_OBJECT_FACTORY_V10.create('PostalCodeTargetBid') + bid.PostalCode = bulk_bid.location + bid.IsExcluded = True + self.postal_code_target.Bids.PostalCodeTargetBid.append(bid) + + +class BulkCampaignNegativeLocationTarget(_BulkNegativeLocationTarget, _BulkSubTargetCampaignMixin): + """ Represents a negative geographical location target that is associated with a campaign. + + This class exposes the :attr:`city_target`, :attr:`metro_area_target`, :attr:`state_target`, + :attr:`country_target`, and :attr:`postal_code_target`. + Each sub type can be read and written as fields of the Campaign Location Target record in a bulk file. + + For more information, see Campaign Negative Location Target at http://go.microsoft.com/fwlink/?LinkID=620244. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + campaign_id=None, + campaign_name=None, ): + _BulkNegativeLocationTarget.__init__( + self, + city_target=city_target, + metro_area_target=metro_area_target, + state_target=state_target, + country_target=country_target, + postal_code_target=postal_code_target, + status=status, + target_id=target_id, + entity_id=campaign_id, + entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkCampaignNegativeLocationTargetBid() + + +class BulkAdGroupNegativeLocationTarget(_BulkNegativeLocationTarget, _BulkSubTargetAdGroupMixin): + """ Represents a negative geographical location target that is associated with an ad group. + + This class exposes the :attr:`city_target`, :attr:`metro_area_target`, :attr:`state_target`, + :attr:`country_target`, and :attr:`postal_code_target`. + Each sub type can be read and written as fields of the Ad Group Location Target record in a bulk file. + + For more information, see Ad Group Negative Location Target at http://go.microsoft.com/fwlink/?LinkID=620256. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + city_target=None, + metro_area_target=None, + state_target=None, + country_target=None, + postal_code_target=None, + status=None, + target_id=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, ): + _BulkNegativeLocationTarget.__init__( + self, + city_target=city_target, + metro_area_target=metro_area_target, + state_target=state_target, + country_target=country_target, + postal_code_target=postal_code_target, + status=status, + target_id=target_id, + entity_id=ad_group_id, + entity_name=ad_group_name, + parent_entity_name=campaign_name, + ) + + def _create_bid(self): + return BulkAdGroupNegativeLocationTargetBid() diff --git a/bingads/v10/bulk/entities/unknown_bulk_entity.py b/bingads/v10/bulk/entities/unknown_bulk_entity.py index 17e65a56..ddc129b4 100644 --- a/bingads/v10/bulk/entities/unknown_bulk_entity.py +++ b/bingads/v10/bulk/entities/unknown_bulk_entity.py @@ -21,7 +21,7 @@ def process_mappings_from_row_values(self, row_values): self._values = row_values.to_dict() def process_mappings_to_row_values(self, row_values): - for (key, value) in self._values.items(): + for (key, value) in list(self._values.items()): row_values[key] = value def read_additional_data(self, stream_reader): diff --git a/bingads/v10/bulk/entities/unknown_bulk_entity.py.bak b/bingads/v10/bulk/entities/unknown_bulk_entity.py.bak new file mode 100644 index 00000000..17e65a56 --- /dev/null +++ b/bingads/v10/bulk/entities/unknown_bulk_entity.py.bak @@ -0,0 +1,28 @@ +from bingads.v10.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity + + +class UnknownBulkEntity(_SingleRecordBulkEntity): + """ Reserved to support new record types that may be added to the Bulk schema. """ + + def __init__(self): + super(UnknownBulkEntity, self).__init__() + self._values = None + + @property + def values(self): + """ The forward compatibility map of fields and values. + + :rtype: dict | None + """ + + return self._values + + def process_mappings_from_row_values(self, row_values): + self._values = row_values.to_dict() + + def process_mappings_to_row_values(self, row_values): + for (key, value) in self._values.items(): + row_values[key] = value + + def read_additional_data(self, stream_reader): + super(UnknownBulkEntity, self).read_additional_data(stream_reader) diff --git a/bingads/v10/bulk/file_reader.py b/bingads/v10/bulk/file_reader.py index f1a74672..9fee97de 100644 --- a/bingads/v10/bulk/file_reader.py +++ b/bingads/v10/bulk/file_reader.py @@ -50,12 +50,12 @@ def __iter__(self): return self def __next__(self): - return self.next() + return next(self) def close(self): self.__exit__(None, None, None) - def next(self): + def __next__(self): return self.read_next_entity() def read_next_entity(self): diff --git a/bingads/v10/bulk/file_reader.py.bak b/bingads/v10/bulk/file_reader.py.bak new file mode 100644 index 00000000..f1a74672 --- /dev/null +++ b/bingads/v10/bulk/file_reader.py.bak @@ -0,0 +1,159 @@ +from .enums import DownloadFileType, ResultFileType +from .entities.bulk_entity import BulkEntity +#from ..internal.bulk.stream_reader import _BulkStreamReader +#from ..internal.bulk.entities.multi_record_bulk_entity import _MultiRecordBulkEntity +from bingads.v10.internal.bulk.stream_reader import _BulkStreamReader +from bingads.v10.internal.bulk.entities.multi_record_bulk_entity import _MultiRecordBulkEntity + + +class BulkFileReader: + """ Provides a method to read bulk entities from a bulk file and make them accessible as an enumerable list. + + For more information about the Bulk File Schema, see http://go.microsoft.com/fwlink/?LinkID=620269. + """ + + def __init__(self, + file_path, + file_format=DownloadFileType.csv, + result_file_type=ResultFileType.full_download, + encoding=None): + """ Initializes a new instance of this class with the specified file details. + + :param file_path: The path of the bulk file to read. + :type file_path: str + :param file_format: The bulk file format. + :type file_format: DownloadFileType + :param result_file_type: The result file type. + :type result_file_type: ResultFileType + :param encoding: The encoding of bulk file. + :type encoding: str + """ + + self._file_path = file_path + self._file_format = file_format + self._result_file_type = result_file_type + self._encoding = encoding + + self._is_for_full_download = result_file_type is ResultFileType.full_download + self._entities_iterator = None + self._bulk_stream_reader = _BulkStreamReader(file_path=self.file_path, file_format=self.file_format) + self._bulk_stream_reader.__enter__() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._entities_iterator = None + self._bulk_stream_reader.__exit__(exc_type, exc_value, traceback) + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def close(self): + self.__exit__(None, None, None) + + def next(self): + return self.read_next_entity() + + def read_next_entity(self): + """ Reads next entity from the bulk file. + + :return: next entity + :rtype: BulkEntity + """ + + if self._entities_iterator is None: + self._entities_iterator = self.read_entities() + + return next(self._entities_iterator) + + def read_entities(self): + """ Gets an enumerable list of bulk entities that were read from the file. + + :return: an generator over the entities + :rtype: collections.Iterator[BulkEntity] + """ + + next_batch = self._read_next_batch() + + while next_batch is not None: + for entity in next_batch: + yield entity + next_batch = self._read_next_batch() + + def _read_next_batch(self): + """ Reads next batch of entities from the file. + + Batch means a set of related entities. + It can be one :class:`._SingleRecordBulkEntity`, one :class:`._MultiRecordBulkEntity` containing its child + entities or a set of related child entities (for example several :class:`.BulkSiteLink`s logically belonging + to the same SiteLink Ad Extension. + + :return: Next batch of entities + :rtype: _SingleRecordBulkEntity or _MultiRecordBulkEntity + """ + + next_object = self._bulk_stream_reader.read() + + if next_object is None: + return None + if next_object.can_enclose_in_multiline_entity: + multi_record_entity = next_object.enclose_in_multiline_entity() + multi_record_entity.read_related_data_from_stream(self._bulk_stream_reader) + if self._is_for_full_download: + return [multi_record_entity] + return self._extract_child_entities_if_needed(multi_record_entity) + if isinstance(next_object, BulkEntity): + return [next_object] + raise NotImplementedError() + + def _extract_child_entities_if_needed(self, entity): + # If the entity is a MultiLine entity and it has all child objects (delete all row was present), just return it + if not isinstance(entity, _MultiRecordBulkEntity) or entity.all_children_are_present: + yield entity + else: + # If not all child objects are present (there was no delete all row and we only have part of the MultiLine entity), return child object individually + for child_generator in ( + self._extract_child_entities_if_needed(child_entity) + for child_entity in entity.child_entities): + for child in child_generator: + yield child + + @property + def file_path(self): + """ The path of the bulk file to read. + + :rtype: str + """ + + return self._file_path + + @property + def file_format(self): + """ The bulk file format. + + :rtype: DownloadFileType + """ + + return self._file_format + + @property + def result_file_type(self): + """ The result file type. + + :rtype: ResultFileType + """ + + return self._result_file_type + + @property + def encoding(self): + """ The encoding of bulk file. + + :rtype: str + """ + + return self._encoding diff --git a/bingads/v10/internal/bulk/bulk_object_factory.py b/bingads/v10/internal/bulk/bulk_object_factory.py index 42b2bb43..884e63f4 100644 --- a/bingads/v10/internal/bulk/bulk_object_factory.py +++ b/bingads/v10/internal/bulk/bulk_object_factory.py @@ -186,7 +186,7 @@ def get_bulk_row_type(bulk_object): return _BulkObjectFactory.TYPE_REVERSE_MAP[type(bulk_object)] -for (k, v) in _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP.items(): +for (k, v) in list(_BulkObjectFactory.INDIVIDUAL_ENTITY_MAP.items()): _BulkObjectFactory.TYPE_REVERSE_MAP[type(v.create_func())] = k if v.create_identifier_func is not None: diff --git a/bingads/v10/internal/bulk/bulk_object_factory.py.bak b/bingads/v10/internal/bulk/bulk_object_factory.py.bak new file mode 100644 index 00000000..42b2bb43 --- /dev/null +++ b/bingads/v10/internal/bulk/bulk_object_factory.py.bak @@ -0,0 +1,199 @@ +from bingads.v10.bulk.entities import * +from bingads.v10.bulk.entities.ad_extensions.bulk_site_links_ad_extensions import _SiteLinkAdExtensionIdentifier +from bingads.v10.bulk.entities.targets.bulk_targets import _BulkCampaignTargetIdentifier, \ + _BulkAdGroupTargetIdentifier, _BulkTargetIdentifier +from bingads.v10.internal.bulk.string_table import _StringTable +from bingads.v10.internal.bulk.entity_info import _EntityInfo +from bingads.v10.bulk.entities.bulk_negative_sites import _BulkAdGroupNegativeSitesIdentifier, \ + _BulkCampaignNegativeSitesIdentifier +from bingads.v10.internal.bulk.format_version import _FormatVersion + + +class _BulkObjectFactory(): + INDIVIDUAL_ENTITY_MAP = { + _StringTable.Account: _EntityInfo(lambda: BulkAccount()), + _StringTable.Budget: _EntityInfo(lambda: BulkBudget()), + _StringTable.Campaign: _EntityInfo(lambda: BulkCampaign()), + _StringTable.AdGroup: _EntityInfo(lambda: BulkAdGroup()), + _StringTable.Keyword: _EntityInfo(lambda: BulkKeyword()), + _StringTable.SiteLinksAdExtension: _EntityInfo( + lambda: BulkSiteLink(), + _StringTable.SiteLinkExtensionOrder, + lambda: _SiteLinkAdExtensionIdentifier() + ), + _StringTable.CampaignSiteLinksAdExtension: _EntityInfo( + lambda: BulkCampaignSiteLinkAdExtension() + ), + _StringTable.AdGroupSiteLinksAdExtension: _EntityInfo( + lambda: BulkAdGroupSiteLinkAdExtension() + ), + _StringTable.CallAdExtension: _EntityInfo(lambda: BulkCallAdExtension()), + _StringTable.CampaignCallAdExtension: _EntityInfo(lambda: BulkCampaignCallAdExtension()), + _StringTable.ImageAdExtension: _EntityInfo(lambda: BulkImageAdExtension()), + _StringTable.CampaignImageAdExtension: _EntityInfo(lambda: BulkCampaignImageAdExtension()), + _StringTable.AdGroupImageAdExtension: _EntityInfo(lambda: BulkAdGroupImageAdExtension()), + _StringTable.CalloutAdExtension: _EntityInfo(lambda: BulkCalloutAdExtension()), + _StringTable.CampaignCalloutAdExtension: _EntityInfo(lambda: BulkCampaignCalloutAdExtension()), + _StringTable.AdGroupCalloutAdExtension: _EntityInfo(lambda: BulkAdGroupCalloutAdExtension()), + _StringTable.ReviewAdExtension: _EntityInfo(lambda: BulkReviewAdExtension()), + _StringTable.CampaignReviewAdExtension: _EntityInfo(lambda: BulkCampaignReviewAdExtension()), + _StringTable.AdGroupReviewAdExtension: _EntityInfo(lambda: BulkAdGroupReviewAdExtension()), + _StringTable.LocationAdExtension: _EntityInfo(lambda: BulkLocationAdExtension()), + _StringTable.CampaignLocationAdExtension: _EntityInfo(lambda: BulkCampaignLocationAdExtension()), + _StringTable.AppAdExtension: _EntityInfo(lambda: BulkAppAdExtension()), + _StringTable.CampaignAppAdExtension: _EntityInfo(lambda: BulkCampaignAppAdExtension()), + _StringTable.AdGroupAppAdExtension: _EntityInfo(lambda: BulkAdGroupAppAdExtension()), + _StringTable.StructuredSnippetAdExtension: _EntityInfo(lambda: BulkStructuredSnippetAdExtension()), + _StringTable.CampaignStructuredSnippetAdExtension: _EntityInfo(lambda: BulkCampaignStructuredSnippetAdExtension()), + _StringTable.AdGroupStructuredSnippetAdExtension: _EntityInfo(lambda: BulkAdGroupStructuredSnippetAdExtension()), + _StringTable.Sitelink2AdExtension: _EntityInfo(lambda: BulkSitelink2AdExtension()), + _StringTable.CampaignSitelink2AdExtension: _EntityInfo(lambda: BulkCampaignSitelink2AdExtension()), + _StringTable.AdGroupSitelink2AdExtension: _EntityInfo(lambda: BulkAdGroupSitelink2AdExtension()), + _StringTable.ProductAd: _EntityInfo(lambda: BulkProductAd()), + _StringTable.TextAd: _EntityInfo(lambda: BulkTextAd()), + _StringTable.AppInstallAd: _EntityInfo(lambda: BulkAppInstallAd()), + _StringTable.ExpandedTextAd: _EntityInfo(lambda: BulkExpandedTextAd()), + _StringTable.DynamicSearchAd: _EntityInfo(lambda: BulkDynamicSearchAd()), + "Campaign Negative Site": _EntityInfo( + lambda: BulkCampaignNegativeSite(), + _StringTable.Website, + lambda: _BulkCampaignNegativeSitesIdentifier() + ), + "Ad Group Negative Site": _EntityInfo( + lambda: BulkAdGroupNegativeSite(), + _StringTable.Website, + lambda: _BulkAdGroupNegativeSitesIdentifier() + ), + + _StringTable.NegativeKeywordList: _EntityInfo(lambda: BulkNegativeKeywordList()), + _StringTable.ListNegativeKeyword: _EntityInfo(lambda: BulkSharedNegativeKeyword()), + _StringTable.CampaignNegativeKeywordList: _EntityInfo(lambda: BulkCampaignNegativeKeywordList()), + _StringTable.CampaignNegativeKeyword: _EntityInfo(lambda: BulkCampaignNegativeKeyword()), + _StringTable.AdGroupNegativeKeyword: _EntityInfo(lambda: BulkAdGroupNegativeKeyword()), + 'Ad Group Age Target': _EntityInfo( + lambda: BulkAdGroupAgeTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupAgeTargetBid) + ), + 'Campaign Age Target': _EntityInfo( + lambda: BulkCampaignAgeTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignAgeTargetBid) + ), + 'Ad Group DayTime Target': _EntityInfo( + lambda: BulkAdGroupDayTimeTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupDayTimeTargetBid) + ), + 'Campaign DayTime Target': _EntityInfo( + lambda: BulkCampaignDayTimeTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignDayTimeTargetBid) + ), + 'Ad Group DeviceOS Target': _EntityInfo( + lambda: BulkAdGroupDeviceOsTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupDeviceOsTargetBid) + ), + 'Campaign DeviceOS Target': _EntityInfo( + lambda: BulkCampaignDeviceOsTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignDeviceOsTargetBid) + ), + 'Ad Group Gender Target': _EntityInfo( + lambda: BulkAdGroupGenderTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupGenderTargetBid) + ), + 'Campaign Gender Target': _EntityInfo( + lambda: BulkCampaignGenderTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignGenderTargetBid) + ), + 'Ad Group Location Target': _EntityInfo( + lambda: BulkAdGroupLocationTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupLocationTargetBid) + ), + 'Campaign Location Target': _EntityInfo( + lambda: BulkCampaignLocationTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignLocationTargetBid) + ), + 'Ad Group Negative Location Target': _EntityInfo( + lambda: BulkAdGroupNegativeLocationTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupNegativeLocationTargetBid) + ), + 'Campaign Negative Location Target': _EntityInfo( + lambda: BulkCampaignNegativeLocationTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignNegativeLocationTargetBid) + ), + 'Ad Group Radius Target': _EntityInfo( + lambda: BulkAdGroupRadiusTargetBid(), + _StringTable.Target, + lambda: _BulkAdGroupTargetIdentifier(target_bid_type=BulkAdGroupRadiusTargetBid) + ), + 'Campaign Radius Target': _EntityInfo( + lambda: BulkCampaignRadiusTargetBid(), + _StringTable.Target, + lambda: _BulkCampaignTargetIdentifier(target_bid_type=BulkCampaignRadiusTargetBid) + ), + 'Campaign Product Scope': _EntityInfo(lambda : BulkCampaignProductScope()), + 'Ad Group Product Partition': _EntityInfo(lambda : BulkAdGroupProductPartition()), + 'Remarketing List': _EntityInfo(lambda : BulkRemarketingList()), + 'Ad Group Remarketing List Association': _EntityInfo(lambda : BulkAdGroupRemarketingListAssociation()), + 'Campaign Negative Dynamic Search Ad Target': _EntityInfo(lambda: BulkCampaignNegativeDynamicSearchAdTarget()), + 'Ad Group Dynamic Search Ad Target': _EntityInfo(lambda: BulkAdGroupDynamicSearchAdTarget()), + 'Ad Group Negative Dynamic Search Ad Target': _EntityInfo(lambda: BulkAdGroupNegativeDynamicSearchAdTarget()), + } + + ADDITIONAL_OBJECT_MAP = { + 'Format Version': lambda: _FormatVersion(), + 'Keyword Best Position Bid': lambda: BulkKeywordBestPositionBid(), + 'Keyword Main Line Bid': lambda: BulkKeywordMainLineBid(), + 'Keyword First Page Bid': lambda: BulkKeywordFirstPageBid(), + } + + TYPE_REVERSE_MAP = {} + TARGET_IDENTIFIER_TYPE_REVERSE_MAP = {} + + @staticmethod + def create_bulk_object(row_values): + type_column = row_values[_StringTable.Type] + + if type_column.endswith('Error'): + return BulkError() + elif type_column in _BulkObjectFactory.ADDITIONAL_OBJECT_MAP: + return _BulkObjectFactory.ADDITIONAL_OBJECT_MAP[type_column]() + elif type_column in _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP: + info = _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP[type_column] + if row_values[_StringTable.Status] == 'Deleted' \ + and info.delete_all_column_name \ + and not row_values[info.delete_all_column_name]: + return info.create_identifier_func() + return info.create_func() + else: + return UnknownBulkEntity() + + @staticmethod + def get_bulk_row_type(bulk_object): + if isinstance(bulk_object, BulkError): + return '{0} Error'.format(_BulkObjectFactory.get_bulk_row_type(bulk_object.entity)) + if isinstance(bulk_object, _BulkTargetIdentifier): + return _BulkObjectFactory.TARGET_IDENTIFIER_TYPE_REVERSE_MAP[type(bulk_object)][bulk_object.target_bid_type] + return _BulkObjectFactory.TYPE_REVERSE_MAP[type(bulk_object)] + + +for (k, v) in _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP.items(): + _BulkObjectFactory.TYPE_REVERSE_MAP[type(v.create_func())] = k + + if v.create_identifier_func is not None: + identifier = v.create_identifier_func() + if isinstance(identifier, _BulkTargetIdentifier): + if not type(identifier) in _BulkObjectFactory.TARGET_IDENTIFIER_TYPE_REVERSE_MAP: + _BulkObjectFactory.TARGET_IDENTIFIER_TYPE_REVERSE_MAP[type(identifier)] = {} + _BulkObjectFactory.TARGET_IDENTIFIER_TYPE_REVERSE_MAP[type(identifier)][identifier.target_bid_type] = k + else: + _BulkObjectFactory.TYPE_REVERSE_MAP[type(identifier)] = k diff --git a/bingads/v10/internal/bulk/csv_headers.py b/bingads/v10/internal/bulk/csv_headers.py index 977c7935..8c0d2090 100644 --- a/bingads/v10/internal/bulk/csv_headers.py +++ b/bingads/v10/internal/bulk/csv_headers.py @@ -223,7 +223,7 @@ def get_mappings(): @staticmethod def initialize_map(): - return dict(zip(_CsvHeaders.HEADERS, range(len(_CsvHeaders.HEADERS)))) + return dict(list(zip(_CsvHeaders.HEADERS, list(range(len(_CsvHeaders.HEADERS)))))) _CsvHeaders.COLUMN_INDEX_MAP = _CsvHeaders.initialize_map() diff --git a/bingads/v10/internal/bulk/csv_headers.py.bak b/bingads/v10/internal/bulk/csv_headers.py.bak new file mode 100644 index 00000000..977c7935 --- /dev/null +++ b/bingads/v10/internal/bulk/csv_headers.py.bak @@ -0,0 +1,229 @@ +from .string_table import _StringTable + + +class _CsvHeaders: + HEADERS = [ + # Common + _StringTable.Type, + _StringTable.Status, + _StringTable.Id, + _StringTable.ParentId, + _StringTable.SubType, + _StringTable.Campaign, + _StringTable.AdGroup, + _StringTable.Website, + _StringTable.SyncTime, + _StringTable.ClientId, + _StringTable.LastModifiedTime, + + # Campaign + _StringTable.TimeZone, + _StringTable.Budget, + _StringTable.BudgetType, + _StringTable.BudgetName, + _StringTable.BudgetId, + _StringTable.KeywordVariantMatchEnabled, + + # AdGroup + _StringTable.StartDate, + _StringTable.EndDate, + _StringTable.NetworkDistribution, + _StringTable.PricingModel, + _StringTable.AdRotation, + _StringTable.SearchNetwork, + _StringTable.SearchBid, + _StringTable.ContentNetwork, + _StringTable.ContentBid, + _StringTable.Language, + + # Ads + _StringTable.Title, + _StringTable.Text, + _StringTable.DisplayUrl, + _StringTable.DestinationUrl, + _StringTable.BusinessName, + _StringTable.PhoneNumber, + _StringTable.PromotionalText, + _StringTable.EditorialStatus, + _StringTable.EditorialLocation, + _StringTable.EditorialTerm, + _StringTable.EditorialReasonCode, + _StringTable.EditorialAppealStatus, + _StringTable.DevicePreference, + + # Keywords + _StringTable.Keyword, + _StringTable.MatchType, + _StringTable.Bid, + _StringTable.Param1, + _StringTable.Param2, + _StringTable.Param3, + + # Location Target + _StringTable.Target, + _StringTable.PhysicalIntent, + _StringTable.TargetAll, + _StringTable.BidAdjustment, + _StringTable.RadiusTargetId, + _StringTable.Name, + _StringTable.OsNames, + _StringTable.Radius, + _StringTable.Unit, + _StringTable.BusinessId, + + # DayTime Target + _StringTable.FromHour, + _StringTable.FromMinute, + _StringTable.ToHour, + _StringTable.ToMinute, + + # AdExtensions common + _StringTable.Version, + + # SiteLink Ad Extensions + _StringTable.SiteLinkExtensionOrder, + _StringTable.SiteLinkDisplayText, + _StringTable.SiteLinkDestinationUrl, + _StringTable.SiteLinkDescription1, + _StringTable.SiteLinkDescription2, + + # Location Ad Extensions + _StringTable.GeoCodeStatus, + _StringTable.IconMediaId, + _StringTable.ImageMediaId, + _StringTable.AddressLine1, + _StringTable.AddressLine2, + _StringTable.PostalCode, + _StringTable.City, + _StringTable.StateOrProvince, + _StringTable.ProvinceName, + _StringTable.Latitude, + _StringTable.Longitude, + + # Call Ad Extensions + _StringTable.CountryCode, + _StringTable.IsCallOnly, + _StringTable.IsCallTrackingEnabled, + _StringTable.RequireTollFreeTrackingNumber, + + # Structured Snippet Ad Extensions + _StringTable.StructuredSnippetHeader, + _StringTable.StructuredSnippetValues, + + # Image Ad Extensions + _StringTable.AltText, + _StringTable.MediaIds, + _StringTable.PublisherCountries, + + # Callout Ad Extension + _StringTable.CalloutText, + + # Product Target + _StringTable.BingMerchantCenterId, + _StringTable.BingMerchantCenterName, + _StringTable.ProductCondition1, + _StringTable.ProductValue1, + _StringTable.ProductCondition2, + _StringTable.ProductValue2, + _StringTable.ProductCondition3, + _StringTable.ProductValue3, + _StringTable.ProductCondition4, + _StringTable.ProductValue4, + _StringTable.ProductCondition5, + _StringTable.ProductValue5, + _StringTable.ProductCondition6, + _StringTable.ProductValue6, + _StringTable.ProductCondition7, + _StringTable.ProductValue7, + _StringTable.ProductCondition8, + _StringTable.ProductValue8, + + # BI + _StringTable.Spend, + _StringTable.Impressions, + _StringTable.Clicks, + _StringTable.CTR, + _StringTable.AvgCPC, + _StringTable.AvgCPM, + _StringTable.AvgPosition, + _StringTable.Conversions, + _StringTable.CPA, + + _StringTable.QualityScore, + _StringTable.KeywordRelevance, + _StringTable.LandingPageRelevance, + _StringTable.LandingPageUserExperience, + + _StringTable.AppPlatform, + _StringTable.AppStoreId, + _StringTable.IsTrackingEnabled, + + _StringTable.Error, + _StringTable.ErrorNumber, + + # Bing Shopping Campaigns + _StringTable.IsExcluded, + _StringTable.ParentAdGroupCriterionId, + _StringTable.CampaignType, + _StringTable.CampaignPriority, + + # V10 added + _StringTable.FieldPath, + + # Upgrade Url + _StringTable.FinalUrl, + _StringTable.FinalMobileUrl, + _StringTable.TrackingTemplate, + _StringTable.CustomParameter, + + # Review Ad Extension + _StringTable.IsExact, + _StringTable.Source, + _StringTable.Url, + + # Bid Strategy + _StringTable.BidStrategyType, + + # Ad Format Preference + _StringTable.AdFormatPreference, + + # Remarketing + _StringTable.RemarketingList, + _StringTable.Description, + _StringTable.MembershipDuration, + _StringTable.Scope, + _StringTable.TagId, + _StringTable.RemarketingListId, + _StringTable.RemarketingTargetingSetting, + _StringTable.RemarketingRule, + + # Expanded Text Ad + _StringTable.TitlePart1, + _StringTable.TitlePart2, + _StringTable.Path1, + _StringTable.Path2, + + # Ad Scheduling + _StringTable.AdSchedule, + _StringTable.UseSearcherTimeZone, + + # Dynamic Search Ads + _StringTable.DomainLanguage, + _StringTable.DynamicAdTargetCondition1, + _StringTable.DynamicAdTargetValue1, + _StringTable.DynamicAdTargetCondition2, + _StringTable.DynamicAdTargetValue2, + _StringTable.DynamicAdTargetCondition3, + _StringTable.DynamicAdTargetValue3, + ] + + @staticmethod + def get_mappings(): + return _CsvHeaders.COLUMN_INDEX_MAP + + @staticmethod + def initialize_map(): + return dict(zip(_CsvHeaders.HEADERS, range(len(_CsvHeaders.HEADERS)))) + + +_CsvHeaders.COLUMN_INDEX_MAP = _CsvHeaders.initialize_map() diff --git a/bingads/v10/internal/bulk/csv_reader.py b/bingads/v10/internal/bulk/csv_reader.py index 476b2412..4e15b886 100644 --- a/bingads/v10/internal/bulk/csv_reader.py +++ b/bingads/v10/internal/bulk/csv_reader.py @@ -48,17 +48,17 @@ def __iter__(self): return self def __next__(self): - return self.next() + return next(self) def close(self): self.__exit__(None, None, None) - def next(self): + def __next__(self): if PY3: return next(self._csv_reader) elif PY2: row = next(self._csv_reader) - return [unicode(cell, encoding='utf-8') for cell in row] + return [str(cell, encoding='utf-8') for cell in row] @property def filename(self): diff --git a/bingads/v10/internal/bulk/csv_reader.py.bak b/bingads/v10/internal/bulk/csv_reader.py.bak new file mode 100644 index 00000000..476b2412 --- /dev/null +++ b/bingads/v10/internal/bulk/csv_reader.py.bak @@ -0,0 +1,73 @@ +import csv +import io +from six import PY2, PY3 + +import chardet + + +class _CsvReader: + + def __init__(self, filename, delimiter, encoding=None): + self._filename = filename + self._delimiter = delimiter + self._encoding = encoding + + if delimiter == ',': + self._dialect = csv.excel + elif delimiter == '\t': + self._dialect = csv.excel_tab + else: + raise ValueError('Do not support delimiter: {0}', delimiter) + + if self._encoding is None: + self._encoding = self._detected_encoding + + self._csv_file = io.open(self.filename, encoding=self.encoding) + + if PY3: + self._csv_reader = csv.reader(self._csv_file, dialect=self._dialect) + elif PY2: + byte_lines = [line.encode('utf-8') for line in self._csv_file] + self._csv_reader = csv.reader(byte_lines, dialect=self._dialect) + + @property + def _detected_encoding(self): + buffer_size = 1024 * 1024 + with open(self._filename, mode='rb') as bfile: + content = bfile.read(buffer_size) + result = chardet.detect(content) + return result['encoding'] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._csv_file.__exit__(exc_type, exc_value, traceback) + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def close(self): + self.__exit__(None, None, None) + + def next(self): + if PY3: + return next(self._csv_reader) + elif PY2: + row = next(self._csv_reader) + return [unicode(cell, encoding='utf-8') for cell in row] + + @property + def filename(self): + return self._filename + + @property + def delimiter(self): + return self._delimiter + + @property + def encoding(self): + return self._encoding diff --git a/bingads/v10/internal/bulk/csv_writer.py b/bingads/v10/internal/bulk/csv_writer.py index 5d8c4ff4..157de753 100644 --- a/bingads/v10/internal/bulk/csv_writer.py +++ b/bingads/v10/internal/bulk/csv_writer.py @@ -38,7 +38,7 @@ def writerow(self, row): self._csv_writer.writerow(row) elif PY2: def unicode_to_str(value): - if not isinstance(value, unicode): + if not isinstance(value, str): return value return value.encode('utf-8') self._csv_writer.writerow([unicode_to_str(cell) for cell in row]) diff --git a/bingads/v10/internal/bulk/csv_writer.py.bak b/bingads/v10/internal/bulk/csv_writer.py.bak new file mode 100644 index 00000000..5d8c4ff4 --- /dev/null +++ b/bingads/v10/internal/bulk/csv_writer.py.bak @@ -0,0 +1,56 @@ +import csv +import codecs +from six import PY2, PY3 + + +class _CsvWriter: + def __init__(self, filename, delimiter): + self._filename = filename + self._delimiter = delimiter + self._encoding = 'utf-8-sig' + + if delimiter == ',': + self._dialect = csv.excel + elif delimiter == '\t': + self._dialect = csv.excel_tab + else: + raise ValueError('Do not support delimiter: {0}', delimiter) + + if PY3: + self._csv_file = codecs.open(filename, mode='w', encoding=self._encoding) + elif PY2: + self._csv_file = open(filename, mode='wb') + self._csv_file.write(codecs.BOM_UTF8) + self._csv_writer = csv.writer(self._csv_file, dialect=self._dialect) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self._csv_file.flush() + self._csv_file.close() + + def close(self): + self.__exit__(None, None, None) + + def writerow(self, row): + if PY3: + self._csv_writer.writerow(row) + elif PY2: + def unicode_to_str(value): + if not isinstance(value, unicode): + return value + return value.encode('utf-8') + self._csv_writer.writerow([unicode_to_str(cell) for cell in row]) + + def writerows(self, rows): + for row in rows: + self.writerow(row) + + @property + def filename(self): + return self._filename + + @property + def delimiter(self): + return self._delimiter diff --git a/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py b/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py index b0c5d847..e8a77c5c 100644 --- a/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py +++ b/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, division, print_function + from abc import ABCMeta, abstractproperty, abstractmethod from future.utils import with_metaclass diff --git a/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py.bak b/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py.bak new file mode 100644 index 00000000..b0c5d847 --- /dev/null +++ b/bingads/v10/internal/bulk/entities/bulk_entity_identifier.py.bak @@ -0,0 +1,35 @@ +from __future__ import absolute_import, division, print_function +from abc import ABCMeta, abstractproperty, abstractmethod + +from future.utils import with_metaclass + +from bingads.v10.bulk.entities import BulkError +from bingads.v10.internal.bulk.bulk_object import _BulkObject + + +class _BulkEntityIdentifier(with_metaclass(ABCMeta, _BulkObject)): + + @abstractproperty + def is_delete_row(self): + raise NotImplementedError() + + @abstractmethod + def _create_entity_with_this_identifier(self): + raise NotImplementedError() + + def write_to_stream(self, row_writer, exclude_readonly_data): + row_writer.write_object_row(self) + + def read_related_data_from_stream(self, stream_reader): + if self.is_delete_row: + has_more_errors = True + + while has_more_errors: + has_more_errors, error = stream_reader.try_read(BulkError) + + @property + def can_enclose_in_multiline_entity(self): + return True + + def enclose_in_multiline_entity(self): + return self._create_entity_with_this_identifier() diff --git a/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py b/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py index 3b6b8752..13536124 100644 --- a/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py +++ b/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py @@ -42,7 +42,7 @@ def has_errors(self): :rtype: bool """ - return any(map(lambda x: x.has_errors, self.child_entities)) + return any([x.has_errors for x in self.child_entities]) @property def last_modified_time(self): diff --git a/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py.bak b/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py.bak new file mode 100644 index 00000000..3b6b8752 --- /dev/null +++ b/bingads/v10/internal/bulk/entities/multi_record_bulk_entity.py.bak @@ -0,0 +1,67 @@ +from abc import ABCMeta, abstractproperty +from future.utils import with_metaclass +from bingads.v10.bulk.entities.bulk_entity import BulkEntity + + +class _MultiRecordBulkEntity(with_metaclass(ABCMeta, BulkEntity)): + """ Bulk entity that has its data in multiple records within the bulk file. + + For example, :class:`.BulkSiteLinkAdExtension` is a multi record bulk entity which can contain one or more + :class:`.BulkSiteLink` child entities, which are themselves derived from :class:`.SingleRecordBulkEntity`. + + For more information, see Bulk File Schema at http://go.microsoft.com/fwlink/?LinkID=620269. + """ + + def __init__(self): + super(_MultiRecordBulkEntity, self).__init__() + + @abstractproperty + def child_entities(self): + """ The child entities that this multi record entity contains. + + :rtype: list[BulkEntity] + """ + + raise NotImplementedError() + + @abstractproperty + def all_children_are_present(self): + """ True, if the object is fully constructed (contains all of its children), determined by the presence of delete all row, False otherwise + + :rtype: bool + """ + + raise NotImplementedError() + + @property + def has_errors(self): + """ Indicates whether or not the errors property of any of the ChildEntities is null or empty. + + If true, one or more ChildEntities contains the details of one or more :class:`.BulkError` objects. + + :rtype: bool + """ + + return any(map(lambda x: x.has_errors, self.child_entities)) + + @property + def last_modified_time(self): + """ Gets the last modified time for the first child entity, or null if there are no ChildEntities. + + :rtype: datetime.datetime + """ + + return self.child_entities[0].last_modified_time if self.child_entities else None + + @property + def can_enclose_in_multiline_entity(self): + return super(_MultiRecordBulkEntity, self).can_enclose_in_multiline_entity + + def enclose_in_multiline_entity(self): + return super(_MultiRecordBulkEntity, self).enclose_in_multiline_entity() + + def read_from_row_values(self, row_values): + super(_MultiRecordBulkEntity, self).read_from_row_values(row_values) + + def write_to_row_values(self, row_values): + super(_MultiRecordBulkEntity, self).write_to_row_values(row_values) diff --git a/bingads/v10/internal/bulk/object_reader.py b/bingads/v10/internal/bulk/object_reader.py index 90d5f856..5707902e 100644 --- a/bingads/v10/internal/bulk/object_reader.py +++ b/bingads/v10/internal/bulk/object_reader.py @@ -13,7 +13,7 @@ def __init__(self, file_path, delimiter, encoding=None): self._csv_reader = _CsvReader(self.file_path, delimiter=self.delimiter) self._csv_reader.__enter__() headers = self._read_headers() - self._column_mapping = dict(zip(headers, range(0, len(headers)))) + self._column_mapping = dict(list(zip(headers, list(range(0, len(headers)))))) def __enter__(self): return self @@ -27,7 +27,7 @@ def __iter__(self): def __next__(self): return self.read_next_bulk_object() - def next(self): + def __next__(self): return self.__next__() def read_next_bulk_object(self): @@ -54,7 +54,7 @@ def _read_next_row_values(self): def _read_headers(self): # Need to strip BOM marker by hand, take care def remove_bom(unicode_str): - unicode_bom = u'\N{ZERO WIDTH NO-BREAK SPACE}' + unicode_bom = '\N{ZERO WIDTH NO-BREAK SPACE}' if unicode_str and unicode_str[0] == unicode_bom: unicode_str = unicode_str[1:] return unicode_str diff --git a/bingads/v10/internal/bulk/object_reader.py.bak b/bingads/v10/internal/bulk/object_reader.py.bak new file mode 100644 index 00000000..90d5f856 --- /dev/null +++ b/bingads/v10/internal/bulk/object_reader.py.bak @@ -0,0 +1,75 @@ +from .bulk_object_factory import _BulkObjectFactory +from .row_values import _RowValues +from .csv_reader import _CsvReader + +class _BulkObjectReader(): + """ Provides a method to read one row from bulk file and return the corresponding :class:`._BulkObject` """ + + def __init__(self, file_path, delimiter, encoding=None): + self._file_path = file_path + self._delimiter = delimiter + self._encoding = encoding + + self._csv_reader = _CsvReader(self.file_path, delimiter=self.delimiter) + self._csv_reader.__enter__() + headers = self._read_headers() + self._column_mapping = dict(zip(headers, range(0, len(headers)))) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._csv_reader.__exit__(exc_type, exc_value, traceback) + + def __iter__(self): + return self + + def __next__(self): + return self.read_next_bulk_object() + + def next(self): + return self.__next__() + + def read_next_bulk_object(self): + """ Reads the next csv row values, creates a new instance of the object and populates it with the row values. + + :return: next bulk object + :rtype: _BulkObject + """ + try: + row_values = self._read_next_row_values() + except StopIteration: + return None + bulk_object = _BulkObjectFactory.create_bulk_object(row_values) + bulk_object.read_from_row_values(row_values) + return bulk_object + + def close(self): + self.__exit__(None, None, None) + + def _read_next_row_values(self): + values = next(self._csv_reader) + return _RowValues(columns=values, mappings=self._column_mapping) + + def _read_headers(self): + # Need to strip BOM marker by hand, take care + def remove_bom(unicode_str): + unicode_bom = u'\N{ZERO WIDTH NO-BREAK SPACE}' + if unicode_str and unicode_str[0] == unicode_bom: + unicode_str = unicode_str[1:] + return unicode_str + + headers = next(self._csv_reader) + return [remove_bom(header) for header in headers] + + @property + def file_path(self): + return self._file_path + + @property + def delimiter(self): + return self._delimiter + + @property + def encoding(self): + return self._encoding diff --git a/bingads/v10/internal/bulk/row_values.py b/bingads/v10/internal/bulk/row_values.py index ef1af953..b208e807 100644 --- a/bingads/v10/internal/bulk/row_values.py +++ b/bingads/v10/internal/bulk/row_values.py @@ -26,7 +26,7 @@ def __len__(self): return len(self.mappings) def __str__(self): - return u'{' + u', '.join([u'{0}:{1}'.format(k, self.columns[v]) for (k, v) in self.mappings.items()]) + u'}' + return '{' + ', '.join(['{0}:{1}'.format(k, self.columns[v]) for (k, v) in list(self.mappings.items())]) + '}' def convert_to_entity(self, entity, bulk_mappings): for mapping in bulk_mappings: @@ -49,7 +49,7 @@ def _create_entity_read_exception(self, entity, mapping, ex): message += " See ColumnValues for detailed row information and InnerException for error details." if six.PY2: message = message.decode('ascii') - message += u' row values: {0}'.format(self) + message += ' row values: {0}'.format(self) return EntityReadException(message=message, row_values=str(self), inner_exception=ex) @@ -59,7 +59,7 @@ def try_get_value(self, header): return True, self[header] def to_dict(self): - return dict([(k, self.columns[v]) for (k, v) in self.mappings.items()]) + return dict([(k, self.columns[v]) for (k, v) in list(self.mappings.items())]) @property def mappings(self): diff --git a/bingads/v10/internal/bulk/row_values.py.bak b/bingads/v10/internal/bulk/row_values.py.bak new file mode 100644 index 00000000..ef1af953 --- /dev/null +++ b/bingads/v10/internal/bulk/row_values.py.bak @@ -0,0 +1,70 @@ +from .csv_headers import _CsvHeaders +from .mappings import _SimpleBulkMapping +from bingads.v10.bulk import EntityReadException +import six + + +class _RowValues: + def __init__(self, mappings=None, columns=None): + self._mappings = mappings + self._columns = columns + if self.mappings is None: + self._mappings = _CsvHeaders.get_mappings() + if self.columns is None: + self._columns = [None] * len(self._mappings) + + def __getitem__(self, key): + return self.columns[self._mappings[key]] + + def __setitem__(self, key, value): + self.columns[self._mappings[key]] = value + + def __contains__(self, item): + return item in self.mappings + + def __len__(self): + return len(self.mappings) + + def __str__(self): + return u'{' + u', '.join([u'{0}:{1}'.format(k, self.columns[v]) for (k, v) in self.mappings.items()]) + u'}' + + def convert_to_entity(self, entity, bulk_mappings): + for mapping in bulk_mappings: + try: + mapping.convert_to_entity(self, entity) + except Exception as ex: + raise self._create_entity_read_exception(entity, mapping, ex) + + def _create_entity_read_exception(self, entity, mapping, ex): + entity_type = str(type(entity)) + + if isinstance(mapping, _SimpleBulkMapping): + message = "Couldn't parse column {0} of {1} entity: {2}".format( + mapping.header, + entity_type, + str(ex) + ) + else: + message = "Couldn't parse {0} entity: {1}".format(entity_type, str(ex)) + message += " See ColumnValues for detailed row information and InnerException for error details." + if six.PY2: + message = message.decode('ascii') + message += u' row values: {0}'.format(self) + + return EntityReadException(message=message, row_values=str(self), inner_exception=ex) + + def try_get_value(self, header): + if header not in self.mappings: + return False, None + return True, self[header] + + def to_dict(self): + return dict([(k, self.columns[v]) for (k, v) in self.mappings.items()]) + + @property + def mappings(self): + return self._mappings + + @property + def columns(self): + return self._columns diff --git a/bingads/v10/internal/bulk/stream_reader.py b/bingads/v10/internal/bulk/stream_reader.py index 72377280..9d7855d8 100644 --- a/bingads/v10/internal/bulk/stream_reader.py +++ b/bingads/v10/internal/bulk/stream_reader.py @@ -37,7 +37,7 @@ def __next__(self): def close(self): self.__exit__(None, None, None) - def next(self): + def __next__(self): return self.__next__() def read(self): diff --git a/bingads/v10/internal/bulk/stream_reader.py.bak b/bingads/v10/internal/bulk/stream_reader.py.bak new file mode 100644 index 00000000..72377280 --- /dev/null +++ b/bingads/v10/internal/bulk/stream_reader.py.bak @@ -0,0 +1,99 @@ +from bingads.v10.bulk import DownloadFileType +from .bulk_object import _BulkObject +#from ..error_messages import _ErrorMessages +from .format_version import _FormatVersion +from .object_reader import _BulkObjectReader + +from bingads.internal.error_messages import _ErrorMessages + +class _BulkStreamReader(): + """ Reads a bulk object and also its related data (for example corresponding errors) from the stream.""" + + __SUPPORTED_VERSIONS = ["4", "4.0"] + + def __init__(self, file_path, file_format, encoding=None): + self._file_path = file_path + self._file_format = file_format + self._encoding = encoding + + self._delimiter = ',' if self.file_format == DownloadFileType.csv else '\t' + self._passed_first_row = False + self._bulk_object_reader = _BulkObjectReader(self.file_path, self.delimiter) + self._bulk_object_reader.__enter__() + self._next_object = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._bulk_object_reader.__exit__(exc_type, exc_val, exc_tb) + + def __iter__(self): + return self + + def __next__(self): + return self.read() + + def close(self): + self.__exit__(None, None, None) + + def next(self): + return self.__next__() + + def read(self): + """ Returns the next object from the file + + :return: next object + :rtype: :class:`._BulkObject` + """ + + _, bulk_object = self.try_read(_BulkObject) + return bulk_object + + def try_read(self, return_type, predicate=lambda x: True): + """ Reads the object only if it has a certain type + + :param return_type: The type of object that should be returned + :type return_type: class + :param predicate: A test that should be run against the object + :type predicate: function accepting on argument of the BulkObject + :return: an object of the type specified + :rtype: (bool, _BulkObject) + """ + peeked = self._peek() + if peeked is not None and isinstance(peeked, return_type) and predicate(peeked): + self._next_object = None + peeked.read_related_data_from_stream(self) + return True, peeked + return False, None + + def _peek(self): + if not self._passed_first_row: + first_row_object = self._bulk_object_reader.read_next_bulk_object() + if isinstance(first_row_object, _FormatVersion): + if first_row_object.value not in _BulkStreamReader.__SUPPORTED_VERSIONS: + raise NotImplementedError( + _ErrorMessages.get_format_version_not_supported_message(str(first_row_object.value))) + else: + self._next_object = first_row_object + self._passed_first_row = True + if self._next_object is not None: + return self._next_object + self._next_object = self._bulk_object_reader.read_next_bulk_object() + return self._next_object + + @property + def file_path(self): + return self._file_path + + @property + def file_format(self): + return self._file_format + + @property + def delimiter(self): + return self._delimiter + + @property + def encoding(self): + return self._encoding diff --git a/bingads/v11/bulk/entities/bulk_negative_sites.py b/bingads/v11/bulk/entities/bulk_negative_sites.py index d6f4f1fa..c2b99c2e 100644 --- a/bingads/v11/bulk/entities/bulk_negative_sites.py +++ b/bingads/v11/bulk/entities/bulk_negative_sites.py @@ -450,10 +450,10 @@ def convert_api_to_bulk_negative_site(website): bulk_ad_group_negative_site._website = website return bulk_ad_group_negative_site - return map(convert_api_to_bulk_negative_site, self._ad_group_negative_sites.NegativeSites.string) + return list(map(convert_api_to_bulk_negative_site, self._ad_group_negative_sites.NegativeSites.string)) def reconstruct_api_objects(self): - self._ad_group_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + self._ad_group_negative_sites.NegativeSites.string = list([x.website for x in self.negative_sites]) def _create_identifier(self): return _BulkAdGroupNegativeSitesIdentifier( @@ -559,10 +559,10 @@ def convert_api_to_bulk_negative_site(website): bulk_campaign_negative_site._website = website return bulk_campaign_negative_site - return map(convert_api_to_bulk_negative_site, self._campaign_negative_sites.NegativeSites.string) + return list(map(convert_api_to_bulk_negative_site, self._campaign_negative_sites.NegativeSites.string)) def reconstruct_api_objects(self): - self._campaign_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + self._campaign_negative_sites.NegativeSites.string = list([x.website for x in self.negative_sites]) def _create_identifier(self): return _BulkCampaignNegativeSitesIdentifier( diff --git a/bingads/v11/bulk/entities/bulk_negative_sites.py.bak b/bingads/v11/bulk/entities/bulk_negative_sites.py.bak new file mode 100644 index 00000000..d6f4f1fa --- /dev/null +++ b/bingads/v11/bulk/entities/bulk_negative_sites.py.bak @@ -0,0 +1,761 @@ +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 + +from bingads.v11.internal.bulk.entities.bulk_entity_identifier import _BulkEntityIdentifier +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping, _DynamicColumnNameMapping +from bingads.v11.internal.bulk.entities.multi_record_bulk_entity import _MultiRecordBulkEntity +from bingads.v11.internal.extensions import bulk_str + + +class _BulkNegativeSite(_SingleRecordBulkEntity): + """ This abstract base class for the bulk negative sites that are assigned individually to a campaign or ad group entity. + + *See also:* + + * :class:`.BulkAdGroupNegativeSite` + * :class:`.BulkCampaignNegativeSite` + """ + + def __init__(self, identifier, website=None): + super(_BulkNegativeSite, self).__init__() + + self._identifier = identifier + self._website = website + + @property + def website(self): + """ The URL of a website on which you do not want your ads displayed. + + Corresponds to the 'Website' field in the bulk file. + + :rtype: str + """ + + return self._website + + @website.setter + def website(self, website): + self._website = website + + @property + def status(self): + """ The status of the negative site association. + + :rtype: str + """ + + return self._identifier.status + + @status.setter + def status(self, value): + self._identifier.status = value + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Website, + field_to_csv=lambda c: c.website, + csv_to_field=lambda c, v: setattr(c, 'website', v) + ), + ] + + def process_mappings_from_row_values(self, row_values): + self._identifier.read_from_row_values(row_values) + row_values.convert_to_entity(self, _BulkNegativeSite._MAPPINGS) + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._identifier.write_to_row_values(row_values, exclude_readonly_data) + self.convert_to_values(row_values, _BulkNegativeSite._MAPPINGS) + + @property + def can_enclose_in_multiline_entity(self): + return True + + def enclose_in_multiline_entity(self): + return self.create_negative_sites_with_this_negative_site() + + def create_negative_sites_with_this_negative_site(self): + raise NotImplementedError() + + def read_additional_data(self, stream_reader): + super(_BulkNegativeSite, self).read_additional_data(stream_reader) + + +class BulkAdGroupNegativeSite(_BulkNegativeSite): + """ Represents a negative site that is assigned to an ad group. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Ad Group Negative Site record in a bulk file. + + For more information, see Ad Group Negative Site at https://go.microsoft.com/fwlink/?linkid=846127. + + One :class:`.BulkAdGroupNegativeSites` exposes a read only list of :class:`.BulkAdGroupNegativeSite`. Each + :class:`.BulkAdGroupNegativeSite` instance corresponds to one Ad Group Negative Site record in the bulk file. If you + upload a :class:`.BulkAdGroupNegativeSites`, then you are effectively replacing any existing negative sites + assigned to the ad group. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + status=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None, + website=None): + super(BulkAdGroupNegativeSite, self).__init__( + _BulkAdGroupNegativeSitesIdentifier( + status=status, + ad_group_id=ad_group_id, + ad_group_name=ad_group_name, + campaign_name=campaign_name, + ), + website=website + ) + + @property + def ad_group_id(self): + """ The identifier of the ad group that the negative site is assigned. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.ad_group_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._identifier.ad_group_id = value + + @property + def ad_group_name(self): + """ The name of the ad group that the negative site is assigned. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._identifier.ad_group_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._identifier.ad_group_name = value + + @property + def campaign_name(self): + """ The name of the ad group that the negative site is assigned. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._identifier.campaign_name + + @campaign_name.setter + def campaign_name(self, value): + self._identifier.campaign_name = value + + def create_negative_sites_with_this_negative_site(self): + return BulkAdGroupNegativeSites(site=self) + + +class BulkCampaignNegativeSite(_BulkNegativeSite): + """ Represents a negative site that is assigned to an campaign. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Campaign Negative Site record in a bulk file. + + For more information, see Campaign Negative Site at https://go.microsoft.com/fwlink/?linkid=846127. + + One :class:`.BulkCampaignNegativeSites` exposes a read only list of :class:`.BulkCampaignNegativeSite`. Each + :class:`.BulkCampaignNegativeSite` instance corresponds to one Campaign Negative Site record in the bulk file. If you + upload a :class:`.BulkCampaignNegativeSites`, then you are effectively replacing any existing negative sites + assigned to the campaign. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + status=None, + campaign_id=None, + campaign_name=None, + website=None): + super(BulkCampaignNegativeSite, self).__init__( + _BulkCampaignNegativeSitesIdentifier( + status=status, + campaign_id=campaign_id, + campaign_name=campaign_name + ), + website=website + ) + + @property + def campaign_id(self): + """ The identifier of the campaign that the negative site is assigned. + + Corresponds to the 'Parent Id' field in the bulk file. + + :rtype: int + """ + + return self._identifier.campaign_id + + @campaign_id.setter + def campaign_id(self, value): + self._identifier.campaign_id = value + + @property + def campaign_name(self): + """ The name of the campaign that the negative site is assigned. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + return self._identifier.campaign_name + + @campaign_name.setter + def campaign_name(self, value): + self._identifier.campaign_name = value + + def create_negative_sites_with_this_negative_site(self): + return BulkCampaignNegativeSites(site=self) + + +class _BulkNegativeSites(_MultiRecordBulkEntity): + """ This abstract base class for the bulk negative sites that assigned in sets to a campaign or ad group entity. """ + + def __init__(self, status=None, site=None, identifier=None): + super(_BulkNegativeSites, self).__init__() + + self._bulk_negative_sites = [] + self._first_row_identifier = None + self._has_delete_all_row = None + + self._site = site + self._identifier = identifier + + if self._site and self._identifier: + raise ValueError('Conflicting keyword arguments of site and identifier provided') + + if self._site: + if not isinstance(self._site, self.site_class): + raise ValueError('Negative site object provided is not of type: {0}'.format(self.site_class.__name__)) + self._bulk_negative_sites.append(self._site) + self._identifier = self._site._identifier + + if self._identifier: + if not isinstance(self._identifier, self.identifier_class): + raise ValueError( + 'Negative site object provided is not of type: {0}'.format(self.identifier_class.__name__)) + self._first_row_identifier = self._identifier + self._has_delete_all_row = self._identifier.is_delete_row + + self._status = status + + @property + def status(self): + """ The status of the negative site association. + + :rtype: str + """ + + return self._status + + @status.setter + def status(self, status): + self._status = status + + @property + def negative_sites(self): + return self._bulk_negative_sites + + @property + def child_entities(self): + return self.negative_sites + + def _create_identifier(self): + raise NotImplementedError() + + def _validate_properties_not_null(self): + raise NotImplementedError() + + def write_to_stream(self, row_writer, exclude_readonly_data): + self._validate_properties_not_null() + + delete_row = self._create_identifier() + delete_row._status = 'Deleted' + row_writer.write_object_row(delete_row, exclude_readonly_data) + + if self._status == 'Deleted': + return + + for site in self.convert_api_to_bulk_negative_sites(): + site.write_to_stream(row_writer, exclude_readonly_data) + + def convert_api_to_bulk_negative_sites(self): + raise NotImplementedError() + + def reconstruct_api_objects(self): + raise NotImplementedError() + + @property + def site_class(self): + raise NotImplementedError() + + @property + def identifier_class(self): + raise NotImplementedError() + + def read_related_data_from_stream(self, stream_reader): + has_more_rows = True + while has_more_rows: + site_success, site = stream_reader.try_read( + self.site_class, + lambda x: x._identifier == self._first_row_identifier + ) + if site_success: + self._bulk_negative_sites.append(site) + else: + identifier_success, identifier = stream_reader.try_read( + self.identifier_class, + lambda x: x == self._first_row_identifier + ) + if identifier_success: + if identifier.is_delete_row: + self._has_delete_all_row = True + else: + has_more_rows = False + + self.reconstruct_api_objects() + self._status = 'Active' if self._bulk_negative_sites else 'Deleted' + + @property + def all_children_are_present(self): + return self._has_delete_all_row + + +class BulkAdGroupNegativeSites(_BulkNegativeSites): + """ Represents one or more negative sites that are assigned to an ad group. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Ad Group Negative Site record in a bulk file. + + For more information, see Ad Group Negative Site at https://go.microsoft.com/fwlink/?linkid=846127. + + One :class:`.BulkAdGroupNegativeSites` has one or more :class:`.BulkAdGroupNegativeSite`. Each :class:`.BulkAdGroupNegativeSite` instance + corresponds to one Ad Group Negative Site record in the bulk file. If you upload a :class:`.BulkAdGroupNegativeSites`, + then you are effectively replacing any existing negative sites assigned to the ad group. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + ad_group_negative_sites=None, + ad_group_name=None, + campaign_name=None, + status=None, + site=None, + identifier=None): + super(BulkAdGroupNegativeSites, self).__init__( + status=status, + site=site, + identifier=identifier, + ) + + self._ad_group_negative_sites = ad_group_negative_sites + self._ad_group_name = ad_group_name + self._campaign_name = campaign_name + + if self._identifier: + self.set_data_from_identifier(self._identifier) + + @property + def ad_group_negative_sites(self): + """ The AdGroupNegativeSites Data Object of the Campaign Management Service. + + subset of AdGroupNegativeSites properties are available in the Ad Group Negative Site record. + For more information, see Ad Group Negative Site at https://go.microsoft.com/fwlink/?linkid=846127. + """ + + return self._ad_group_negative_sites + + @ad_group_negative_sites.setter + def ad_group_negative_sites(self, ad_group_negative_sites): + self._ad_group_negative_sites = ad_group_negative_sites + + @property + def ad_group_name(self): + """ The name of the ad group that the negative site is assigned. + + Corresponds to the 'Ad Group' field in the bulk file. + + :rtype: str + """ + + return self._ad_group_name + + @ad_group_name.setter + def ad_group_name(self, ad_group_name): + self._ad_group_name = ad_group_name + + @property + def campaign_name(self): + """ The name of the campaign that the negative site is assigned. + + Corresponds to the 'Campaign' field in the bulk file. + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def set_data_from_identifier(self, identifier): + self._ad_group_negative_sites = _CAMPAIGN_OBJECT_FACTORY_V11.create('AdGroupNegativeSites') + self._ad_group_negative_sites.AdGroupId = identifier.ad_group_id + self._ad_group_name = identifier.ad_group_name + self._campaign_name = identifier.campaign_name + + def convert_api_to_bulk_negative_sites(self): + self._validate_list_not_null_or_empty( + self._ad_group_negative_sites.NegativeSites, + self._ad_group_negative_sites.NegativeSites.string, + 'ad_group_negative_sites.negative_sites' + ) + + def convert_api_to_bulk_negative_site(website): + bulk_ad_group_negative_site = BulkAdGroupNegativeSite() + bulk_ad_group_negative_site.ad_group_id = self._ad_group_negative_sites.AdGroupId + bulk_ad_group_negative_site.ad_group_name = self._ad_group_name + bulk_ad_group_negative_site.campaign_name = self._campaign_name + bulk_ad_group_negative_site._website = website + return bulk_ad_group_negative_site + + return map(convert_api_to_bulk_negative_site, self._ad_group_negative_sites.NegativeSites.string) + + def reconstruct_api_objects(self): + self._ad_group_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + + def _create_identifier(self): + return _BulkAdGroupNegativeSitesIdentifier( + ad_group_id=self._ad_group_negative_sites.AdGroupId, + ad_group_name=self._ad_group_name, + campaign_name=self.campaign_name + ) + + def _validate_properties_not_null(self): + self._validate_property_not_null(self._ad_group_negative_sites, 'ad_group_negative_sites') + + @property + def identifier_class(self): + return _BulkAdGroupNegativeSitesIdentifier + + @property + def site_class(self): + return BulkAdGroupNegativeSite + + +class BulkCampaignNegativeSites(_BulkNegativeSites): + """ Represents one or more negative sites that are assigned to an campaign. Each negative site can be read or written in a bulk file. + + This class exposes properties that can be read and written as fields of the Campaign Negative Site record in a bulk file. + + For more information, see Campaign Negative Site at https://go.microsoft.com/fwlink/?linkid=846127. + + One :class:`.BulkCampaignNegativeSites` has one or more :class:`.BulkCampaignNegativeSite`. Each :class:`.BulkCampaignNegativeSite` instance + corresponds to one Campaign Negative Site record in the bulk file. If you upload a :class:`.BulkCampaignNegativeSites`, + then you are effectively replacing any existing negative sites assigned to the campaign. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + campaign_negative_sites=None, + campaign_name=None, + status=None, + site=None, + identifier=None): + super(BulkCampaignNegativeSites, self).__init__( + status=status, + site=site, + identifier=identifier + ) + + self._campaign_negative_sites = campaign_negative_sites + self._campaign_name = campaign_name + + if self._identifier: + self.set_data_from_identifier(self._identifier) + + @property + def campaign_negative_sites(self): + """ The CampaignNegativeSites Data Object of the Campaign Management Service. + + A subset of CampaignNegativeSites properties are available in the Campaign Negative Site record. + For more information, see Campaign Negative Site at https://go.microsoft.com/fwlink/?linkid=846127. + + """ + + return self._campaign_negative_sites + + @campaign_negative_sites.setter + def campaign_negative_sites(self, campaign_negative_sites): + self._campaign_negative_sites = campaign_negative_sites + + @property + def campaign_name(self): + """ The name of the campaign that the negative site is assigned. + + Corresponds to the 'Campaign' field in the bulk file. + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def set_data_from_identifier(self, identifier): + self._campaign_negative_sites = _CAMPAIGN_OBJECT_FACTORY_V11.create('CampaignNegativeSites') + self.campaign_negative_sites.CampaignId = identifier.campaign_id + self._campaign_name = identifier.campaign_name + + def convert_api_to_bulk_negative_sites(self): + self._validate_list_not_null_or_empty( + self._campaign_negative_sites.NegativeSites, + self._campaign_negative_sites.NegativeSites.string, + 'campaign_negative_sites.negative_sites' + ) + + def convert_api_to_bulk_negative_site(website): + bulk_campaign_negative_site = BulkCampaignNegativeSite() + bulk_campaign_negative_site.campaign_id = self._campaign_negative_sites.CampaignId + bulk_campaign_negative_site.campaign_name = self._campaign_name + bulk_campaign_negative_site._website = website + return bulk_campaign_negative_site + + return map(convert_api_to_bulk_negative_site, self._campaign_negative_sites.NegativeSites.string) + + def reconstruct_api_objects(self): + self._campaign_negative_sites.NegativeSites.string = list(map(lambda x: x.website, self.negative_sites)) + + def _create_identifier(self): + return _BulkCampaignNegativeSitesIdentifier( + campaign_id=self._campaign_negative_sites.CampaignId, + campaign_name=self._campaign_name + ) + + def _validate_properties_not_null(self): + self._validate_property_not_null(self._campaign_negative_sites, 'campaign_negative_sites') + + @property + def identifier_class(self): + return _BulkCampaignNegativeSitesIdentifier + + @property + def site_class(self): + return BulkCampaignNegativeSite + + +class _BulkNegativeSiteIdentifier(_BulkEntityIdentifier): + def __init__(self, status=None, entity_id=None, entity_name=None): + self._status = status + self._entity_id = entity_id + self._entity_name = entity_name + + @property + def status(self): + return self._status + + @property + def entity_id(self): + return self._entity_id + + @property + def entity_name(self): + return self._entity_name + + @property + def _parent_column_name(self): + raise NotImplementedError() + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Status, + field_to_csv=lambda c: bulk_str(c._status), + csv_to_field=lambda c, v: setattr(c, '_status', v if v else None) + ), + _SimpleBulkMapping( + header=_StringTable.ParentId, + field_to_csv=lambda c: None if c._entity_id == 0 else bulk_str(c._entity_id), + csv_to_field=lambda c, v: setattr(c, '_entity_id', int(v) if v else 0) + ), + _DynamicColumnNameMapping( + header_func=lambda c: c._parent_column_name, + field_to_csv=lambda c: c._entity_name, + csv_to_field=lambda c, v: setattr(c, '_entity_name', v) + ) + ] + + @property + def is_delete_row(self): + return self._status == 'Deleted' + + def read_from_row_values(self, row_values): + row_values.convert_to_entity(self, _BulkNegativeSiteIdentifier._MAPPINGS) + + def write_to_row_values(self, row_values, exclude_readonly_data): + self.convert_to_values(row_values, _BulkNegativeSiteIdentifier._MAPPINGS) + + +class _BulkCampaignNegativeSitesIdentifier(_BulkNegativeSiteIdentifier): + def __init__(self, status=None, campaign_id=None, campaign_name=None): + super(_BulkCampaignNegativeSitesIdentifier, self).__init__( + status, + campaign_id, + campaign_name, + ) + + def __eq__(self, other): + is_name_not_empty = ( + self.campaign_name is not None and + len(self.campaign_name) > 0 + ) + return ( + type(self) == type(other) and + ( + self.campaign_id == other.campaign_id or + ( + is_name_not_empty and + self.campaign_name == other.campaign_name + ) + ) + ) + + @property + def campaign_id(self): + return self._entity_id + + @campaign_id.setter + def campaign_id(self, value): + self._entity_id = value + + @property + def campaign_name(self): + return self._entity_name + + @campaign_name.setter + def campaign_name(self, value): + self._entity_name = value + + def _create_entity_with_this_identifier(self): + return BulkCampaignNegativeSites(identifier=self) + + @property + def _parent_column_name(self): + return _StringTable.Campaign + + +class _BulkAdGroupNegativeSitesIdentifier(_BulkNegativeSiteIdentifier): + def __init__(self, + status=None, + ad_group_id=None, + ad_group_name=None, + campaign_name=None): + super(_BulkAdGroupNegativeSitesIdentifier, self).__init__( + status, + ad_group_id, + ad_group_name, + ) + self._campaign_name = campaign_name + + def __eq__(self, other): + is_name_not_empty = ( + self.campaign_name is not None and + len(self.campaign_name) > 0 and + self.ad_group_name is not None and + len(self.ad_group_name) > 0 + ) + return ( + type(self) == type(other) and + ( + self.ad_group_id == other.ad_group_id or + ( + is_name_not_empty and + self.campaign_name == other.campaign_name and + self.ad_group_name == other.ad_group_name + ) + ) + ) + + @property + def ad_group_id(self): + return self._entity_id + + @ad_group_id.setter + def ad_group_id(self, value): + self._entity_id = value + + @property + def ad_group_name(self): + return self._entity_name + + @ad_group_name.setter + def ad_group_name(self, value): + self._entity_name = value + + @property + def campaign_name(self): + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + _MAPPINGS = [ + _SimpleBulkMapping( + header=_StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + ] + + def read_from_row_values(self, row_values): + super(_BulkAdGroupNegativeSitesIdentifier, self).read_from_row_values(row_values) + row_values.convert_to_entity(self, _BulkAdGroupNegativeSitesIdentifier._MAPPINGS) + + def write_to_row_values(self, row_values, exclude_readonly_data): + super(_BulkAdGroupNegativeSitesIdentifier, self).write_to_row_values(row_values, exclude_readonly_data) + self.convert_to_values(row_values, _BulkAdGroupNegativeSitesIdentifier._MAPPINGS) + + def _create_entity_with_this_identifier(self): + return BulkAdGroupNegativeSites(identifier=self) + + @property + def _parent_column_name(self): + return _StringTable.AdGroup diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py index 5081cb1b..bb3b5bc4 100644 --- a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py +++ b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py @@ -65,7 +65,7 @@ def __init__(self, _SimpleBulkMapping( _StringTable.Target, field_to_csv=lambda c: field_to_csv_LocationTarget(c.biddable_ad_group_criterion), - csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.biddable_ad_group_criterion, long(v) if v else None) + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.biddable_ad_group_criterion, int(v) if v else None) ), _SimpleBulkMapping( _StringTable.SubType, diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py.bak b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py.bak new file mode 100644 index 00000000..5081cb1b --- /dev/null +++ b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_location_criterion.py.bak @@ -0,0 +1,133 @@ +from bingads.v11.bulk.entities import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.extensions import * + +class BulkAdGroupLocationCriterion(_SingleRecordBulkEntity): + """ Represents an Ad Group Location Criterion that can be read or written in a bulk file. + + This class exposes the :attr:`biddable_ad_group_criterion` property that can be read and written as fields of the + Ad Group Location Criterion record in a bulk file. + + For more information, see Ad Group Location Criterion at https://go.microsoft.com/fwlink/?linkid=846127. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + biddable_ad_group_criterion=None, + campaign_name=None, + ad_group_name=None, ): + super(BulkAdGroupLocationCriterion, self).__init__() + + self._biddable_ad_group_criterion = biddable_ad_group_criterion + self._campaign_name = campaign_name + self._ad_group_name =ad_group_name + + _MAPPINGS = [ + _SimpleBulkMapping( + _StringTable.Status, + field_to_csv=lambda c: bulk_str(c.biddable_ad_group_criterion.Status), + csv_to_field=lambda c, v: setattr(c.biddable_ad_group_criterion, 'Status', v if v else None) + ), + _SimpleBulkMapping( + _StringTable.Id, + field_to_csv=lambda c: bulk_str(c.biddable_ad_group_criterion.Id), + csv_to_field=lambda c, v: setattr(c.biddable_ad_group_criterion, 'Id', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.biddable_ad_group_criterion.AdGroupId), + csv_to_field=lambda c, v: setattr(c.biddable_ad_group_criterion, 'AdGroupId', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + _SimpleBulkMapping( + _StringTable.AdGroup, + field_to_csv=lambda c: c.ad_group_name, + csv_to_field=lambda c, v: setattr(c, 'ad_group_name', v) + ), + _SimpleBulkMapping( + _StringTable.BidAdjustment, + field_to_csv=lambda c: field_to_csv_BidAdjustment(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_BidAdjustment(c.biddable_ad_group_criterion, float(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Target, + field_to_csv=lambda c: field_to_csv_LocationTarget(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.biddable_ad_group_criterion, long(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.SubType, + field_to_csv=lambda c: field_to_csv_LocationType(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationType(c.biddable_ad_group_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Name, + field_to_csv=lambda c: field_to_csv_LocationName(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationName(c.biddable_ad_group_criterion, v) + ), + ] + + @property + def biddable_ad_group_criterion(self): + """ Defines a Ad Group Criterion """ + + return self._biddable_ad_group_criterion + + @biddable_ad_group_criterion.setter + def biddable_ad_group_criterion(self, biddable_ad_group_criterion): + self._biddable_ad_group_criterion = biddable_ad_group_criterion + + @property + def campaign_name(self): + """ The name of the Campaign + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + @property + def ad_group_name(self): + """ The name of the Ad Group + + :rtype: str + """ + + return self._ad_group_name + + @ad_group_name.setter + def ad_group_name(self, ad_group_name): + self._ad_group_name = ad_group_name + + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._validate_property_not_null(self.biddable_ad_group_criterion, 'biddable_ad_group_criterion') + self.convert_to_values(row_values, BulkAdGroupLocationCriterion._MAPPINGS) + + def process_mappings_from_row_values(self, row_values): + self._biddable_ad_group_criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('BiddableAdGroupCriterion') + self._biddable_ad_group_criterion.Type = 'BiddableAdGroupCriterion' + self._biddable_ad_group_criterion.Criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('LocationCriterion') + self._biddable_ad_group_criterion.Criterion.Type = 'LocationCriterion' + self._biddable_ad_group_criterion.CriterionBid = _CAMPAIGN_OBJECT_FACTORY_V11.create('BidMultiplier') + self._biddable_ad_group_criterion.CriterionBid.Type = 'BidMultiplier' + row_values.convert_to_entity(self, BulkAdGroupLocationCriterion._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(BulkAdGroupLocationCriterion, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py index f87422c5..bb1480a9 100644 --- a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py +++ b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py @@ -60,7 +60,7 @@ def __init__(self, _SimpleBulkMapping( _StringTable.Target, field_to_csv=lambda c: field_to_csv_LocationTarget(c.negative_ad_group_criterion), - csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.negative_ad_group_criterion, long(v) if v else None) + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.negative_ad_group_criterion, int(v) if v else None) ), _SimpleBulkMapping( _StringTable.SubType, diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py.bak b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py.bak new file mode 100644 index 00000000..f87422c5 --- /dev/null +++ b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_negative_location_criterion.py.bak @@ -0,0 +1,126 @@ +from bingads.v11.bulk.entities import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.extensions import * + +class BulkAdGroupNegativeLocationCriterion(_SingleRecordBulkEntity): + """ Represents an Ad Group Negative Location Criterion that can be read or written in a bulk file. + + This class exposes the :attr:`negative_ad_group_criterion` property that can be read and written as fields of the + Ad Group Negative Location Criterion record in a bulk file. + + For more information, see Ad Group Negative Location Criterion at https://go.microsoft.com/fwlink/?linkid=846127. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + negative_ad_group_criterion=None, + campaign_name=None, + ad_group_name=None, ): + super(BulkAdGroupNegativeLocationCriterion, self).__init__() + + self._negative_ad_group_criterion = negative_ad_group_criterion + self._campaign_name = campaign_name + self._ad_group_name =ad_group_name + + _MAPPINGS = [ + _SimpleBulkMapping( + _StringTable.Status, + field_to_csv=lambda c: bulk_str(c.negative_ad_group_criterion.Status), + csv_to_field=lambda c, v: setattr(c.negative_ad_group_criterion, 'Status', v if v else None) + ), + _SimpleBulkMapping( + _StringTable.Id, + field_to_csv=lambda c: bulk_str(c.negative_ad_group_criterion.Id), + csv_to_field=lambda c, v: setattr(c.negative_ad_group_criterion, 'Id', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.negative_ad_group_criterion.AdGroupId), + csv_to_field=lambda c, v: setattr(c.negative_ad_group_criterion, 'AdGroupId', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + _SimpleBulkMapping( + _StringTable.AdGroup, + field_to_csv=lambda c: c.ad_group_name, + csv_to_field=lambda c, v: setattr(c, 'ad_group_name', v) + ), + _SimpleBulkMapping( + _StringTable.Target, + field_to_csv=lambda c: field_to_csv_LocationTarget(c.negative_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.negative_ad_group_criterion, long(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.SubType, + field_to_csv=lambda c: field_to_csv_LocationType(c.negative_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationType(c.negative_ad_group_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Name, + field_to_csv=lambda c: field_to_csv_LocationName(c.negative_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationName(c.negative_ad_group_criterion, v) + ), + ] + + @property + def negative_ad_group_criterion(self): + """ Defines a Ad Group Criterion """ + + return self._negative_ad_group_criterion + + @negative_ad_group_criterion.setter + def negative_ad_group_criterion(self, negative_ad_group_criterion): + self._negative_ad_group_criterion = negative_ad_group_criterion + + @property + def campaign_name(self): + """ The name of the Campaign + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + @property + def ad_group_name(self): + """ The name of the Ad Group + + :rtype: str + """ + + return self._ad_group_name + + @ad_group_name.setter + def ad_group_name(self, ad_group_name): + self._ad_group_name = ad_group_name + + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._validate_property_not_null(self.negative_ad_group_criterion, 'negative_ad_group_criterion') + self.convert_to_values(row_values, BulkAdGroupNegativeLocationCriterion._MAPPINGS) + + def process_mappings_from_row_values(self, row_values): + self._negative_ad_group_criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('NegativeAdGroupCriterion') + self._negative_ad_group_criterion.Type = 'NegativeAdGroupCriterion' + self._negative_ad_group_criterion.Criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('LocationCriterion') + self._negative_ad_group_criterion.Criterion.Type = 'LocationCriterion' + row_values.convert_to_entity(self, BulkAdGroupNegativeLocationCriterion._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(BulkAdGroupNegativeLocationCriterion, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py index 7ba4bc2f..3ea53f78 100644 --- a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py +++ b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py @@ -70,7 +70,7 @@ def __init__(self, _SimpleBulkMapping( _StringTable.Radius, field_to_csv=lambda c: field_to_csv_Radius(c.biddable_ad_group_criterion), - csv_to_field=lambda c, v: csv_to_field_Radius(c.biddable_ad_group_criterion, long(v) if v else None) + csv_to_field=lambda c, v: csv_to_field_Radius(c.biddable_ad_group_criterion, int(v) if v else None) ), _SimpleBulkMapping( _StringTable.Unit, diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py.bak b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py.bak new file mode 100644 index 00000000..7ba4bc2f --- /dev/null +++ b/bingads/v11/bulk/entities/target_criterions/bulk_ad_group_radius_criterion.py.bak @@ -0,0 +1,143 @@ +from bingads.v11.bulk.entities import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.extensions import * + +class BulkAdGroupRadiusCriterion(_SingleRecordBulkEntity): + """ Represents an Ad Group Radius Criterion that can be read or written in a bulk file. + + This class exposes the :attr:`biddable_ad_group_criterion` property that can be read and written as fields of the + Ad Group Radius Criterion record in a bulk file. + + For more information, see Ad Group Radius Criterion at https://go.microsoft.com/fwlink/?linkid=846127. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + biddable_ad_group_criterion=None, + campaign_name=None, + ad_group_name=None, ): + super(BulkAdGroupRadiusCriterion, self).__init__() + + self._biddable_ad_group_criterion = biddable_ad_group_criterion + self._campaign_name = campaign_name + self._ad_group_name =ad_group_name + + _MAPPINGS = [ + _SimpleBulkMapping( + _StringTable.Status, + field_to_csv=lambda c: bulk_str(c.biddable_ad_group_criterion.Status), + csv_to_field=lambda c, v: setattr(c.biddable_ad_group_criterion, 'Status', v if v else None) + ), + _SimpleBulkMapping( + _StringTable.Id, + field_to_csv=lambda c: bulk_str(c.biddable_ad_group_criterion.Id), + csv_to_field=lambda c, v: setattr(c.biddable_ad_group_criterion, 'Id', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.biddable_ad_group_criterion.AdGroupId), + csv_to_field=lambda c, v: setattr(c.biddable_ad_group_criterion, 'AdGroupId', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + _SimpleBulkMapping( + _StringTable.AdGroup, + field_to_csv=lambda c: c.ad_group_name, + csv_to_field=lambda c, v: setattr(c, 'ad_group_name', v) + ), + _SimpleBulkMapping( + _StringTable.BidAdjustment, + field_to_csv=lambda c: field_to_csv_BidAdjustment(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_BidAdjustment(c.biddable_ad_group_criterion, float(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Name, + field_to_csv=lambda c: field_to_csv_RadiusName(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_RadiusName(c.biddable_ad_group_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Radius, + field_to_csv=lambda c: field_to_csv_Radius(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_Radius(c.biddable_ad_group_criterion, long(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Unit, + field_to_csv=lambda c: field_to_csv_RadiusUnit(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_RadiusUnit(c.biddable_ad_group_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Latitude, + field_to_csv=lambda c: field_to_csv_LatitudeDegrees(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LatitudeDegrees(c.biddable_ad_group_criterion, float(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Longitude, + field_to_csv=lambda c: field_to_csv_LongitudeDegrees(c.biddable_ad_group_criterion), + csv_to_field=lambda c, v: csv_to_field_LongitudeDegrees(c.biddable_ad_group_criterion, float(v) if v else None) + ), + ] + + @property + def biddable_ad_group_criterion(self): + """ Defines a Ad Group Criterion """ + + return self._biddable_ad_group_criterion + + @biddable_ad_group_criterion.setter + def biddable_ad_group_criterion(self, biddable_ad_group_criterion): + self._biddable_ad_group_criterion = biddable_ad_group_criterion + + @property + def campaign_name(self): + """ The name of the Campaign + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + @property + def ad_group_name(self): + """ The name of the Ad Group + + :rtype: str + """ + + return self._ad_group_name + + @ad_group_name.setter + def ad_group_name(self, ad_group_name): + self._ad_group_name = ad_group_name + + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._validate_property_not_null(self.biddable_ad_group_criterion, 'biddable_ad_group_criterion') + self.convert_to_values(row_values, BulkAdGroupRadiusCriterion._MAPPINGS) + + def process_mappings_from_row_values(self, row_values): + self._biddable_ad_group_criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('BiddableAdGroupCriterion') + self._biddable_ad_group_criterion.Type = 'BiddableAdGroupCriterion' + self._biddable_ad_group_criterion.Criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('RadiusCriterion') + self._biddable_ad_group_criterion.Criterion.Type = 'RadiusCriterion' + self._biddable_ad_group_criterion.CriterionBid = _CAMPAIGN_OBJECT_FACTORY_V11.create('BidMultiplier') + self._biddable_ad_group_criterion.CriterionBid.Type = 'BidMultiplier' + row_values.convert_to_entity(self, BulkAdGroupRadiusCriterion._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(BulkAdGroupRadiusCriterion, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py index c1805bc0..6f660ec5 100644 --- a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py +++ b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py @@ -58,7 +58,7 @@ def __init__(self, _SimpleBulkMapping( _StringTable.Target, field_to_csv=lambda c: field_to_csv_LocationTarget(c.biddable_campaign_criterion), - csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.biddable_campaign_criterion, long(v) if v else None) + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.biddable_campaign_criterion, int(v) if v else None) ), _SimpleBulkMapping( _StringTable.SubType, diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py.bak b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py.bak new file mode 100644 index 00000000..c1805bc0 --- /dev/null +++ b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_location_criterion.py.bak @@ -0,0 +1,112 @@ +from bingads.v11.bulk.entities import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.extensions import * + +class BulkCampaignLocationCriterion(_SingleRecordBulkEntity): + """ Represents an Campaign Location Criterion that can be read or written in a bulk file. + + This class exposes the :attr:`biddable_campaign_criterion` property that can be read and written as fields of the + Campaign Location Criterion record in a bulk file. + + For more information, see Campaign Location Criterion at https://go.microsoft.com/fwlink/?linkid=846127. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + biddable_campaign_criterion=None, + campaign_name=None, ): + super(BulkCampaignLocationCriterion, self).__init__() + + self._biddable_campaign_criterion = biddable_campaign_criterion + self._campaign_name = campaign_name + + _MAPPINGS = [ + _SimpleBulkMapping( + _StringTable.Status, + field_to_csv=lambda c: bulk_str(c.biddable_campaign_criterion.Status), + csv_to_field=lambda c, v: setattr(c.biddable_campaign_criterion, 'Status', v if v else None) + ), + _SimpleBulkMapping( + _StringTable.Id, + field_to_csv=lambda c: bulk_str(c.biddable_campaign_criterion.Id), + csv_to_field=lambda c, v: setattr(c.biddable_campaign_criterion, 'Id', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.biddable_campaign_criterion.CampaignId), + csv_to_field=lambda c, v: setattr(c.biddable_campaign_criterion, 'CampaignId', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + _SimpleBulkMapping( + _StringTable.BidAdjustment, + field_to_csv=lambda c: field_to_csv_BidAdjustment(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_BidAdjustment(c.biddable_campaign_criterion, float(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Target, + field_to_csv=lambda c: field_to_csv_LocationTarget(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.biddable_campaign_criterion, long(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.SubType, + field_to_csv=lambda c: field_to_csv_LocationType(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationType(c.biddable_campaign_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Name, + field_to_csv=lambda c: field_to_csv_LocationName(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationName(c.biddable_campaign_criterion, v) + ), + ] + + @property + def biddable_campaign_criterion(self): + """ Defines a Campaign Criterion """ + + return self._biddable_campaign_criterion + + @biddable_campaign_criterion.setter + def biddable_campaign_criterion(self, biddable_campaign_criterion): + self._biddable_campaign_criterion = biddable_campaign_criterion + + @property + def campaign_name(self): + """ The name of the Campaign + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._validate_property_not_null(self.biddable_campaign_criterion, 'biddable_campaign_criterion') + self.convert_to_values(row_values, BulkCampaignLocationCriterion._MAPPINGS) + + def process_mappings_from_row_values(self, row_values): + self._biddable_campaign_criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('BiddableCampaignCriterion') + self._biddable_campaign_criterion.Type = 'BiddableCampaignCriterion' + self._biddable_campaign_criterion.Criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('LocationCriterion') + self._biddable_campaign_criterion.Criterion.Type = 'LocationCriterion' + self._biddable_campaign_criterion.CriterionBid = _CAMPAIGN_OBJECT_FACTORY_V11.create('BidMultiplier') + self._biddable_campaign_criterion.CriterionBid.Type = 'BidMultiplier' + row_values.convert_to_entity(self, BulkCampaignLocationCriterion._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(BulkCampaignLocationCriterion, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py index 8d13a9ea..7cf1253f 100644 --- a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py +++ b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py @@ -53,7 +53,7 @@ def __init__(self, _SimpleBulkMapping( _StringTable.Target, field_to_csv=lambda c: field_to_csv_LocationTarget(c.negative_campaign_criterion), - csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.negative_campaign_criterion, long(v) if v else None) + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.negative_campaign_criterion, int(v) if v else None) ), _SimpleBulkMapping( _StringTable.SubType, diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py.bak b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py.bak new file mode 100644 index 00000000..8d13a9ea --- /dev/null +++ b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_negative_location_criterion.py.bak @@ -0,0 +1,105 @@ +from bingads.v11.bulk.entities import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.extensions import * + +class BulkCampaignNegativeLocationCriterion(_SingleRecordBulkEntity): + """ Represents an Campaign Negative Location Criterion that can be read or written in a bulk file. + + This class exposes the :attr:`negative_campaign_criterion` property that can be read and written as fields of the + Campaign Negative Location Criterion record in a bulk file. + + For more information, see Campaign Negative Location Criterion at https://go.microsoft.com/fwlink/?linkid=846127. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + negative_campaign_criterion=None, + campaign_name=None, ): + super(BulkCampaignNegativeLocationCriterion, self).__init__() + + self._negative_campaign_criterion = negative_campaign_criterion + self._campaign_name = campaign_name + + _MAPPINGS = [ + _SimpleBulkMapping( + _StringTable.Status, + field_to_csv=lambda c: bulk_str(c.negative_campaign_criterion.Status), + csv_to_field=lambda c, v: setattr(c.negative_campaign_criterion, 'Status', v if v else None) + ), + _SimpleBulkMapping( + _StringTable.Id, + field_to_csv=lambda c: bulk_str(c.negative_campaign_criterion.Id), + csv_to_field=lambda c, v: setattr(c.negative_campaign_criterion, 'Id', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.negative_campaign_criterion.CampaignId), + csv_to_field=lambda c, v: setattr(c.negative_campaign_criterion, 'CampaignId', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + _SimpleBulkMapping( + _StringTable.Target, + field_to_csv=lambda c: field_to_csv_LocationTarget(c.negative_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationTarget(c.negative_campaign_criterion, long(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.SubType, + field_to_csv=lambda c: field_to_csv_LocationType(c.negative_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationType(c.negative_campaign_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Name, + field_to_csv=lambda c: field_to_csv_LocationName(c.negative_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LocationName(c.negative_campaign_criterion, v) + ), + ] + + @property + def negative_campaign_criterion(self): + """ Defines a Campaign Criterion """ + + return self._negative_campaign_criterion + + @negative_campaign_criterion.setter + def negative_campaign_criterion(self, negative_campaign_criterion): + self._negative_campaign_criterion = negative_campaign_criterion + + @property + def campaign_name(self): + """ The name of the Campaign + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._validate_property_not_null(self.negative_campaign_criterion, 'negative_campaign_criterion') + self.convert_to_values(row_values, BulkCampaignNegativeLocationCriterion._MAPPINGS) + + def process_mappings_from_row_values(self, row_values): + self._negative_campaign_criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('NegativeCampaignCriterion') + self._negative_campaign_criterion.Type = 'NegativeCampaignCriterion' + self._negative_campaign_criterion.Criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('LocationCriterion') + self._negative_campaign_criterion.Criterion.Type = 'LocationCriterion' + row_values.convert_to_entity(self, BulkCampaignNegativeLocationCriterion._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(BulkCampaignNegativeLocationCriterion, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py index 08f8435a..26a38cff 100644 --- a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py +++ b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py @@ -63,7 +63,7 @@ def __init__(self, _SimpleBulkMapping( _StringTable.Radius, field_to_csv=lambda c: field_to_csv_Radius(c.biddable_campaign_criterion), - csv_to_field=lambda c, v: csv_to_field_Radius(c.biddable_campaign_criterion, long(v) if v else None) + csv_to_field=lambda c, v: csv_to_field_Radius(c.biddable_campaign_criterion, int(v) if v else None) ), _SimpleBulkMapping( _StringTable.Unit, diff --git a/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py.bak b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py.bak new file mode 100644 index 00000000..08f8435a --- /dev/null +++ b/bingads/v11/bulk/entities/target_criterions/bulk_campaign_radius_criterion.py.bak @@ -0,0 +1,122 @@ +from bingads.v11.bulk.entities import * +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11 +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity +from bingads.v11.internal.bulk.mappings import _SimpleBulkMapping +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.extensions import * + +class BulkCampaignRadiusCriterion(_SingleRecordBulkEntity): + """ Represents an Campaign Radius Criterion that can be read or written in a bulk file. + + This class exposes the :attr:`biddable_campaign_criterion` property that can be read and written as fields of the + Campaign Radius Criterion record in a bulk file. + + For more information, see Campaign Radius Criterion at https://go.microsoft.com/fwlink/?linkid=846127. + + *See also:* + + * :class:`.BulkServiceManager` + * :class:`.BulkOperation` + * :class:`.BulkFileReader` + * :class:`.BulkFileWriter` + """ + + def __init__(self, + biddable_campaign_criterion=None, + campaign_name=None, ): + super(BulkCampaignRadiusCriterion, self).__init__() + + self._biddable_campaign_criterion = biddable_campaign_criterion + self._campaign_name = campaign_name + + _MAPPINGS = [ + _SimpleBulkMapping( + _StringTable.Status, + field_to_csv=lambda c: bulk_str(c.biddable_campaign_criterion.Status), + csv_to_field=lambda c, v: setattr(c.biddable_campaign_criterion, 'Status', v if v else None) + ), + _SimpleBulkMapping( + _StringTable.Id, + field_to_csv=lambda c: bulk_str(c.biddable_campaign_criterion.Id), + csv_to_field=lambda c, v: setattr(c.biddable_campaign_criterion, 'Id', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.ParentId, + field_to_csv=lambda c: bulk_str(c.biddable_campaign_criterion.CampaignId), + csv_to_field=lambda c, v: setattr(c.biddable_campaign_criterion, 'CampaignId', int(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Campaign, + field_to_csv=lambda c: c.campaign_name, + csv_to_field=lambda c, v: setattr(c, 'campaign_name', v) + ), + _SimpleBulkMapping( + _StringTable.BidAdjustment, + field_to_csv=lambda c: field_to_csv_BidAdjustment(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_BidAdjustment(c.biddable_campaign_criterion, float(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Name, + field_to_csv=lambda c: field_to_csv_RadiusName(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_RadiusName(c.biddable_campaign_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Radius, + field_to_csv=lambda c: field_to_csv_Radius(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_Radius(c.biddable_campaign_criterion, long(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Unit, + field_to_csv=lambda c: field_to_csv_RadiusUnit(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_RadiusUnit(c.biddable_campaign_criterion, v) + ), + _SimpleBulkMapping( + _StringTable.Latitude, + field_to_csv=lambda c: field_to_csv_LatitudeDegrees(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LatitudeDegrees(c.biddable_campaign_criterion, float(v) if v else None) + ), + _SimpleBulkMapping( + _StringTable.Longitude, + field_to_csv=lambda c: field_to_csv_LongitudeDegrees(c.biddable_campaign_criterion), + csv_to_field=lambda c, v: csv_to_field_LongitudeDegrees(c.biddable_campaign_criterion, float(v) if v else None) + ), + ] + + @property + def biddable_campaign_criterion(self): + """ Defines a Campaign Criterion """ + + return self._biddable_campaign_criterion + + @biddable_campaign_criterion.setter + def biddable_campaign_criterion(self, biddable_campaign_criterion): + self._biddable_campaign_criterion = biddable_campaign_criterion + + @property + def campaign_name(self): + """ The name of the Campaign + + :rtype: str + """ + + return self._campaign_name + + @campaign_name.setter + def campaign_name(self, campaign_name): + self._campaign_name = campaign_name + + def process_mappings_to_row_values(self, row_values, exclude_readonly_data): + self._validate_property_not_null(self.biddable_campaign_criterion, 'biddable_campaign_criterion') + self.convert_to_values(row_values, BulkCampaignRadiusCriterion._MAPPINGS) + + def process_mappings_from_row_values(self, row_values): + self._biddable_campaign_criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('BiddableCampaignCriterion') + self._biddable_campaign_criterion.Type = 'BiddableCampaignCriterion' + self._biddable_campaign_criterion.Criterion = _CAMPAIGN_OBJECT_FACTORY_V11.create('RadiusCriterion') + self._biddable_campaign_criterion.Criterion.Type = 'RadiusCriterion' + self._biddable_campaign_criterion.CriterionBid = _CAMPAIGN_OBJECT_FACTORY_V11.create('BidMultiplier') + self._biddable_campaign_criterion.CriterionBid.Type = 'BidMultiplier' + row_values.convert_to_entity(self, BulkCampaignRadiusCriterion._MAPPINGS) + + def read_additional_data(self, stream_reader): + super(BulkCampaignRadiusCriterion, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/entities/unknown_bulk_entity.py b/bingads/v11/bulk/entities/unknown_bulk_entity.py index 528c6433..98a859d1 100644 --- a/bingads/v11/bulk/entities/unknown_bulk_entity.py +++ b/bingads/v11/bulk/entities/unknown_bulk_entity.py @@ -21,7 +21,7 @@ def process_mappings_from_row_values(self, row_values): self._values = row_values.to_dict() def process_mappings_to_row_values(self, row_values): - for (key, value) in self._values.items(): + for (key, value) in list(self._values.items()): row_values[key] = value def read_additional_data(self, stream_reader): diff --git a/bingads/v11/bulk/entities/unknown_bulk_entity.py.bak b/bingads/v11/bulk/entities/unknown_bulk_entity.py.bak new file mode 100644 index 00000000..528c6433 --- /dev/null +++ b/bingads/v11/bulk/entities/unknown_bulk_entity.py.bak @@ -0,0 +1,28 @@ +from bingads.v11.internal.bulk.entities.single_record_bulk_entity import _SingleRecordBulkEntity + + +class UnknownBulkEntity(_SingleRecordBulkEntity): + """ Reserved to support new record types that may be added to the Bulk schema. """ + + def __init__(self): + super(UnknownBulkEntity, self).__init__() + self._values = None + + @property + def values(self): + """ The forward compatibility map of fields and values. + + :rtype: dict | None + """ + + return self._values + + def process_mappings_from_row_values(self, row_values): + self._values = row_values.to_dict() + + def process_mappings_to_row_values(self, row_values): + for (key, value) in self._values.items(): + row_values[key] = value + + def read_additional_data(self, stream_reader): + super(UnknownBulkEntity, self).read_additional_data(stream_reader) diff --git a/bingads/v11/bulk/file_reader.py b/bingads/v11/bulk/file_reader.py index 682e8f26..5790cc89 100644 --- a/bingads/v11/bulk/file_reader.py +++ b/bingads/v11/bulk/file_reader.py @@ -48,12 +48,12 @@ def __iter__(self): return self def __next__(self): - return self.next() + return next(self) def close(self): self.__exit__(None, None, None) - def next(self): + def __next__(self): return self.read_next_entity() def read_next_entity(self): diff --git a/bingads/v11/bulk/file_reader.py.bak b/bingads/v11/bulk/file_reader.py.bak new file mode 100644 index 00000000..682e8f26 --- /dev/null +++ b/bingads/v11/bulk/file_reader.py.bak @@ -0,0 +1,157 @@ +from .enums import DownloadFileType, ResultFileType +from .entities.bulk_entity import BulkEntity +from bingads.v11.internal.bulk.stream_reader import _BulkStreamReader +from bingads.v11.internal.bulk.entities.multi_record_bulk_entity import _MultiRecordBulkEntity + + +class BulkFileReader: + """ Provides a method to read bulk entities from a bulk file and make them accessible as an enumerable list. + + For more information about the Bulk File Schema, see https://go.microsoft.com/fwlink/?linkid=846127. + """ + + def __init__(self, + file_path, + file_format=DownloadFileType.csv, + result_file_type=ResultFileType.full_download, + encoding=None): + """ Initializes a new instance of this class with the specified file details. + + :param file_path: The path of the bulk file to read. + :type file_path: str + :param file_format: The bulk file format. + :type file_format: DownloadFileType + :param result_file_type: The result file type. + :type result_file_type: ResultFileType + :param encoding: The encoding of bulk file. + :type encoding: str + """ + + self._file_path = file_path + self._file_format = file_format + self._result_file_type = result_file_type + self._encoding = encoding + + self._is_for_full_download = result_file_type is ResultFileType.full_download + self._entities_iterator = None + self._bulk_stream_reader = _BulkStreamReader(file_path=self.file_path, file_format=self.file_format) + self._bulk_stream_reader.__enter__() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._entities_iterator = None + self._bulk_stream_reader.__exit__(exc_type, exc_value, traceback) + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def close(self): + self.__exit__(None, None, None) + + def next(self): + return self.read_next_entity() + + def read_next_entity(self): + """ Reads next entity from the bulk file. + + :return: next entity + :rtype: BulkEntity + """ + + if self._entities_iterator is None: + self._entities_iterator = self.read_entities() + + return next(self._entities_iterator) + + def read_entities(self): + """ Gets an enumerable list of bulk entities that were read from the file. + + :return: an generator over the entities + :rtype: collections.Iterator[BulkEntity] + """ + + next_batch = self._read_next_batch() + + while next_batch is not None: + for entity in next_batch: + yield entity + next_batch = self._read_next_batch() + + def _read_next_batch(self): + """ Reads next batch of entities from the file. + + Batch means a set of related entities. + It can be one :class:`._SingleRecordBulkEntity`, one :class:`._MultiRecordBulkEntity` containing its child + entities or a set of related child entities (for example several :class:`.BulkSiteLink`s logically belonging + to the same SiteLink Ad Extension. + + :return: Next batch of entities + :rtype: _SingleRecordBulkEntity or _MultiRecordBulkEntity + """ + + next_object = self._bulk_stream_reader.read() + + if next_object is None: + return None + if next_object.can_enclose_in_multiline_entity: + multi_record_entity = next_object.enclose_in_multiline_entity() + multi_record_entity.read_related_data_from_stream(self._bulk_stream_reader) + if self._is_for_full_download: + return [multi_record_entity] + return self._extract_child_entities_if_needed(multi_record_entity) + if isinstance(next_object, BulkEntity): + return [next_object] + raise NotImplementedError() + + def _extract_child_entities_if_needed(self, entity): + # If the entity is a MultiLine entity and it has all child objects (delete all row was present), just return it + if not isinstance(entity, _MultiRecordBulkEntity) or entity.all_children_are_present: + yield entity + else: + # If not all child objects are present (there was no delete all row and we only have part of the MultiLine entity), return child object individually + for child_generator in ( + self._extract_child_entities_if_needed(child_entity) + for child_entity in entity.child_entities): + for child in child_generator: + yield child + + @property + def file_path(self): + """ The path of the bulk file to read. + + :rtype: str + """ + + return self._file_path + + @property + def file_format(self): + """ The bulk file format. + + :rtype: DownloadFileType + """ + + return self._file_format + + @property + def result_file_type(self): + """ The result file type. + + :rtype: ResultFileType + """ + + return self._result_file_type + + @property + def encoding(self): + """ The encoding of bulk file. + + :rtype: str + """ + + return self._encoding diff --git a/bingads/v11/internal/bulk/bulk_object_factory.py b/bingads/v11/internal/bulk/bulk_object_factory.py index 81c090f2..400303d6 100644 --- a/bingads/v11/internal/bulk/bulk_object_factory.py +++ b/bingads/v11/internal/bulk/bulk_object_factory.py @@ -128,7 +128,7 @@ def get_bulk_row_type(bulk_object): return _BulkObjectFactory.TYPE_REVERSE_MAP[type(bulk_object)] -for (k, v) in _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP.items(): +for (k, v) in list(_BulkObjectFactory.INDIVIDUAL_ENTITY_MAP.items()): _BulkObjectFactory.TYPE_REVERSE_MAP[type(v.create_func())] = k if v.create_identifier_func is not None: diff --git a/bingads/v11/internal/bulk/bulk_object_factory.py.bak b/bingads/v11/internal/bulk/bulk_object_factory.py.bak new file mode 100644 index 00000000..81c090f2 --- /dev/null +++ b/bingads/v11/internal/bulk/bulk_object_factory.py.bak @@ -0,0 +1,136 @@ +from bingads.v11.bulk.entities import * +from bingads.v11.bulk.entities.ad_extensions.bulk_site_links_ad_extensions import _SiteLinkAdExtensionIdentifier +from bingads.v11.internal.bulk.string_table import _StringTable +from bingads.v11.internal.bulk.entity_info import _EntityInfo +from bingads.v11.bulk.entities.bulk_negative_sites import _BulkAdGroupNegativeSitesIdentifier, \ + _BulkCampaignNegativeSitesIdentifier +from bingads.v11.internal.bulk.format_version import _FormatVersion + + +class _BulkObjectFactory(): + INDIVIDUAL_ENTITY_MAP = { + _StringTable.Account: _EntityInfo(lambda: BulkAccount()), + _StringTable.Budget: _EntityInfo(lambda: BulkBudget()), + _StringTable.Campaign: _EntityInfo(lambda: BulkCampaign()), + _StringTable.AdGroup: _EntityInfo(lambda: BulkAdGroup()), + _StringTable.Keyword: _EntityInfo(lambda: BulkKeyword()), + _StringTable.SiteLinksAdExtension: _EntityInfo( + lambda: BulkSiteLink(), + _StringTable.SiteLinkExtensionOrder, + lambda: _SiteLinkAdExtensionIdentifier() + ), + _StringTable.CampaignSiteLinksAdExtension: _EntityInfo( + lambda: BulkCampaignSiteLinkAdExtension() + ), + _StringTable.AdGroupSiteLinksAdExtension: _EntityInfo( + lambda: BulkAdGroupSiteLinkAdExtension() + ), + _StringTable.CallAdExtension: _EntityInfo(lambda: BulkCallAdExtension()), + _StringTable.CampaignCallAdExtension: _EntityInfo(lambda: BulkCampaignCallAdExtension()), + _StringTable.ImageAdExtension: _EntityInfo(lambda: BulkImageAdExtension()), + _StringTable.CampaignImageAdExtension: _EntityInfo(lambda: BulkCampaignImageAdExtension()), + _StringTable.AdGroupImageAdExtension: _EntityInfo(lambda: BulkAdGroupImageAdExtension()), + _StringTable.CalloutAdExtension: _EntityInfo(lambda: BulkCalloutAdExtension()), + _StringTable.CampaignCalloutAdExtension: _EntityInfo(lambda: BulkCampaignCalloutAdExtension()), + _StringTable.AdGroupCalloutAdExtension: _EntityInfo(lambda: BulkAdGroupCalloutAdExtension()), + _StringTable.ReviewAdExtension: _EntityInfo(lambda: BulkReviewAdExtension()), + _StringTable.CampaignReviewAdExtension: _EntityInfo(lambda: BulkCampaignReviewAdExtension()), + _StringTable.AdGroupReviewAdExtension: _EntityInfo(lambda: BulkAdGroupReviewAdExtension()), + _StringTable.LocationAdExtension: _EntityInfo(lambda: BulkLocationAdExtension()), + _StringTable.CampaignLocationAdExtension: _EntityInfo(lambda: BulkCampaignLocationAdExtension()), + _StringTable.AppAdExtension: _EntityInfo(lambda: BulkAppAdExtension()), + _StringTable.CampaignAppAdExtension: _EntityInfo(lambda: BulkCampaignAppAdExtension()), + _StringTable.AdGroupAppAdExtension: _EntityInfo(lambda: BulkAdGroupAppAdExtension()), + _StringTable.StructuredSnippetAdExtension: _EntityInfo(lambda: BulkStructuredSnippetAdExtension()), + _StringTable.CampaignStructuredSnippetAdExtension: _EntityInfo(lambda: BulkCampaignStructuredSnippetAdExtension()), + _StringTable.AdGroupStructuredSnippetAdExtension: _EntityInfo(lambda: BulkAdGroupStructuredSnippetAdExtension()), + _StringTable.Sitelink2AdExtension: _EntityInfo(lambda: BulkSitelink2AdExtension()), + _StringTable.CampaignSitelink2AdExtension: _EntityInfo(lambda: BulkCampaignSitelink2AdExtension()), + _StringTable.AdGroupSitelink2AdExtension: _EntityInfo(lambda: BulkAdGroupSitelink2AdExtension()), + _StringTable.ProductAd: _EntityInfo(lambda: BulkProductAd()), + _StringTable.TextAd: _EntityInfo(lambda: BulkTextAd()), + _StringTable.AppInstallAd: _EntityInfo(lambda: BulkAppInstallAd()), + _StringTable.ExpandedTextAd: _EntityInfo(lambda: BulkExpandedTextAd()), + _StringTable.DynamicSearchAd: _EntityInfo(lambda: BulkDynamicSearchAd()), + "Campaign Negative Site": _EntityInfo( + lambda: BulkCampaignNegativeSite(), + _StringTable.Website, + lambda: _BulkCampaignNegativeSitesIdentifier() + ), + "Ad Group Negative Site": _EntityInfo( + lambda: BulkAdGroupNegativeSite(), + _StringTable.Website, + lambda: _BulkAdGroupNegativeSitesIdentifier() + ), + + _StringTable.NegativeKeywordList: _EntityInfo(lambda: BulkNegativeKeywordList()), + _StringTable.ListNegativeKeyword: _EntityInfo(lambda: BulkSharedNegativeKeyword()), + _StringTable.CampaignNegativeKeywordList: _EntityInfo(lambda: BulkCampaignNegativeKeywordList()), + _StringTable.CampaignNegativeKeyword: _EntityInfo(lambda: BulkCampaignNegativeKeyword()), + _StringTable.AdGroupNegativeKeyword: _EntityInfo(lambda: BulkAdGroupNegativeKeyword()), + 'Campaign Product Scope': _EntityInfo(lambda : BulkCampaignProductScope()), + 'Ad Group Product Partition': _EntityInfo(lambda : BulkAdGroupProductPartition()), + 'Remarketing List': _EntityInfo(lambda : BulkRemarketingList()), + 'Ad Group Remarketing List Association': _EntityInfo(lambda : BulkAdGroupRemarketingListAssociation()), + 'Campaign Negative Dynamic Search Ad Target': _EntityInfo(lambda: BulkCampaignNegativeDynamicSearchAdTarget()), + 'Ad Group Dynamic Search Ad Target': _EntityInfo(lambda: BulkAdGroupDynamicSearchAdTarget()), + 'Ad Group Negative Dynamic Search Ad Target': _EntityInfo(lambda: BulkAdGroupNegativeDynamicSearchAdTarget()), + 'Ad Group Age Criterion': _EntityInfo(lambda: BulkAdGroupAgeCriterion()), + 'Ad Group DayTime Criterion': _EntityInfo(lambda: BulkAdGroupDayTimeCriterion()), + 'Ad Group DeviceOS Criterion': _EntityInfo(lambda: BulkAdGroupDeviceCriterion()), + 'Ad Group Gender Criterion': _EntityInfo(lambda: BulkAdGroupGenderCriterion()), + 'Ad Group Location Criterion': _EntityInfo(lambda: BulkAdGroupLocationCriterion()), + 'Ad Group Location Intent Criterion': _EntityInfo(lambda: BulkAdGroupLocationIntentCriterion()), + 'Ad Group Negative Location Criterion': _EntityInfo(lambda: BulkAdGroupNegativeLocationCriterion()), + 'Ad Group Radius Criterion': _EntityInfo(lambda: BulkAdGroupRadiusCriterion()), + 'Campaign Age Criterion': _EntityInfo(lambda: BulkCampaignAgeCriterion()), + 'Campaign DayTime Criterion': _EntityInfo(lambda: BulkCampaignDayTimeCriterion()), + 'Campaign DeviceOS Criterion': _EntityInfo(lambda: BulkCampaignDeviceCriterion()), + 'Campaign Gender Criterion': _EntityInfo(lambda: BulkCampaignGenderCriterion()), + 'Campaign Location Criterion': _EntityInfo(lambda: BulkCampaignLocationCriterion()), + 'Campaign Location Intent Criterion': _EntityInfo(lambda: BulkCampaignLocationIntentCriterion()), + 'Campaign Negative Location Criterion': _EntityInfo(lambda: BulkCampaignNegativeLocationCriterion()), + 'Campaign Radius Criterion': _EntityInfo(lambda: BulkCampaignRadiusCriterion()), + } + + ADDITIONAL_OBJECT_MAP = { + 'Format Version': lambda: _FormatVersion(), + 'Keyword Best Position Bid': lambda: BulkKeywordBestPositionBid(), + 'Keyword Main Line Bid': lambda: BulkKeywordMainLineBid(), + 'Keyword First Page Bid': lambda: BulkKeywordFirstPageBid(), + } + + TYPE_REVERSE_MAP = {} + TARGET_IDENTIFIER_TYPE_REVERSE_MAP = {} + + @staticmethod + def create_bulk_object(row_values): + type_column = row_values[_StringTable.Type] + + if type_column.endswith('Error'): + return BulkError() + elif type_column in _BulkObjectFactory.ADDITIONAL_OBJECT_MAP: + return _BulkObjectFactory.ADDITIONAL_OBJECT_MAP[type_column]() + elif type_column in _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP: + info = _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP[type_column] + if row_values[_StringTable.Status] == 'Deleted' \ + and info.delete_all_column_name \ + and not row_values[info.delete_all_column_name]: + return info.create_identifier_func() + return info.create_func() + else: + return UnknownBulkEntity() + + @staticmethod + def get_bulk_row_type(bulk_object): + if isinstance(bulk_object, BulkError): + return '{0} Error'.format(_BulkObjectFactory.get_bulk_row_type(bulk_object.entity)) + return _BulkObjectFactory.TYPE_REVERSE_MAP[type(bulk_object)] + + +for (k, v) in _BulkObjectFactory.INDIVIDUAL_ENTITY_MAP.items(): + _BulkObjectFactory.TYPE_REVERSE_MAP[type(v.create_func())] = k + + if v.create_identifier_func is not None: + identifier = v.create_identifier_func() + _BulkObjectFactory.TYPE_REVERSE_MAP[type(identifier)] = k diff --git a/bingads/v11/internal/bulk/csv_headers.py b/bingads/v11/internal/bulk/csv_headers.py index fcb8226d..865483aa 100644 --- a/bingads/v11/internal/bulk/csv_headers.py +++ b/bingads/v11/internal/bulk/csv_headers.py @@ -223,7 +223,7 @@ def get_mappings(): @staticmethod def initialize_map(): - return dict(zip(_CsvHeaders.HEADERS, range(len(_CsvHeaders.HEADERS)))) + return dict(list(zip(_CsvHeaders.HEADERS, list(range(len(_CsvHeaders.HEADERS)))))) _CsvHeaders.COLUMN_INDEX_MAP = _CsvHeaders.initialize_map() diff --git a/bingads/v11/internal/bulk/csv_headers.py.bak b/bingads/v11/internal/bulk/csv_headers.py.bak new file mode 100644 index 00000000..fcb8226d --- /dev/null +++ b/bingads/v11/internal/bulk/csv_headers.py.bak @@ -0,0 +1,229 @@ +from .string_table import _StringTable + + +class _CsvHeaders: + HEADERS = [ + # Common + _StringTable.Type, + _StringTable.Status, + _StringTable.Id, + _StringTable.ParentId, + _StringTable.SubType, + _StringTable.Campaign, + _StringTable.AdGroup, + _StringTable.Website, + _StringTable.SyncTime, + _StringTable.ClientId, + _StringTable.LastModifiedTime, + + # Campaign + _StringTable.TimeZone, + _StringTable.Budget, + _StringTable.BudgetType, + _StringTable.BudgetName, + _StringTable.BudgetId, + _StringTable.KeywordVariantMatchEnabled, + + # AdGroup + _StringTable.StartDate, + _StringTable.EndDate, + _StringTable.NetworkDistribution, + _StringTable.PricingModel, + _StringTable.AdRotation, + _StringTable.SearchNetwork, + _StringTable.SearchBid, + _StringTable.ContentNetwork, + _StringTable.ContentBid, + _StringTable.Language, + + # Ads + _StringTable.Title, + _StringTable.Text, + _StringTable.DisplayUrl, + _StringTable.DestinationUrl, + _StringTable.BusinessName, + _StringTable.PhoneNumber, + _StringTable.PromotionalText, + _StringTable.EditorialStatus, + _StringTable.EditorialLocation, + _StringTable.EditorialTerm, + _StringTable.EditorialReasonCode, + _StringTable.EditorialAppealStatus, + _StringTable.DevicePreference, + + # Keywords + _StringTable.Keyword, + _StringTable.MatchType, + _StringTable.Bid, + _StringTable.Param1, + _StringTable.Param2, + _StringTable.Param3, + + # Location Target + _StringTable.Target, + _StringTable.PhysicalIntent, + _StringTable.TargetAll, + _StringTable.BidAdjustment, + _StringTable.RadiusTargetId, + _StringTable.Name, + _StringTable.OsNames, + _StringTable.Radius, + _StringTable.Unit, + _StringTable.BusinessId, + + # DayTime Target + _StringTable.FromHour, + _StringTable.FromMinute, + _StringTable.ToHour, + _StringTable.ToMinute, + + # AdExtensions common + _StringTable.Version, + + # SiteLink Ad Extensions + _StringTable.SiteLinkExtensionOrder, + _StringTable.SiteLinkDisplayText, + _StringTable.SiteLinkDestinationUrl, + _StringTable.SiteLinkDescription1, + _StringTable.SiteLinkDescription2, + + # Location Ad Extensions + _StringTable.GeoCodeStatus, + _StringTable.IconMediaId, + _StringTable.ImageMediaId, + _StringTable.AddressLine1, + _StringTable.AddressLine2, + _StringTable.PostalCode, + _StringTable.City, + _StringTable.StateOrProvince, + _StringTable.ProvinceName, + _StringTable.Latitude, + _StringTable.Longitude, + + # Call Ad Extensions + _StringTable.CountryCode, + _StringTable.IsCallOnly, + _StringTable.IsCallTrackingEnabled, + _StringTable.RequireTollFreeTrackingNumber, + + # Structured Snippet Ad Extensions + _StringTable.StructuredSnippetHeader, + _StringTable.StructuredSnippetValues, + + # Image Ad Extensions + _StringTable.AltText, + _StringTable.MediaIds, + _StringTable.PublisherCountries, + + # Callout Ad Extension + _StringTable.CalloutText, + + # Product Target + _StringTable.BingMerchantCenterId, + _StringTable.BingMerchantCenterName, + _StringTable.ProductCondition1, + _StringTable.ProductValue1, + _StringTable.ProductCondition2, + _StringTable.ProductValue2, + _StringTable.ProductCondition3, + _StringTable.ProductValue3, + _StringTable.ProductCondition4, + _StringTable.ProductValue4, + _StringTable.ProductCondition5, + _StringTable.ProductValue5, + _StringTable.ProductCondition6, + _StringTable.ProductValue6, + _StringTable.ProductCondition7, + _StringTable.ProductValue7, + _StringTable.ProductCondition8, + _StringTable.ProductValue8, + + # BI + _StringTable.Spend, + _StringTable.Impressions, + _StringTable.Clicks, + _StringTable.CTR, + _StringTable.AvgCPC, + _StringTable.AvgCPM, + _StringTable.AvgPosition, + _StringTable.Conversions, + _StringTable.CPA, + + _StringTable.QualityScore, + _StringTable.KeywordRelevance, + _StringTable.LandingPageRelevance, + _StringTable.LandingPageUserExperience, + + _StringTable.AppPlatform, + _StringTable.AppStoreId, + _StringTable.IsTrackingEnabled, + + _StringTable.Error, + _StringTable.ErrorNumber, + + # Bing Shopping Campaigns + _StringTable.IsExcluded, + _StringTable.ParentAdGroupCriterionId, + _StringTable.CampaignType, + _StringTable.CampaignPriority, + + # V10 added + _StringTable.FieldPath, + + # Upgrade Url + _StringTable.FinalUrl, + _StringTable.FinalMobileUrl, + _StringTable.TrackingTemplate, + _StringTable.CustomParameter, + + # Review Ad Extension + _StringTable.IsExact, + _StringTable.Source, + _StringTable.Url, + + # Bid Strategy + _StringTable.BidStrategyType, + + # Ad Format Preference + _StringTable.AdFormatPreference, + + # Remarketing + _StringTable.Audience, + _StringTable.Description, + _StringTable.MembershipDuration, + _StringTable.Scope, + _StringTable.TagId, + _StringTable.AudienceId, + _StringTable.RemarketingTargetingSetting, + _StringTable.RemarketingRule, + + # Expanded Text Ad + _StringTable.TitlePart1, + _StringTable.TitlePart2, + _StringTable.Path1, + _StringTable.Path2, + + # Ad Scheduling + _StringTable.AdSchedule, + _StringTable.UseSearcherTimeZone, + + # Dynamic Search Ads + _StringTable.DomainLanguage, + _StringTable.DynamicAdTargetCondition1, + _StringTable.DynamicAdTargetValue1, + _StringTable.DynamicAdTargetCondition2, + _StringTable.DynamicAdTargetValue2, + _StringTable.DynamicAdTargetCondition3, + _StringTable.DynamicAdTargetValue3, + ] + + @staticmethod + def get_mappings(): + return _CsvHeaders.COLUMN_INDEX_MAP + + @staticmethod + def initialize_map(): + return dict(zip(_CsvHeaders.HEADERS, range(len(_CsvHeaders.HEADERS)))) + + +_CsvHeaders.COLUMN_INDEX_MAP = _CsvHeaders.initialize_map() diff --git a/bingads/v11/internal/bulk/csv_reader.py b/bingads/v11/internal/bulk/csv_reader.py index 476b2412..4e15b886 100644 --- a/bingads/v11/internal/bulk/csv_reader.py +++ b/bingads/v11/internal/bulk/csv_reader.py @@ -48,17 +48,17 @@ def __iter__(self): return self def __next__(self): - return self.next() + return next(self) def close(self): self.__exit__(None, None, None) - def next(self): + def __next__(self): if PY3: return next(self._csv_reader) elif PY2: row = next(self._csv_reader) - return [unicode(cell, encoding='utf-8') for cell in row] + return [str(cell, encoding='utf-8') for cell in row] @property def filename(self): diff --git a/bingads/v11/internal/bulk/csv_reader.py.bak b/bingads/v11/internal/bulk/csv_reader.py.bak new file mode 100644 index 00000000..476b2412 --- /dev/null +++ b/bingads/v11/internal/bulk/csv_reader.py.bak @@ -0,0 +1,73 @@ +import csv +import io +from six import PY2, PY3 + +import chardet + + +class _CsvReader: + + def __init__(self, filename, delimiter, encoding=None): + self._filename = filename + self._delimiter = delimiter + self._encoding = encoding + + if delimiter == ',': + self._dialect = csv.excel + elif delimiter == '\t': + self._dialect = csv.excel_tab + else: + raise ValueError('Do not support delimiter: {0}', delimiter) + + if self._encoding is None: + self._encoding = self._detected_encoding + + self._csv_file = io.open(self.filename, encoding=self.encoding) + + if PY3: + self._csv_reader = csv.reader(self._csv_file, dialect=self._dialect) + elif PY2: + byte_lines = [line.encode('utf-8') for line in self._csv_file] + self._csv_reader = csv.reader(byte_lines, dialect=self._dialect) + + @property + def _detected_encoding(self): + buffer_size = 1024 * 1024 + with open(self._filename, mode='rb') as bfile: + content = bfile.read(buffer_size) + result = chardet.detect(content) + return result['encoding'] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._csv_file.__exit__(exc_type, exc_value, traceback) + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def close(self): + self.__exit__(None, None, None) + + def next(self): + if PY3: + return next(self._csv_reader) + elif PY2: + row = next(self._csv_reader) + return [unicode(cell, encoding='utf-8') for cell in row] + + @property + def filename(self): + return self._filename + + @property + def delimiter(self): + return self._delimiter + + @property + def encoding(self): + return self._encoding diff --git a/bingads/v11/internal/bulk/csv_writer.py b/bingads/v11/internal/bulk/csv_writer.py index 5d8c4ff4..157de753 100644 --- a/bingads/v11/internal/bulk/csv_writer.py +++ b/bingads/v11/internal/bulk/csv_writer.py @@ -38,7 +38,7 @@ def writerow(self, row): self._csv_writer.writerow(row) elif PY2: def unicode_to_str(value): - if not isinstance(value, unicode): + if not isinstance(value, str): return value return value.encode('utf-8') self._csv_writer.writerow([unicode_to_str(cell) for cell in row]) diff --git a/bingads/v11/internal/bulk/csv_writer.py.bak b/bingads/v11/internal/bulk/csv_writer.py.bak new file mode 100644 index 00000000..5d8c4ff4 --- /dev/null +++ b/bingads/v11/internal/bulk/csv_writer.py.bak @@ -0,0 +1,56 @@ +import csv +import codecs +from six import PY2, PY3 + + +class _CsvWriter: + def __init__(self, filename, delimiter): + self._filename = filename + self._delimiter = delimiter + self._encoding = 'utf-8-sig' + + if delimiter == ',': + self._dialect = csv.excel + elif delimiter == '\t': + self._dialect = csv.excel_tab + else: + raise ValueError('Do not support delimiter: {0}', delimiter) + + if PY3: + self._csv_file = codecs.open(filename, mode='w', encoding=self._encoding) + elif PY2: + self._csv_file = open(filename, mode='wb') + self._csv_file.write(codecs.BOM_UTF8) + self._csv_writer = csv.writer(self._csv_file, dialect=self._dialect) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self._csv_file.flush() + self._csv_file.close() + + def close(self): + self.__exit__(None, None, None) + + def writerow(self, row): + if PY3: + self._csv_writer.writerow(row) + elif PY2: + def unicode_to_str(value): + if not isinstance(value, unicode): + return value + return value.encode('utf-8') + self._csv_writer.writerow([unicode_to_str(cell) for cell in row]) + + def writerows(self, rows): + for row in rows: + self.writerow(row) + + @property + def filename(self): + return self._filename + + @property + def delimiter(self): + return self._delimiter diff --git a/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py b/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py index 0aa24a41..fb069aee 100644 --- a/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py +++ b/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, division, print_function + from abc import ABCMeta, abstractproperty, abstractmethod from future.utils import with_metaclass diff --git a/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py.bak b/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py.bak new file mode 100644 index 00000000..0aa24a41 --- /dev/null +++ b/bingads/v11/internal/bulk/entities/bulk_entity_identifier.py.bak @@ -0,0 +1,35 @@ +from __future__ import absolute_import, division, print_function +from abc import ABCMeta, abstractproperty, abstractmethod + +from future.utils import with_metaclass + +from bingads.v11.bulk.entities import BulkError +from bingads.v11.internal.bulk.bulk_object import _BulkObject + + +class _BulkEntityIdentifier(with_metaclass(ABCMeta, _BulkObject)): + + @abstractproperty + def is_delete_row(self): + raise NotImplementedError() + + @abstractmethod + def _create_entity_with_this_identifier(self): + raise NotImplementedError() + + def write_to_stream(self, row_writer, exclude_readonly_data): + row_writer.write_object_row(self) + + def read_related_data_from_stream(self, stream_reader): + if self.is_delete_row: + has_more_errors = True + + while has_more_errors: + has_more_errors, error = stream_reader.try_read(BulkError) + + @property + def can_enclose_in_multiline_entity(self): + return True + + def enclose_in_multiline_entity(self): + return self._create_entity_with_this_identifier() diff --git a/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py b/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py index 3b3fb95d..ec9bbb6c 100644 --- a/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py +++ b/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py @@ -42,7 +42,7 @@ def has_errors(self): :rtype: bool """ - return any(map(lambda x: x.has_errors, self.child_entities)) + return any([x.has_errors for x in self.child_entities]) @property def last_modified_time(self): diff --git a/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py.bak b/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py.bak new file mode 100644 index 00000000..3b3fb95d --- /dev/null +++ b/bingads/v11/internal/bulk/entities/multi_record_bulk_entity.py.bak @@ -0,0 +1,67 @@ +from abc import ABCMeta, abstractproperty +from future.utils import with_metaclass +from bingads.v11.bulk.entities.bulk_entity import BulkEntity + + +class _MultiRecordBulkEntity(with_metaclass(ABCMeta, BulkEntity)): + """ Bulk entity that has its data in multiple records within the bulk file. + + For example, :class:`.BulkSiteLinkAdExtension` is a multi record bulk entity which can contain one or more + :class:`.BulkSiteLink` child entities, which are themselves derived from :class:`.SingleRecordBulkEntity`. + + For more information, see Bulk File Schema at https://go.microsoft.com/fwlink/?linkid=846127. + """ + + def __init__(self): + super(_MultiRecordBulkEntity, self).__init__() + + @abstractproperty + def child_entities(self): + """ The child entities that this multi record entity contains. + + :rtype: list[BulkEntity] + """ + + raise NotImplementedError() + + @abstractproperty + def all_children_are_present(self): + """ True, if the object is fully constructed (contains all of its children), determined by the presence of delete all row, False otherwise + + :rtype: bool + """ + + raise NotImplementedError() + + @property + def has_errors(self): + """ Indicates whether or not the errors property of any of the ChildEntities is null or empty. + + If true, one or more ChildEntities contains the details of one or more :class:`.BulkError` objects. + + :rtype: bool + """ + + return any(map(lambda x: x.has_errors, self.child_entities)) + + @property + def last_modified_time(self): + """ Gets the last modified time for the first child entity, or null if there are no ChildEntities. + + :rtype: datetime.datetime + """ + + return self.child_entities[0].last_modified_time if self.child_entities else None + + @property + def can_enclose_in_multiline_entity(self): + return super(_MultiRecordBulkEntity, self).can_enclose_in_multiline_entity + + def enclose_in_multiline_entity(self): + return super(_MultiRecordBulkEntity, self).enclose_in_multiline_entity() + + def read_from_row_values(self, row_values): + super(_MultiRecordBulkEntity, self).read_from_row_values(row_values) + + def write_to_row_values(self, row_values): + super(_MultiRecordBulkEntity, self).write_to_row_values(row_values) diff --git a/bingads/v11/internal/bulk/object_reader.py b/bingads/v11/internal/bulk/object_reader.py index 90d5f856..5707902e 100644 --- a/bingads/v11/internal/bulk/object_reader.py +++ b/bingads/v11/internal/bulk/object_reader.py @@ -13,7 +13,7 @@ def __init__(self, file_path, delimiter, encoding=None): self._csv_reader = _CsvReader(self.file_path, delimiter=self.delimiter) self._csv_reader.__enter__() headers = self._read_headers() - self._column_mapping = dict(zip(headers, range(0, len(headers)))) + self._column_mapping = dict(list(zip(headers, list(range(0, len(headers)))))) def __enter__(self): return self @@ -27,7 +27,7 @@ def __iter__(self): def __next__(self): return self.read_next_bulk_object() - def next(self): + def __next__(self): return self.__next__() def read_next_bulk_object(self): @@ -54,7 +54,7 @@ def _read_next_row_values(self): def _read_headers(self): # Need to strip BOM marker by hand, take care def remove_bom(unicode_str): - unicode_bom = u'\N{ZERO WIDTH NO-BREAK SPACE}' + unicode_bom = '\N{ZERO WIDTH NO-BREAK SPACE}' if unicode_str and unicode_str[0] == unicode_bom: unicode_str = unicode_str[1:] return unicode_str diff --git a/bingads/v11/internal/bulk/object_reader.py.bak b/bingads/v11/internal/bulk/object_reader.py.bak new file mode 100644 index 00000000..90d5f856 --- /dev/null +++ b/bingads/v11/internal/bulk/object_reader.py.bak @@ -0,0 +1,75 @@ +from .bulk_object_factory import _BulkObjectFactory +from .row_values import _RowValues +from .csv_reader import _CsvReader + +class _BulkObjectReader(): + """ Provides a method to read one row from bulk file and return the corresponding :class:`._BulkObject` """ + + def __init__(self, file_path, delimiter, encoding=None): + self._file_path = file_path + self._delimiter = delimiter + self._encoding = encoding + + self._csv_reader = _CsvReader(self.file_path, delimiter=self.delimiter) + self._csv_reader.__enter__() + headers = self._read_headers() + self._column_mapping = dict(zip(headers, range(0, len(headers)))) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._csv_reader.__exit__(exc_type, exc_value, traceback) + + def __iter__(self): + return self + + def __next__(self): + return self.read_next_bulk_object() + + def next(self): + return self.__next__() + + def read_next_bulk_object(self): + """ Reads the next csv row values, creates a new instance of the object and populates it with the row values. + + :return: next bulk object + :rtype: _BulkObject + """ + try: + row_values = self._read_next_row_values() + except StopIteration: + return None + bulk_object = _BulkObjectFactory.create_bulk_object(row_values) + bulk_object.read_from_row_values(row_values) + return bulk_object + + def close(self): + self.__exit__(None, None, None) + + def _read_next_row_values(self): + values = next(self._csv_reader) + return _RowValues(columns=values, mappings=self._column_mapping) + + def _read_headers(self): + # Need to strip BOM marker by hand, take care + def remove_bom(unicode_str): + unicode_bom = u'\N{ZERO WIDTH NO-BREAK SPACE}' + if unicode_str and unicode_str[0] == unicode_bom: + unicode_str = unicode_str[1:] + return unicode_str + + headers = next(self._csv_reader) + return [remove_bom(header) for header in headers] + + @property + def file_path(self): + return self._file_path + + @property + def delimiter(self): + return self._delimiter + + @property + def encoding(self): + return self._encoding diff --git a/bingads/v11/internal/bulk/row_values.py b/bingads/v11/internal/bulk/row_values.py index f6db2c60..db59c3f7 100644 --- a/bingads/v11/internal/bulk/row_values.py +++ b/bingads/v11/internal/bulk/row_values.py @@ -26,7 +26,7 @@ def __len__(self): return len(self.mappings) def __str__(self): - return u'{' + u', '.join([u'{0}:{1}'.format(k, self.columns[v]) for (k, v) in self.mappings.items()]) + u'}' + return '{' + ', '.join(['{0}:{1}'.format(k, self.columns[v]) for (k, v) in list(self.mappings.items())]) + '}' def convert_to_entity(self, entity, bulk_mappings): for mapping in bulk_mappings: @@ -49,7 +49,7 @@ def _create_entity_read_exception(self, entity, mapping, ex): message += " See ColumnValues for detailed row information and InnerException for error details." if six.PY2: message = message.decode('ascii') - message += u' row values: {0}'.format(self) + message += ' row values: {0}'.format(self) return EntityReadException(message=message, row_values=str(self), inner_exception=ex) @@ -59,7 +59,7 @@ def try_get_value(self, header): return True, self[header] def to_dict(self): - return dict([(k, self.columns[v]) for (k, v) in self.mappings.items()]) + return dict([(k, self.columns[v]) for (k, v) in list(self.mappings.items())]) @property def mappings(self): diff --git a/bingads/v11/internal/bulk/row_values.py.bak b/bingads/v11/internal/bulk/row_values.py.bak new file mode 100644 index 00000000..f6db2c60 --- /dev/null +++ b/bingads/v11/internal/bulk/row_values.py.bak @@ -0,0 +1,70 @@ +from .csv_headers import _CsvHeaders +from .mappings import _SimpleBulkMapping +from bingads.v11.bulk import EntityReadException +import six + + +class _RowValues: + def __init__(self, mappings=None, columns=None): + self._mappings = mappings + self._columns = columns + if self.mappings is None: + self._mappings = _CsvHeaders.get_mappings() + if self.columns is None: + self._columns = [None] * len(self._mappings) + + def __getitem__(self, key): + return self.columns[self._mappings[key]] + + def __setitem__(self, key, value): + self.columns[self._mappings[key]] = value + + def __contains__(self, item): + return item in self.mappings + + def __len__(self): + return len(self.mappings) + + def __str__(self): + return u'{' + u', '.join([u'{0}:{1}'.format(k, self.columns[v]) for (k, v) in self.mappings.items()]) + u'}' + + def convert_to_entity(self, entity, bulk_mappings): + for mapping in bulk_mappings: + try: + mapping.convert_to_entity(self, entity) + except Exception as ex: + raise self._create_entity_read_exception(entity, mapping, ex) + + def _create_entity_read_exception(self, entity, mapping, ex): + entity_type = str(type(entity)) + + if isinstance(mapping, _SimpleBulkMapping): + message = "Couldn't parse column {0} of {1} entity: {2}".format( + mapping.header, + entity_type, + str(ex) + ) + else: + message = "Couldn't parse {0} entity: {1}".format(entity_type, str(ex)) + message += " See ColumnValues for detailed row information and InnerException for error details." + if six.PY2: + message = message.decode('ascii') + message += u' row values: {0}'.format(self) + + return EntityReadException(message=message, row_values=str(self), inner_exception=ex) + + def try_get_value(self, header): + if header not in self.mappings: + return False, None + return True, self[header] + + def to_dict(self): + return dict([(k, self.columns[v]) for (k, v) in self.mappings.items()]) + + @property + def mappings(self): + return self._mappings + + @property + def columns(self): + return self._columns diff --git a/bingads/v11/internal/bulk/stream_reader.py b/bingads/v11/internal/bulk/stream_reader.py index e893ca69..ed6e525a 100644 --- a/bingads/v11/internal/bulk/stream_reader.py +++ b/bingads/v11/internal/bulk/stream_reader.py @@ -36,7 +36,7 @@ def __next__(self): def close(self): self.__exit__(None, None, None) - def next(self): + def __next__(self): return self.__next__() def read(self): diff --git a/bingads/v11/internal/bulk/stream_reader.py.bak b/bingads/v11/internal/bulk/stream_reader.py.bak new file mode 100644 index 00000000..e893ca69 --- /dev/null +++ b/bingads/v11/internal/bulk/stream_reader.py.bak @@ -0,0 +1,98 @@ +from bingads.v11.bulk import DownloadFileType +from .bulk_object import _BulkObject +from .format_version import _FormatVersion +from .object_reader import _BulkObjectReader +from bingads.internal.error_messages import _ErrorMessages + + +class _BulkStreamReader(): + """ Reads a bulk object and also its related data (for example corresponding errors) from the stream.""" + + __SUPPORTED_VERSIONS = ["5", "5.0"] + + def __init__(self, file_path, file_format, encoding=None): + self._file_path = file_path + self._file_format = file_format + self._encoding = encoding + + self._delimiter = ',' if self.file_format == DownloadFileType.csv else '\t' + self._passed_first_row = False + self._bulk_object_reader = _BulkObjectReader(self.file_path, self.delimiter) + self._bulk_object_reader.__enter__() + self._next_object = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._bulk_object_reader.__exit__(exc_type, exc_val, exc_tb) + + def __iter__(self): + return self + + def __next__(self): + return self.read() + + def close(self): + self.__exit__(None, None, None) + + def next(self): + return self.__next__() + + def read(self): + """ Returns the next object from the file + + :return: next object + :rtype: :class:`._BulkObject` + """ + + _, bulk_object = self.try_read(_BulkObject) + return bulk_object + + def try_read(self, return_type, predicate=lambda x: True): + """ Reads the object only if it has a certain type + + :param return_type: The type of object that should be returned + :type return_type: class + :param predicate: A test that should be run against the object + :type predicate: function accepting on argument of the BulkObject + :return: an object of the type specified + :rtype: (bool, _BulkObject) + """ + peeked = self._peek() + if peeked is not None and isinstance(peeked, return_type) and predicate(peeked): + self._next_object = None + peeked.read_related_data_from_stream(self) + return True, peeked + return False, None + + def _peek(self): + if not self._passed_first_row: + first_row_object = self._bulk_object_reader.read_next_bulk_object() + if isinstance(first_row_object, _FormatVersion): + if first_row_object.value not in _BulkStreamReader.__SUPPORTED_VERSIONS: + raise NotImplementedError( + _ErrorMessages.get_format_version_not_supported_message(str(first_row_object.value))) + else: + self._next_object = first_row_object + self._passed_first_row = True + if self._next_object is not None: + return self._next_object + self._next_object = self._bulk_object_reader.read_next_bulk_object() + return self._next_object + + @property + def file_path(self): + return self._file_path + + @property + def file_format(self): + return self._file_format + + @property + def delimiter(self): + return self._delimiter + + @property + def encoding(self): + return self._encoding diff --git a/bingads/v11/internal/extensions.py b/bingads/v11/internal/extensions.py index f2a71805..73fc1950 100644 --- a/bingads/v11/internal/extensions.py +++ b/bingads/v11/internal/extensions.py @@ -45,7 +45,7 @@ def bulk_str(value): if isinstance(value, str): return value if PY2: - if isinstance(value, unicode): + if isinstance(value, str): return value return str(value) diff --git a/bingads/v11/internal/extensions.py.bak b/bingads/v11/internal/extensions.py.bak new file mode 100644 index 00000000..f2a71805 --- /dev/null +++ b/bingads/v11/internal/extensions.py.bak @@ -0,0 +1,1218 @@ +from datetime import datetime + +from bingads.v11.internal.bulk.string_table import _StringTable +from six import PY2 +import re +from bingads.service_client import _CAMPAIGN_OBJECT_FACTORY_V11, _CAMPAIGN_MANAGEMENT_SERVICE_V11, _CAMPAIGN_OBJECT_FACTORY_V11, _CAMPAIGN_MANAGEMENT_SERVICE_V11 + + +DELETE_VALUE = "delete_value" +_BULK_DATETIME_FORMAT = '%m/%d/%Y %H:%M:%S' +_BULK_DATETIME_FORMAT_2 = '%m/%d/%Y %H:%M:%S.%f' +_BULK_DATE_FORMAT = "%m/%d/%Y" + +url_splitter = ";\\s*(?=https?://)" +custom_param_splitter = "(? 0: + entity.UrlCustomParameters.Parameters.CustomParameter = params + + +def csv_to_field_Urls(entity, value): + """ + set FinalUrls / FinalMobileUrls string field + :param entity: FinalUrls / FinalMobileUrls + :param value: the content in csv + :return:set field values + """ + if value is None or value == '': + return + splitter = re.compile(url_splitter) + entity.string = splitter.split(value) + + +def field_to_csv_Urls(entity): + """ + parse entity to csv content + :param entity: FinalUrls / FinalMobileUrls + :return: csv content + """ + if entity is None: + return None + if entity.string is None: + return DELETE_VALUE + if len(entity.string) == 0: + return None + return '; '.join(entity.string) + + +def field_to_csv_BidStrategyType(entity): + """ + parse entity to csv content + :param entity: entity which has BiddingScheme attribute + :return: csv content + """ + if entity.BiddingScheme is None or type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('BiddingScheme')): + return None + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('EnhancedCpcBiddingScheme')): + return 'EnhancedCpc' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('InheritFromParentBiddingScheme')): + return 'InheritFromParent' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('MaxConversionsBiddingScheme')): + return 'MaxConversions' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('ManualCpcBiddingScheme')): + return 'ManualCpc' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('TargetCpaBiddingScheme')): + return 'TargetCpa' + elif type(entity.BiddingScheme) == type(_CAMPAIGN_OBJECT_FACTORY_V11.create('MaxClicksBiddingScheme')): + return 'MaxClicks' + else: + raise TypeError('Unsupported Bid Strategy Type') + + +def csv_to_field_BidStrategyType(entity, value): + """ + set BiddingScheme + :param entity: entity which has BiddingScheme attribute + :param value: the content in csv + :return: + """ + if value is None or value == '': + return + elif value == 'EnhancedCpc': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V11.create('EnhancedCpcBiddingScheme') + elif value == 'InheritFromParent': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V11.create('InheritFromParentBiddingScheme') + elif value == 'MaxConversions': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V11.create('MaxConversionsBiddingScheme') + elif value == 'ManualCpc': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V11.create('ManualCpcBiddingScheme') + elif value == 'TargetCpa': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V11.create('TargetCpaBiddingScheme') + elif value == 'MaxClicks': + entity.BiddingScheme = _CAMPAIGN_OBJECT_FACTORY_V11.create('MaxClicksBiddingScheme') + else: + raise ValueError('Unknown Bid Strategy Type') + entity.BiddingScheme.Type = value + + +def field_to_csv_AdFormatPreference(entity): + """ + convert entity field to csv content + :param entity: entity which has ForwardCompatibilityMap attribute + :return: + """ + if entity.ForwardCompatibilityMap is None or entity.ForwardCompatibilityMap.KeyValuePairOfstringstring is None \ + or len(entity.ForwardCompatibilityMap.KeyValuePairOfstringstring) == 0: + return None + for key_value_pair in entity.ForwardCompatibilityMap.KeyValuePairOfstringstring: + if key_value_pair.key == 'NativePreference': + if key_value_pair.value.lower() == 'true': + return 'Native' + elif key_value_pair.value.lower() == 'false': + return 'All' + else: + raise ValueError('Unknown value for Native Preference: {0}'.format(key_value_pair.value)) + return None + + +def csv_to_field_AdFormatPreference(entity, value): + """ + parse csv content and set entity attribute + :param entity: entity which has ForwardCompatibilityMap attribute + :param value: csv content value + :return: + """ + ad_format_preference = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns2:KeyValuePairOfstringstring') + ad_format_preference.key = 'NativePreference' + if value is None or value == '' or value == 'All': + ad_format_preference.value = 'False' + elif value == 'Native': + ad_format_preference.value = 'True' + else: + raise ValueError('Unknown value for Native Preference: {0}'.format(value)) + entity.ForwardCompatibilityMap = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns2:ArrayOfKeyValuePairOfstringstring') + entity.ForwardCompatibilityMap.KeyValuePairOfstringstring.append(ad_format_preference) + + +def csv_to_field_StructuredSnippetValues(entity, value): + if value is not None and value != '': + entity.Values.string = value.split(';') + +def field_to_csv_StructuredSnippetValues(entity): + if entity.Values is not None and entity.Values.string is not None and len(entity.Values.string) > 0: + return ';'.join(entity.Values.string) + return None + + +def ad_rotation_bulk_str(value): + if value is None: + return None + elif value.Type is None: + return DELETE_VALUE + else: + return bulk_str(value.Type) + + +def parse_ad_rotation(value): + if not value: + return None + ad_rotation = _CAMPAIGN_OBJECT_FACTORY_V11.create('AdRotation') + ad_rotation.Type = None if value == DELETE_VALUE else value + return ad_rotation + + +def parse_ad_group_bid(value): + if not value: + return None + bid = _CAMPAIGN_OBJECT_FACTORY_V11.create('Bid') + bid.Amount = float(value) + return bid + + +def ad_group_bid_bulk_str(value): + if value is None or value.Amount is None: + return None + return bulk_str(value.Amount) + + +def keyword_bid_bulk_str(value): + if value is None: + return DELETE_VALUE + if value.Amount is None: + return None + return bulk_str(value.Amount) + + +def parse_keyword_bid(value): + bid = _CAMPAIGN_OBJECT_FACTORY_V11.create('Bid') + if not value: + bid.Amount = None + else: + bid.Amount = float(value) + return bid + + +def minute_bulk_str(value): + if value == 'Zero': + return '0' + elif value == 'Fifteen': + return '15' + elif value == 'Thirty': + return '30' + elif value == 'FortyFive': + return '45' + else: + raise ValueError('Unknown minute') + +def parse_fixed_bid(value): + if not value: + return None + fixed_bid = _CAMPAIGN_OBJECT_FACTORY_V11.create('FixedBid') + fixed_bid.Amount = float(value) + return fixed_bid + +def fixed_bid_bulk_str(value): + if value is None or not hasattr(value, 'Amount') or value.Amount is None: + return None + return bulk_str(value.Amount) + +def parse_minute(value): + minute_number = int(value) + if minute_number == 0: + return 'Zero' + elif minute_number == 15: + return 'Fifteen' + elif minute_number == 30: + return 'Thirty' + elif minute_number == 45: + return 'FortyFive' + raise ValueError('Unknown minute') + + +def format_Day(value): + Day = _CAMPAIGN_OBJECT_FACTORY_V11.create('Day') + if value.lower() == 'monday': + return Day.Monday + elif value.lower() == 'tuesday': + return Day.Tuesday + elif value.lower() == 'wednesday': + return Day.Wednesday + elif value.lower() == 'thursday': + return Day.Thursday + elif value.lower() == 'friday': + return Day.Friday + elif value.lower() == 'saturday': + return Day.Saturday + elif value.lower() == 'sunday': + return Day.Sunday + raise ValueError('Unable to parse day: {0}'.format(value)) + +def parse_location_target_type(value): + if value == 'Metro Area': + return 'MetroArea' + elif value == 'Postal Code': + return 'PostalCode' + else: + return value + + +def location_target_type_bulk_str(value): + if value == 'MetroArea': + return 'Metro Area' + elif value == 'PostalCode': + return 'Postal Code' + else: + return value + + +def field_to_csv_AdSchedule(entity): + """ + get the bulk string for Scheduling DayTimeRanges + :param entity: Scheduling entity + :return: bulk str + """ + if entity is None: + return None + if entity.DayTimeRanges is None: + return DELETE_VALUE + return ';'.join('({0}[{1:02d}:{2:02d}-{3:02d}:{4:02d}])' + .format(d.Day, d.StartHour, int(minute_bulk_str(d.StartMinute)), d.EndHour, int(minute_bulk_str(d.EndMinute))) + for d in entity.DayTimeRanges.DayTime + ) + + +def csv_to_field_AdSchedule(entity, value): + if value is None or value.strip() == '': + return + daytime_strs = value.split(';') + ad_schedule_pattern = '\((Monday|Tuesday|Wednesday|ThursDay|Friday|Saturday|Sunday)\[(\d\d?):(\d\d)-(\d\d?):(\d\d)\]\)' + pattern = re.compile(ad_schedule_pattern, re.IGNORECASE) + daytimes = [] + for daytime_str in daytime_strs: + match = pattern.match(daytime_str) + if match: + daytime = _CAMPAIGN_OBJECT_FACTORY_V11.create('DayTime') + daytime.Day = format_Day(match.group(1)) + daytime.StartHour = int(match.group(2)) + daytime.StartMinute = parse_minute(match.group(3)) + daytime.EndHour = int(match.group(4)) + daytime.EndMinute = parse_minute(match.group(5)) + daytimes.append(daytime) + else: + raise ValueError('Unable to parse DayTime: {0}'.format(daytime_str)) + entity.DayTimeRanges.DayTime = daytimes + + +def field_to_csv_SchedulingStartDate(entity): + """ + write scheduling StartDate to bulk string + :param entity: Scheduling entity + :return: date bulk string + """ + if entity is None: + return None + elif entity.StartDate is None: + return DELETE_VALUE + # this case is what the suds creates by default. return None instead of a delete value + elif entity.StartDate.Day is None and entity.StartDate.Month is None and entity.StartDate.Year is None: + return None + return '{0!s}/{1!s}/{2!s}'.format(entity.StartDate.Month, entity.StartDate.Day, entity.StartDate.Year) + + +def field_to_csv_SchedulingEndDate(entity): + """ + write scheduling EndDate to bulk string + :param entity: Scheduling entity + :return: date bulk string + """ + if entity is None: + return None + elif entity.EndDate is None: + return DELETE_VALUE + # this case is what the suds creates by default. return None instead of a delete value + elif entity.EndDate.Day is None and entity.EndDate.Month is None and entity.EndDate.Year is None: + return None + return '{0!s}/{1!s}/{2!s}'.format(entity.EndDate.Month, entity.EndDate.Day, entity.EndDate.Year) + + +def field_to_csv_UseSearcherTimeZone(entity): + """ + get Scheduling UseSearcherTimeZone bulk str + :param entity: Scheduling entity + :return: bulk str + """ + if entity is None: + return None + # this case is what suds creates by default, while set it to delete value since there's no other case for delete value + elif entity.UseSearcherTimeZone is None: + return DELETE_VALUE + else: + return str(entity.UseSearcherTimeZone) + + +def csv_to_field_BudgetType(entity, value, version=11): + if value is None or value == '': + entity.BudgetType = None + elif value == 'MonthlyBudgetSpendUntilDepleted' and version == 11: + entity.BudgetType = BudgetLimitType.MonthlyBudgetSpendUntilDepleted + elif value == 'DailyBudgetAccelerated': + entity.BudgetType = BudgetLimitType.DailyBudgetAccelerated + elif value == 'DailyBudgetStandard': + entity.BudgetType = BudgetLimitType.DailyBudgetStandard + else: + raise ValueError('Unable to parse BudgetType: {0}'.format(value)) + + +def csv_to_field_DSAWebsite(entity, value): + """ + Set Campaign settings Domain Name from bulk value if the campaign type is Dynamic Search Campaign + :param entity: campaign entity + :param value: bulk str value + """ + if not entity.CampaignType or len(entity.CampaignType) == 0 or entity.CampaignType[0] != "DynamicSearchAds": + return + if len(entity.Settings.Setting) > 0 and entity.Settings.Setting[0].Type == 'DynamicSearchAdsSetting': + entity.Settings.Setting[0].DomainName = value + else: + setting = _CAMPAIGN_OBJECT_FACTORY_V11.create('DynamicSearchAdsSetting') + setting.DomainName = value + setting.Type = 'DynamicSearchAdsSetting' + entity.Settings.Setting.append(setting) + + +def field_to_csv_DSAWebsite(entity): + """ + convert campaign settings Domain Name to bulk str if the campaign is Dynamic Search Campaign + :param entity: campaign entity + :return: bulk str + """ + if entity.CampaignType is not None and (entity.CampaignType == 'DynamicSearchAds' or ( + len(entity.CampaignType) != 0 and entity.CampaignType[0] == 'DynamicSearchAds')): + if entity.Settings is None or entity.Settings.Setting is None or len(entity.Settings.Setting) == 0: + return None + setting = entity.Settings.Setting[0] + if isinstance(setting, type(DynamicSearchAdsSetting)): + return setting.DomainName + return None + + +def csv_to_field_DSADomainLanguage(entity, value): + """ + Set Campaign settings Language from bulk value if the campaign type is Dynamic Search Campaign + :param entity: campaign entity + :param value: bulk str value + """ + if not entity.CampaignType or len(entity.CampaignType) == 0 or entity.CampaignType[0] != "DynamicSearchAds": + return + if len(entity.Settings.Setting) > 0 and entity.Settings.Setting[0].Type == 'DynamicSearchAdsSetting': + entity.Settings.Setting[0].Language = value + else: + setting = _CAMPAIGN_OBJECT_FACTORY_V11.create('DynamicSearchAdsSetting') + setting.Language = value + setting.Type = 'DynamicSearchAdsSetting' + entity.Settings.Setting.append(setting) + + +def field_to_csv_DSADomainLanguage(entity): + """ + convert campaign settings Language to bulk str if the campaign is Dynamic Search Campaign + :param entity: campaign entity + :return: bulk str + """ + if entity.CampaignType is not None and (entity.CampaignType == 'DynamicSearchAds' or ( + len(entity.CampaignType) != 0 and entity.CampaignType[0] == 'DynamicSearchAds')): + if not entity.Settings or not entity.Settings.Setting or len(entity.Settings.Setting) == 0: + return None + setting = entity.Settings.Setting[0] + if isinstance(setting, type(DynamicSearchAdsSetting)): + return setting.Language + + return None + + +def field_to_csv_WebpageParameter_CriterionName(entity): + if entity.Criterion is None or entity.Criterion.Parameter is None or entity.Criterion.Parameter.CriterionName is None: + return None + if not entity.Criterion.Parameter.CriterionName: + return DELETE_VALUE + return entity.Criterion.Parameter.CriterionName + + +def csv_to_field_WebpageParameter_CriterionName(entity, value): + if value is None or value == '': + return + if entity.Criterion is not None and isinstance(entity.Criterion, type(Webpage)): + entity.Criterion.Parameter.CriterionName = value + else: + webpage = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:Webpage') + webpage.Parameter.CriterionName = value + entity.Criterion = webpage + + +def entity_to_csv_DSAWebpageParameter(entity, row_values): + """ + Set Campaign/AdGroup Criterion (WebpagePage) Web page parameters from bulk values + :param entity: campaign/ad group criterion entity + :param row_values: bulk row values + """ + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion, type(Webpage)) and \ + entity.Criterion.Parameter is not None and entity.Criterion.Parameter.Conditions is not None and \ + entity.Criterion.Parameter.Conditions.WebpageCondition is not None: + condition_prefix = _StringTable.DynamicAdTargetCondition1[:-1] + value_prefix = _StringTable.DynamicAdTargetValue1[:-1] + + conditions = entity.Criterion.Parameter.Conditions.WebpageCondition + for i in range(0, len(conditions)): + row_values[condition_prefix + str(i + 1)] = conditions[i].Operand + row_values[value_prefix + str(i + 1)] = conditions[i].Argument + + +def csv_to_entity_DSAWebpageParameter(row_values, entity): + """ + convert Campaign/Ad Group Criterion (WebpagePage) Web page parameters to bulk row values + :param row_values: bulk row values + :param entity: campaign/ad group criterion entity + """ + MAX_NUMBER_OF_CONDITIONS = 3 + condition_prefix = _StringTable.DynamicAdTargetCondition1[:-1] + value_prefix = _StringTable.DynamicAdTargetValue1[:-1] + + conditions = [] + for i in range(0, MAX_NUMBER_OF_CONDITIONS): + condition_success, webpage_condition = row_values.try_get_value(condition_prefix + str(i + 1)) + value_success, webpage_value = row_values.try_get_value(value_prefix + str(i + 1)) + if condition_success and value_success and webpage_condition is not None and webpage_condition != '': + condition = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:WebpageCondition') + if webpage_condition.lower() == 'url': + condition.Operand = WebpageConditionOperand.Url + elif webpage_condition.lower() == "category": + condition.Operand = WebpageConditionOperand.Category + elif webpage_condition.lower() == 'pagetitle': + condition.Operand = WebpageConditionOperand.PageTitle + elif webpage_condition.lower() == 'pagecontent': + condition.Operand = WebpageConditionOperand.PageContent + else: + # TODO wait bug 54825 to be fixed + if webpage_condition.lower() == 'none': + continue + raise ValueError("Unknown WebpageConditionOperand value: {0}".format(webpage_condition)) + + condition.Argument = webpage_value + conditions.append(condition) + + if len(conditions) > 0: + if entity.Criterion is not None and isinstance(entity.Criterion, type(Webpage)): + entity.Criterion.Parameter.Conditions.WebpageCondition = conditions + else: + webpage = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:Webpage') + webpage.Parameter.Conditions.WebpageCondition = conditions + entity.Criterion = webpage + + +def parse_bool(value): + if value is None or value == '': + return None + elif value.lower() == 'true': + return True + elif value.lower() == 'false': + return False + else: + raise ValueError('Unable to parse bool value: {0}.'.format(value)) + + +def field_to_csv_RemarketingRule(entity): + """ + convert remarketing rule to bulk string + :param entity: remarketing list entity + """ + if entity.Rule == None: + return None + + rule = entity.Rule + if (isinstance(rule, type(PageVisitorsRule))): + return 'PageVisitors{0}'.format(rule_item_groups_str(rule.RuleItemGroups.RuleItemGroup)) + elif (isinstance(rule, type(PageVisitorsWhoVisitedAnotherPageRule))): + return 'PageVisitorsWhoVisitedAnotherPage({0}) and ({1})'.format( + rule_item_groups_str(rule.RuleItemGroups.RuleItemGroup), + rule_item_groups_str(rule.AnotherRuleItemGroups.RuleItemGroup)) + elif (isinstance(rule, type(PageVisitorsWhoDidNotVisitAnotherPageRule))): + return 'PageVisitorsWhoDidNotVisitAnotherPage({0}) and not ({1})'.format( + rule_item_groups_str(rule.IncludeRuleItemGroups.RuleItemGroup), + rule_item_groups_str(rule.ExcludeRuleItemGroups.RuleItemGroup)) + elif (isinstance(rule, type(CustomEventsRule))): + return 'CustomEvents{0}'.format(custom_event_rule_str(rule)) + elif (isinstance(rule, type(RemarketingRule))): + return None + else: + raise ValueError('Unsupported Remarketing Rule type: {0}'.format(type(entity.RemarketingRule))) + + +def rule_item_groups_str(groups): + if groups is None or len(groups) == 0: + raise ValueError('Remarketing RuleItemGroups is None or empty.') + + return ' or '.join(['({0})'.format(rule_items_str(group.Items.RuleItem)) for group in groups]) + + +def rule_items_str(items): + if items is None or len(items) == 0: + raise ValueError('Remarketing RuleItem list is None or empty.') + + return ' and '.join(['({0} {1} {2})'.format(item.Operand, item.Operator, item.Value) for item in items]) + + +def custom_event_rule_str(rule): + rule_items = [] + if rule.ActionOperator is not None and rule.Action is not None: + rule_items.append('Action {0} {1}'.format(rule.ActionOperator, rule.Action)) + if rule.CategoryOperator is not None and rule.Category is not None: + rule_items.append('Category {0} {1}'.format(rule.CategoryOperator, rule.Category)) + if rule.LabelOperator is not None and rule.Label is not None: + rule_items.append('Label {0} {1}'.format(rule.LabelOperator, rule.Label)) + if rule.ValueOperator is not None and rule.Value is not None: + rule_items.append('Value {0} {1}'.format(rule.ValueOperator, rule.Value)) + + if len(rule_items) == 0: + raise ValueError('Remarketing CustomEvents RuleItem list is empty') + + return ' and '.join('({0})'.format(item) for item in rule_items) + + +def csv_to_field_RemarketingRule(entity, value): + """ + parse remarketing rule string and set remarketing rule attribute value + :param entity: remarketing list entity + :param value: bulk string value + """ + if value is None or value == '': + return + + type_end_pos = value.index('(') + if type_end_pos <= 0: + raise ValueError('Invalid Remarketing Rule: {0}'.format(value)) + + rule_type = value[:type_end_pos] + rule = value[type_end_pos:] + + if rule_type.lower() == 'pagevisitors': + entity.Rule = parse_rule_PageVisitors(rule) + elif rule_type.lower() == 'pagevisitorswhovisitedanotherpage': + entity.Rule = parse_rule_PageVisitorsWhoVisitedAnotherPage(rule) + elif rule_type.lower() == 'pagevisitorswhodidnotvisitanotherpage': + entity.Rule = parse_rule_PageVisitorsWhoDidNotVisitAnotherPage(rule) + elif rule_type.lower() == 'customevents': + entity.Rule = parse_rule_CustomEvents(rule) + else: + raise ValueError('Invalid Remarketing Rule Type: {0}'.format(rule_type)) + + +def field_to_csv_CriterionAudienceId(entity): + if entity is None or entity.Criterion is None or entity.Criterion.AudienceId is None: + return None + return bulk_str(entity.Criterion.AudienceId) + + +def csv_to_field_CriterionAudienceId(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion, type(AudienceCriterion)): + entity.Criterion.AudienceId = value + + +def field_to_csv_BidAdjustment(entity): + if entity is None or entity.CriterionBid is None or entity.CriterionBid.Multiplier is None: + return None + return bulk_str(entity.CriterionBid.Multiplier) + + +def csv_to_field_BidAdjustment(entity, value): + if value is None or value == '': + return + if entity is not None and entity.CriterionBid is not None and isinstance(entity.CriterionBid, type(BidMultiplier)): + entity.CriterionBid.Multiplier = value + +def field_to_csv_AgeTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.AgeRange is None: + return None + return entity.Criterion.AgeRange + +def csv_to_field_AgeTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(AgeCriterion)): + setattr(entity.Criterion, "AgeRange", value) + +def field_to_csv_DayTimeTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.Day is None: + return None + return entity.Criterion.Day + +def csv_to_field_DayTimeTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "Day", value) + +def field_to_csv_FromHour(entity): + if entity is None or entity.Criterion is None or entity.Criterion.FromHour is None: + return None + return str(entity.Criterion.FromHour) + +def csv_to_field_FromHour(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "FromHour", value) + +def field_to_csv_FromMinute(entity): + if entity is None or entity.Criterion is None or entity.Criterion.FromMinute is None: + return None + return minute_bulk_str(entity.Criterion.FromMinute) + +def csv_to_field_FromMinute(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "FromMinute", parse_minute(value)) + +def field_to_csv_ToHour(entity): + if entity is None or entity.Criterion is None or entity.Criterion.ToHour is None: + return None + return str(entity.Criterion.ToHour) + +def csv_to_field_ToHour(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "ToHour", value) + +def field_to_csv_ToMinute(entity): + if entity is None or entity.Criterion is None or entity.Criterion.ToMinute is None: + return None + return minute_bulk_str(entity.Criterion.ToMinute) + +def csv_to_field_ToMinute(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DayTimeCriterion)): + setattr(entity.Criterion, "ToMinute", parse_minute(value)) + +def field_to_csv_DeviceTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.DeviceName is None: + return None + return entity.Criterion.DeviceName + +def csv_to_field_DeviceTarget(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DeviceCriterion)): + setattr(entity.Criterion, "DeviceName", value) + +def field_to_csv_OSName(entity): + if entity is None or entity.Criterion is None or entity.Criterion.OSName is None: + return None + return entity.Criterion.OSName + +def csv_to_field_OSName(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(DeviceCriterion)): + setattr(entity.Criterion, "OSName", value) + +def field_to_csv_GenderTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.GenderType is None: + return None + return entity.Criterion.GenderType + +def csv_to_field_GenderTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(GenderCriterion)): + setattr(entity.Criterion, "GenderType", value) + +def field_to_csv_LocationTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LocationId is None: + return None + return str(entity.Criterion.LocationId) + +def csv_to_field_LocationTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationCriterion)): + setattr(entity.Criterion, "LocationId", value) + +def field_to_csv_LocationType(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LocationType is None: + return None + return entity.Criterion.LocationType + +def csv_to_field_LocationType(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationCriterion)): + setattr(entity.Criterion, "LocationType", value) + +def field_to_csv_LocationName(entity): + if entity is None or entity.Criterion is None or entity.Criterion.DisplayName is None: + return None + return entity.Criterion.DisplayName + +def csv_to_field_LocationName(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationCriterion)): + setattr(entity.Criterion, "DisplayName", value) + +def field_to_csv_LocationIntentTarget(entity): + if entity is None or entity.Criterion is None or entity.Criterion.IntentOption is None: + return None + return entity.Criterion.IntentOption + +def csv_to_field_LocationIntentTarget(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(LocationIntentCriterion)): + setattr(entity.Criterion, "IntentOption", value) + +def field_to_csv_RadiusName(entity): + if entity is None or entity.Criterion is None or entity.Criterion.Name is None: + return None + return entity.Criterion.Name + +def csv_to_field_RadiusName(entity, value): + if value is None: + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "Name", value) + +def field_to_csv_Radius(entity): + if entity is None or entity.Criterion is None or entity.Criterion.Radius is None: + return None + return str(entity.Criterion.Radius) + +def csv_to_field_Radius(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "Radius", value) + +def field_to_csv_RadiusUnit(entity): + if entity is None or entity.Criterion is None or entity.Criterion.RadiusUnit is None: + return None + return entity.Criterion.RadiusUnit + +def csv_to_field_RadiusUnit(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "RadiusUnit", value) + +def field_to_csv_LatitudeDegrees(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LatitudeDegrees is None: + return None + return str(entity.Criterion.LatitudeDegrees) + +def csv_to_field_LatitudeDegrees(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "LatitudeDegrees", value) + +def field_to_csv_LongitudeDegrees(entity): + if entity is None or entity.Criterion is None or entity.Criterion.LongitudeDegrees is None: + return None + return str(entity.Criterion.LongitudeDegrees) + +def csv_to_field_LongitudeDegrees(entity, value): + if value is None or value == '': + return + if entity is not None and entity.Criterion is not None and isinstance(entity.Criterion,type(RadiusCriterion)): + setattr(entity.Criterion, "LongitudeDegrees", value) + +def parse_rule_PageVisitors(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:PageVisitorsRule') + rule.Type = 'PageVisitors' + rule.RuleItemGroups = parse_rule_groups(rule_str) + return rule + + +def parse_rule_PageVisitorsWhoVisitedAnotherPage(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:PageVisitorsWhoVisitedAnotherPageRule') + rule.Type = 'PageVisitorsWhoVisitedAnotherPage' + + groups_split = '))) and (((' + groups_string_list = rule_str.split(groups_split) + + rule.RuleItemGroups = parse_rule_groups(groups_string_list[0]) + rule.AnotherRuleItemGroups = parse_rule_groups(groups_string_list[1]) + + return rule + + +def parse_rule_PageVisitorsWhoDidNotVisitAnotherPage(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:PageVisitorsWhoDidNotVisitAnotherPageRule') + rule.Type = 'PageVisitorsWhoDidNotVisitAnotherPage' + + groups_split = '))) and not (((' + groups_string_list = rule_str.split(groups_split) + + rule.IncludeRuleItemGroups = parse_rule_groups(groups_string_list[0]) + rule.ExcludeRuleItemGroups = parse_rule_groups(groups_string_list[1]) + + return rule + + +def parse_rule_CustomEvents(rule_str): + rule = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:CustomEventsRule') + rule.Type = 'CustomEvents' + + item_split = ') and (' + pattern_for_operand_str = '^(Category|Action|Label|Value) ([^()]*)$' + pattern_for_operand = re.compile(pattern_for_operand_str) + + pattern_number_item_str = '^(Equals|GreaterThan|LessThan|GreaterThanEqualTo|LessThanEqualTo) ([^()]*)$' + pattern_number_item = re.compile(pattern_number_item_str) + + pattern_string_item_str = '^(Equals|Contains|BeginsWith|EndsWith|NotEquals|DoesNotContain|DoesNotBeginWith|DoesNotEndWith) ([^()]*)$' + pattern_string_item = re.compile(pattern_string_item_str) + + item_string_list = rule_str.split(item_split) + for item_string in item_string_list: + item_string = item_string.strip('(').strip(')') + match_for_operand = pattern_for_operand.match(item_string) + + if not match_for_operand: + raise ValueError('Invalid Custom Event rule item: {0}'.format(item_string)) + + operand = match_for_operand.group(1) + operater_and_value_string = match_for_operand.group(2) + + if operand.lower() == 'value': + match_number_item = pattern_number_item.match(operater_and_value_string) + + if not match_number_item: + raise ValueError('Invalid Custom Event number rule item: {0}'.format(item_string)) + + rule.ValueOperator = parse_number_operator(match_number_item.group(1)) + rule.Value = float(match_number_item.group(2)) + else: + match_string_item = pattern_string_item.match(operater_and_value_string) + + if not match_string_item: + raise ValueError('Invalid Custom Event string rule item: {0}'.format(item_string)) + + if operand.lower() == 'category': + rule.CategoryOperator = parse_string_operator(match_string_item.group(1)) + rule.Category = match_string_item.group(2) + elif operand.lower() == 'label': + rule.LabelOperator = parse_string_operator(match_string_item.group(1)) + rule.Label = match_string_item.group(2) + elif operand.lower() == 'action': + rule.ActionOperator = parse_string_operator(match_string_item.group(1)) + rule.Action = match_string_item.group(2) + else: + raise ValueError('Invalid Custom Event string rule operator: {0}'.format(operand)) + + return rule + + +def parse_rule_groups(groups_str): + group_split = ')) or ((' + group_str_list = groups_str.split(group_split) + + rule_item_groups = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:ArrayOfRuleItemGroup') + for group_str in group_str_list: + item_group = parse_rule_items(group_str) + rule_item_groups.RuleItemGroup.append(item_group) + + return rule_item_groups + + +def parse_rule_items(items_str): + item_split = ') and (' + item_str_list = items_str.split(item_split) + + rule_item_group = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:RuleItemGroup') + for item_str in item_str_list: + item = parse_string_rule_item(item_str) + rule_item_group.Items.RuleItem.append(item) + + return rule_item_group + + +def parse_string_rule_item(item_str): + item_str = item_str.strip('(').strip(')') + pattern_str = '^(Url|ReferrerUrl|None) (Equals|Contains|BeginsWith|EndsWith|NotEquals|DoesNotContain|DoesNotBeginWith|DoesNotEndWith) ([^()]*)$' + pattern = re.compile(pattern_str) + + match = pattern.match(item_str) + + if not match: + ValueError('Invalid Rule Item:{0}'.format(item_str)) + + item = _CAMPAIGN_OBJECT_FACTORY_V11.create('ns0:StringRuleItem') + item.Type = 'String' + item.Operand = match.group(1) + item.Operator = parse_string_operator(match.group(2)) + item.Value = match.group(3) + + return item + + +def parse_number_operator(operator): + oper = operator.lower() + if oper == 'equals': + return NumberOperator.Equals + if oper == 'greaterthan': + return NumberOperator.GreaterThan + if oper == 'lessthan': + return NumberOperator.LessThan + if oper == 'greaterthanequalto': + return NumberOperator.GreaterThanEqualTo + if oper == 'lessthanequalto': + return NumberOperator.LessThanEqualTo + raise ValueError('Invalid Number Rule Item operator:{0}'.format(operator)) + + +def parse_string_operator(operator): + oper = operator.lower() + if oper == 'equals': + return StringOperator.Equals + if oper == 'contains': + return StringOperator.Contains + if oper == 'beginswith': + return StringOperator.BeginsWith + if oper == 'endswith': + return StringOperator.EndsWith + if oper == 'notequals': + return StringOperator.NotEquals + if oper == 'doesnotcontain': + return StringOperator.DoesNotContain + if oper == 'doesnotbeginwith': + return StringOperator.DoesNotBeginWith + if oper == 'doesnotendwith': + return StringOperator.DoesNotEndWith + + raise ValueError('Invalid String Rule Item operator:{0}'.format(operator)) diff --git a/docs/conf.py b/docs/conf.py index 5923e6df..40807c05 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. sys.path.insert(0, os.path.abspath('../bingads')) -print(sys.path) +print((sys.path)) # Get the project root dir, which is the parent dir of this cwd = os.getcwd() @@ -56,8 +56,8 @@ master_doc = 'index' # General information about the project. -project = u'Bing Ads SDK' -copyright = u'2016, Bing Ads SDK Team' +project = 'Bing Ads SDK' +copyright = '2016, Bing Ads SDK Team' # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -210,8 +210,8 @@ # [howto/manual]). latex_documents = [ ('index', 'bingads.tex', - u'Bing Ads SDK Documentation', - u'Bing Ads SDK Team', 'manual'), + 'Bing Ads SDK Documentation', + 'Bing Ads SDK Team', 'manual'), ] # The name of an image file (relative to this directory) to place at @@ -241,8 +241,8 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'bingads', - u'Bing Ads SDK Documentation', - [u'Bing Ads SDK Team'], 1) + 'Bing Ads SDK Documentation', + ['Bing Ads SDK Team'], 1) ] # If true, show URL addresses after external links. @@ -256,8 +256,8 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'bingads', - u'Bing Ads SDK Documentation', - u'Bing Ads SDK Team', + 'Bing Ads SDK Documentation', + 'Bing Ads SDK Team', 'bingads', 'One line description of project.', 'Miscellaneous'), diff --git a/docs/conf.py.bak b/docs/conf.py.bak new file mode 100644 index 00000000..5923e6df --- /dev/null +++ b/docs/conf.py.bak @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# bingads documentation build configuration file, created by +# sphinx-quickstart. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another +# directory, add these directories to sys.path here. If the directory is +# relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +sys.path.insert(0, os.path.abspath('../bingads')) +print(sys.path) + +# Get the project root dir, which is the parent dir of this +cwd = os.getcwd() +project_root = os.path.dirname(cwd) + +# Insert the project root dir as the first element in the PYTHONPATH. +# This lets us ensure that the source package is imported, and that its +# version is used. +sys.path.insert(0, project_root) + +import bingads + +# -- General configuration --------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Bing Ads SDK' +copyright = u'2016, Bing Ads SDK Team' + +# The version info for the project you're documenting, acts as replacement +# for |version| and |release|, also used in various other places throughout +# the built documents. +# +# The short X.Y version. +version = bingads.__version__ +# The full version, including alpha/beta/rc tags. +release = bingads.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to +# some non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built +# documents. +#keep_warnings = False + + +# -- Options for HTML output ------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a +# theme further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as +# html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the +# top of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon +# of the docs. This file should be a Windows icon file (.ico) being +# 16x16 or 32x32 pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) +# here, relative to this directory. They are copied after the builtin +# static files, so a file named "default.css" will overwrite the builtin +# "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names +# to template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. +# Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. +# Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages +# will contain a tag referring to it. The value of this option +# must be the base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'bingads_python_doc' + + +# -- Options for LaTeX output ------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + #'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', 'bingads.tex', + u'Bing Ads SDK Documentation', + u'Bing Ads SDK Team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at +# the top of the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings +# are parts, not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output ------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'bingads', + u'Bing Ads SDK Documentation', + [u'Bing Ads SDK Team'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ---------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'bingads', + u'Bing Ads SDK Documentation', + u'Bing Ads SDK Team', + 'bingads', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py index dc08901c..43f764b7 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py @@ -101,13 +101,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py.bak new file mode 100644 index 00000000..dc08901c --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/AdExtensions.py.bak @@ -0,0 +1,1043 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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) +#logging.getLogger('suds.transport.http').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' + + SITELINK_MIGRATION = 'SiteLinkAdExtension' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_campaign_ids(campaign_ids): + for id in campaign_ids['long']: + output_status_message("Campaign successfully added and assigned CampaignId {0}\n".format(id)) + +# Set the read-only properties of an ad extension to null. This operation can be useful between calls to +# GetAdExtensionsByIds and UpdateAdExtensions. The update operation would fail if you send certain read-only +# fields. +def set_read_only_ad_extension_elements_to_none(extension): + if extension is None or extension.Id is None: + return extension + else: + # Set to None for all extension types. + extension.Version = None + + if extension.Type == 'LocationAdExtension': + extension.GeoCodeStatus = None + + return extension + + +def output_ad_extensions(ad_extensions, ad_extension_editorial_reason_collection): + if not hasattr(ad_extensions, 'AdExtension'): + return None + index=0 + for extension in ad_extensions['AdExtension']: + if extension is None or extension.Id is None: + output_status_message('Extension is empty or invalid.') + else: + if extension.Type == 'AppAdExtension': + output_app_ad_extension(extension) + elif extension.Type == 'CallAdExtension': + output_call_ad_extension(extension) + elif extension.Type == 'CalloutAdExtension': + output_callout_ad_extension(extension) + elif extension.Type == 'ImageAdExtension': + output_image_ad_extension(extension) + elif extension.Type == 'LocationAdExtension': + output_location_ad_extension(extension) + elif extension.Type == 'ReviewAdExtension': + output_review_ad_extension(extension) + elif extension.Type == 'SiteLinksAdExtension': + output_site_links_ad_extension(extension) + elif extension.Type == 'Sitelink2AdExtension': + output_sitelink2_ad_extension(extension) + elif extension.Type == 'StructuredSnippetAdExtension': + output_structured_snippet_ad_extension(extension) + else: + output_status_message("Unknown extension type") + + if hasattr(ad_extension_editorial_reason_collection, 'Reasons'): + + # Print any editorial rejection reasons for the corresponding extension. This example + # assumes the same list index for adExtensions and adExtensionEditorialReasonCollection + # as defined above. + + for ad_extension_editorial_reason \ + in ad_extension_editorial_reason_collection.Reasons['AdExtensionEditorialReason']: + + if ad_extension_editorial_reason is not None \ + and ad_extension_editorial_reason.PublisherCountries is not None: + + output_status_message("Editorial Rejection Location: {0}".format(ad_extension_editorial_reason.Location)) + output_status_message("Editorial Rejection PublisherCountries: ") + for publisher_country in ad_extension_editorial_reason.PublisherCountries['string']: + output_status_message(" " + publisher_country) + + output_status_message("Editorial Rejection ReasonCode: {0}".format(ad_extension_editorial_reason.ReasonCode)) + output_status_message("Editorial Rejection Term: {0}".format(ad_extension_editorial_reason.Term)) + + index=index+1 + + output_status_message("\n") + +def output_ad_extension(extension): + if extension is not None: + output_status_message("Id: {0}".format(extension.Id)) + output_status_message("Type: {0}".format(extension.Type)) + output_status_message("ForwardCompatibilityMap: ") + if extension.ForwardCompatibilityMap is not None: + for pair in extension.ForwardCompatibilityMap['KeyValuePairOfstringstring']: + output_status_message("Key: {0}".format(pair.Key)) + output_status_message("Value: {0}".format(pair.Value)) + output_status_message("Scheduling: ") + # Scheduling is not emitted by default, so we must first test whether it exists. + if hasattr(extension, 'Scheduling') and extension.Scheduling is not None: + output_schedule(extension.Scheduling) + output_status_message("Status: {0}".format(extension.Status)) + output_status_message("Version: {0}".format(extension.Version)) + +def output_schedule(schedule): + if schedule is not None: + if schedule.DayTimeRanges is not None: + for day_time in schedule.DayTimeRanges['DayTime']: + output_status_message("Day: {0}".format(day_time.Day)) + output_status_message("EndHour: {0}".format(day_time.EndHour)) + output_status_message("EndMinute: {0}".format(day_time.EndMinute)) + output_status_message("StartHour: {0}".format(day_time.StartHour)) + output_status_message("StartMinute: {0}".format(day_time.StartMinute)) + if schedule.EndDate is not None: + output_status_message(("EndDate: {0}/{1}/{2}".format( + schedule.EndDate.Month, + schedule.EndDate.Day, + schedule.EndDate.Year))) + if schedule.StartDate is not None: + output_status_message(("StartDate: {0}/{1}/{2}".format( + schedule.StartDate.Month, + schedule.StartDate.Day, + schedule.StartDate.Year))) + use_searcher_time_zone = \ + True if (schedule.UseSearcherTimeZone is not None and schedule.UseSearcherTimeZone == True) else False + output_status_message("UseSearcherTimeZone: {0}".format(use_searcher_time_zone)) + +def output_app_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the AppAdExtension + output_status_message("AppPlatform: {0}".format(extension.AppPlatform)) + output_status_message("AppStoreId: {0}".format(extension.AppStoreId)) + output_status_message("DestinationUrl: {0}".format(extension.DestinationUrl)) + output_status_message("DevicePreference: {0}".format(extension.DevicePreference)) + output_status_message("DisplayText: {0}".format(extension.DisplayText)) + +def output_call_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the CallAdExtension + output_status_message("CountryCode: {0}".format(extension.CountryCode)) + output_status_message("DevicePreference: {0}".format(extension.DevicePreference)) + output_status_message("IsCallOnly: {0}".format(extension.IsCallOnly)) + output_status_message("IsCallTrackingEnabled: {0}".format(extension.IsCallTrackingEnabled)) + output_status_message("PhoneNumber: {0}".format(extension.PhoneNumber)) + output_status_message("RequireTollFreeTrackingNumber: {0}".format(extension.RequireTollFreeTrackingNumber)) + +def output_callout_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the CalloutAdExtension + output_status_message("Callout Text: {0}".format(extension.Text)) + +def output_image_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the ImageAdExtension + output_status_message("AlternativeText: {0}".format(extension.AlternativeText)) + output_status_message("Description: {0}".format(extension.Description)) + output_status_message("DestinationUrl: {0}".format(extension.DestinationUrl)) + output_status_message("FinalMobileUrls: ") + if extension.FinalMobileUrls is not None: + for final_mobile_url in extension.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if extension.FinalUrls is not None: + for final_url in extension.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("ImageMediaIds: ") + if extension.ImageMediaIds is not None: + for id in extension.ImageMediaIds['string']: + output_status_message("\t{0}".format(id)) + output_status_message("TrackingUrlTemplate: {0}".format(extension.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if extension.UrlCustomParameters is not None and extension.UrlCustomParameters.Parameters is not None: + for custom_parameter in extension.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_location_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the LocationAdExtension + if extension.Address is not None: + output_status_message("CityName: {0}".format(extension.Address.CityName)) + output_status_message("CountryCode: {0}".format(extension.Address.CountryCode)) + output_status_message("PostalCode: {0}".format(extension.Address.PostalCode)) + output_status_message("ProvinceCode: {0}".format(extension.Address.ProvinceCode)) + output_status_message("ProvinceName: {0}".format(extension.Address.ProvinceName)) + output_status_message("StreetAddress: {0}".format(extension.Address.StreetAddress)) + output_status_message("StreetAddress2: {0}".format(extension.Address.StreetAddress2)) + output_status_message("CompanyName: {0}".format(extension.CompanyName)) + output_status_message("GeoCodeStatus: {0}".format(extension.GeoCodeStatus)) + if extension.GeoPoint is not None: + output_status_message("GeoPoint: ") + output_status_message("LatitudeInMicroDegrees: {0}".format(extension.GeoPoint.LatitudeInMicroDegrees)) + output_status_message("LongitudeInMicroDegrees: {0}".format(extension.GeoPoint.LongitudeInMicroDegrees)) + output_status_message("IconMediaId: {0}".format(extension.IconMediaId)) + output_status_message("ImageMediaId: {0}".format(extension.ImageMediaId)) + output_status_message("PhoneNumber: {0}".format(extension.PhoneNumber)) + +def output_review_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the ReviewAdExtension + output_status_message("IsExact: {0}".format(extension.IsExact)) + output_status_message("Source: {0}".format(extension.Source)) + output_status_message("Text: {0}".format(extension.Text)) + output_status_message("Url: {0}".format(extension.Url)) + +def output_structured_snippet_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the StructuredSnippetAdExtension + output_status_message("Header: {0}".format(extension.Header)) + for value in extension.Values['string']: + output_status_message("\t{0}".format(value)) + +def output_site_links_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the SiteLinksAdExtension + output_site_links(extension.SiteLinks['SiteLink']) + +def output_site_links(site_links): + if site_links is not None: + for site_link in site_links: + output_status_message("Description1: {0}".format(site_link.Description1)) + output_status_message("Description2: {0}".format(site_link.Description2)) + output_status_message("DevicePreference: {0}".format(site_link.DevicePreference)) + output_status_message("DisplayText: {0}".format(site_link.DisplayText)) + output_status_message("DestinationUrl: {0}".format(site_link.DestinationUrl)) + output_status_message("FinalMobileUrls: ") + if site_link.FinalMobileUrls is not None: + for final_mobile_url in site_link.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if site_link.FinalUrls is not None: + for final_url in site_link.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("TrackingUrlTemplate: {0}".format(site_link.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if site_link.UrlCustomParameters is not None and site_link.UrlCustomParameters.Parameters is not None: + for custom_parameter in site_link.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_sitelink2_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the Sitelink2AdExtension + output_status_message("Description1: {0}".format(extension.Description1)) + output_status_message("Description2: {0}".format(extension.Description2)) + output_status_message("DevicePreference: {0}".format(extension.DevicePreference)) + output_status_message("DisplayText: {0}".format(extension.DisplayText)) + output_status_message("DestinationUrl: {0}".format(extension.DestinationUrl)) + output_status_message("FinalMobileUrls: ") + if extension.FinalMobileUrls is not None: + for final_mobile_url in extension.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if extension.FinalUrls is not None: + for final_url in extension.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("TrackingUrlTemplate: {0}".format(extension.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if extension.UrlCustomParameters is not None and extension.UrlCustomParameters.Parameters is not None: + for custom_parameter in extension.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_account_migration_statuses_info(account_migration_statuses_info): + if account_migration_statuses_info is not None: + output_status_message("AccountId: {0}".format(account_migration_statuses_info.AccountId)) + for migration_status_info in account_migration_statuses_info['MigrationStatusInfo']: + output_migration_status_info(migration_status_info) + +def output_migration_status_info(migration_status_info): + if migration_status_info is not None and migration_status_info[1] is not None: + output_status_message("MigrationType: {0}".format(migration_status_info[1][0].MigrationType)) + output_status_message("StartTimeInUtc: {0}".format(migration_status_info[1][0].StartTimeInUtc)) + output_status_message("Status: {0}".format(migration_status_info[1][0].Status)) + +def get_sample_site_links_ad_extensions(): + site_links_ad_extensions=campaign_service.factory.create('ArrayOfAdExtension') + site_links_ad_extension=set_elements_to_none(campaign_service.factory.create('SiteLinksAdExtension')) + site_links=campaign_service.factory.create('ArrayOfSiteLink') + + for index in range(2): + site_link=set_elements_to_none(campaign_service.factory.create('SiteLink')) + site_link.Description1="Simple & Transparent." + site_link.Description2="No Upfront Cost." + site_link.DisplayText = "Women's Shoe Sale " + str(index) + + # If you are currently using the Destination URL, you must upgrade to Final URLs. + # Here is an example of a DestinationUrl you might have used previously. + # site_link.DestinationUrl='http://www.contoso.com/womenshoesale/?season=spring&promocode=PROMO123' + + # To migrate from DestinationUrl to FinalUrls, you can set DestinationUrl + # to an empty string when updating the sitelink. If you are removing DestinationUrl, + # then FinalUrls is required. + # site_link.DestinationUrl="" + + # With FinalUrls you can separate the tracking template, custom parameters, and + # landing page URLs. + final_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_urls.string.append('http://www.contoso.com/womenshoesale') + site_link.FinalUrls=final_urls + + # Final Mobile URLs can also be used if you want to direct the user to a different page + # for mobile devices. + final_mobile_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_mobile_urls.string.append('http://mobile.contoso.com/womenshoesale') + site_link.FinalMobileUrls=final_mobile_urls + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + site_link.TrackingUrlTemplate=None + + # Set custom parameters that are specific to this sitelink, + # and can be used by the sitelink, ad group, campaign, or account level tracking template. + # In this example we are using the campaign level tracking template. + url_custom_parameters=campaign_service.factory.create('ns0:CustomParameters') + parameters=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter1=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter1.Key='promoCode' + custom_parameter1.Value='PROMO' + str(index) + parameters.CustomParameter.append(custom_parameter1) + custom_parameter2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter2.Key='season' + custom_parameter2.Value='summer' + parameters.CustomParameter.append(custom_parameter2) + url_custom_parameters.Parameters=parameters + site_link.UrlCustomParameters=url_custom_parameters + site_links.SiteLink.append(site_link) + + site_links_ad_extension.SiteLinks=site_links + site_links_ad_extensions.AdExtension.append(site_links_ad_extension) + + return site_links_ad_extensions + +def get_sample_sitelink2_ad_extensions(): + sitelink2_ad_extensions=campaign_service.factory.create('ArrayOfAdExtension') + + for index in range(2): + sitelink2_ad_extension=set_elements_to_none(campaign_service.factory.create('Sitelink2AdExtension')) + sitelink2_ad_extension.Description1="Simple & Transparent." + sitelink2_ad_extension.Description2="No Upfront Cost." + sitelink2_ad_extension.DisplayText = "Women's Shoe Sale " + str(index) + + # If you are currently using the Destination URL, you must upgrade to Final URLs. + # Here is an example of a DestinationUrl you might have used previously. + # sitelink2_ad_extension.DestinationUrl='http://www.contoso.com/womenshoesale/?season=spring&promocode=PROMO123' + + # To migrate from DestinationUrl to FinalUrls, you can set DestinationUrl + # to an empty string when updating the ad extension. If you are removing DestinationUrl, + # then FinalUrls is required. + # sitelink2_ad_extension.DestinationUrl="" + + # With FinalUrls you can separate the tracking template, custom parameters, and + # landing page URLs. + final_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_urls.string.append('http://www.contoso.com/womenshoesale') + sitelink2_ad_extension.FinalUrls=final_urls + + # Final Mobile URLs can also be used if you want to direct the user to a different page + # for mobile devices. + final_mobile_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_mobile_urls.string.append('http://mobile.contoso.com/womenshoesale') + sitelink2_ad_extension.FinalMobileUrls=final_mobile_urls + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + sitelink2_ad_extension.TrackingUrlTemplate=None + + # Set custom parameters that are specific to this ad extension, + # and can be used by the ad extension, ad group, campaign, or account level tracking template. + # In this example we are using the campaign level tracking template. + url_custom_parameters=campaign_service.factory.create('ns0:CustomParameters') + parameters=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter1=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter1.Key='promoCode' + custom_parameter1.Value='PROMO' + str(index) + parameters.CustomParameter.append(custom_parameter1) + custom_parameter2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter2.Key='season' + custom_parameter2.Value='summer' + parameters.CustomParameter.append(custom_parameter2) + url_custom_parameters.Parameters=parameters + sitelink2_ad_extension.UrlCustomParameters=url_custom_parameters + sitelink2_ad_extensions.AdExtension.append(sitelink2_ad_extension) + + return sitelink2_ad_extensions + +# Main execution +if __name__ == '__main__': + + 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 + + + # To prepare for the sitelink ad extensions migration by the end of September 2017, you will need + # to determine whether the account has been migrated from SiteLinksAdExtension to Sitelink2AdExtension. + # All ad extension service operations available for both types of sitelinks; however you will + # need to determine which type to add, update, and retrieve. + + sitelink_migration_is_completed = False + + # Optionally you can find out which pilot features the customer is able to use. Even if the customer + # is in pilot for sitelink migrations, the accounts that it contains might not be migrated. + feature_pilot_flags = customer_service.GetCustomerPilotFeatures(authorization_data.customer_id) + + # The pilot flag value for Sitelink ad extension migration is 253. + # Pilot flags apply to all accounts within a given customer; however, + # each account goes through migration individually and has its own migration status. + if(253 in feature_pilot_flags['int']): + # Account migration status below will be either NotStarted, InProgress, or Completed. + output_status_message("Customer is in pilot for Sitelink migration.\n") + else: + # Account migration status below will be NotInPilot. + output_status_message("Customer is not in pilot for Sitelink migration.\n") + + # Even if you have multiple accounts per customer, each account will have its own + # migration status. This example checks one account using the provided AuthorizationData. + account_migration_statuses_infos = campaign_service.GetAccountMigrationStatuses( + {'long': authorization_data.account_id}, + SITELINK_MIGRATION + ) + + for account_migration_statuses_info in account_migration_statuses_infos['AccountMigrationStatusesInfo']: + output_account_migration_statuses_info(account_migration_statuses_info) + for migration_status_info in account_migration_statuses_info['MigrationStatusInfo']: + if migration_status_info[1][0].Status == 'Completed' and SITELINK_MIGRATION == migration_status_info[1][0].MigrationType: + sitelink_migration_is_completed = True + + + # Add a campaign that will later be associated with ad extensions. + + campaigns=campaign_service.factory.create('ArrayOfCampaign') + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving='true' # Accepts 'true', 'false', True, or False + campaign.Status='Paused' + + # Used with FinalUrls shown in the sitelinks that we will add below. + campaign.TrackingUrlTemplate="http://tracker.example.com/?season={_season}&promocode={_promocode}&u={lpurl}" + + campaigns.Campaign.append(campaign) + + add_campaigns_response=campaign_service.AddCampaigns( + AccountId=authorization_data.account_id, + Campaigns=campaigns + ) + campaign_ids={ + 'long': add_campaigns_response.CampaignIds['long'] if add_campaigns_response.CampaignIds['long'] else None + } + + output_campaign_ids(campaign_ids) + + # Specify the extensions. + + ad_extensions=campaign_service.factory.create('ArrayOfAdExtension') + + app_ad_extension=set_elements_to_none(campaign_service.factory.create('AppAdExtension')) + app_ad_extension.AppPlatform='Windows' + app_ad_extension.AppStoreId='AppStoreIdGoesHere' + app_ad_extension.DisplayText='Contoso' + app_ad_extension.DestinationUrl='DestinationUrlGoesHere' + # If you supply the AppAdExtension properties above, then you can add this line. + #ad_extensions.AdExtension.append(app_ad_extension) + + call_ad_extension=set_elements_to_none(campaign_service.factory.create('CallAdExtension')) + call_ad_extension.CountryCode="US" + call_ad_extension.PhoneNumber="2065550100" + call_ad_extension.IsCallOnly=False + call_ad_extension.Status=None + # For this example assume the call center is open Monday - Friday from 9am - 9pm + # in the account's time zone. + call_scheduling=set_elements_to_none(campaign_service.factory.create('Schedule')) + call_day_time_ranges=campaign_service.factory.create('ArrayOfDayTime') + call_monday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_monday.Day='Monday' + call_monday.StartHour=9 + call_monday.StartMinute='Zero' + call_monday.EndHour=21 + call_monday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_monday) + call_tuesday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_tuesday.Day='Tuesday' + call_tuesday.StartHour=9 + call_tuesday.StartMinute='Zero' + call_tuesday.EndHour=21 + call_tuesday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_tuesday) + call_wednesday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_wednesday.Day='Wednesday' + call_wednesday.StartHour=9 + call_wednesday.StartMinute='Zero' + call_wednesday.EndHour=21 + call_wednesday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_wednesday) + call_thursday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_thursday.Day='Thursday' + call_thursday.StartHour=9 + call_thursday.StartMinute='Zero' + call_thursday.EndHour=21 + call_thursday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_thursday) + call_friday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_friday.Day='Friday' + call_friday.StartHour=9 + call_friday.StartMinute='Zero' + call_friday.EndHour=21 + call_friday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_friday) + call_scheduling.DayTimeRanges=call_day_time_ranges + call_scheduling_end_date=campaign_service.factory.create('Date') + call_scheduling_end_date.Day=31 + call_scheduling_end_date.Month=12 + call_scheduling_end_date.Year=strftime("%Y", gmtime()) + call_scheduling.EndDate=call_scheduling_end_date + call_scheduling.StartDate=None + call_ad_extension.Scheduling=call_scheduling + ad_extensions.AdExtension.append(call_ad_extension) + + callout_ad_extension=set_elements_to_none(campaign_service.factory.create('CalloutAdExtension')) + callout_ad_extension.Text="Callout Text" + ad_extensions.AdExtension.append(callout_ad_extension) + + location_ad_extension=set_elements_to_none(campaign_service.factory.create('LocationAdExtension')) + location_ad_extension.PhoneNumber="206-555-0100" + location_ad_extension.CompanyName="Contoso Shoes" + address=campaign_service.factory.create('Address') + address.StreetAddress="1234 Washington Place" + address.StreetAddress2="Suite 1210" + address.CityName="Woodinville" + address.ProvinceName="WA" + address.CountryCode="US" + address.PostalCode="98608" + location_ad_extension.Address=address + location_scheduling=set_elements_to_none(campaign_service.factory.create('Schedule')) + location_day_time_ranges=campaign_service.factory.create('ArrayOfDayTime') + location_day_time=set_elements_to_none(campaign_service.factory.create('DayTime')) + location_day_time.Day='Saturday' + location_day_time.StartHour=9 + location_day_time.StartMinute='Zero' + location_day_time.EndHour=12 + location_day_time.EndMinute='Zero' + location_day_time_ranges.DayTime.append(location_day_time) + location_scheduling.DayTimeRanges=location_day_time_ranges + location_scheduling_end_date=campaign_service.factory.create('Date') + location_scheduling_end_date.Day=31 + location_scheduling_end_date.Month=12 + location_scheduling_end_date.Year=strftime("%Y", gmtime()) + location_scheduling.EndDate=location_scheduling_end_date + location_scheduling.StartDate=None + location_ad_extension.Scheduling=location_scheduling + ad_extensions.AdExtension.append(location_ad_extension) + + review_ad_extension=set_elements_to_none(campaign_service.factory.create('ReviewAdExtension')) + review_ad_extension.IsExact=True + review_ad_extension.Source="Review Source Name" + review_ad_extension.Text="Review Text" + review_ad_extension.Url="http://review.contoso.com" # The Url of the third-party review. This is not your business Url. + ad_extensions.AdExtension.append(review_ad_extension) + + structured_snippet_ad_extension=set_elements_to_none(campaign_service.factory.create('StructuredSnippetAdExtension')) + structured_snippet_ad_extension.Header = "Brands" + values=campaign_service.factory.create('ns4:ArrayOfstring') + values.string.append('Windows') + values.string.append('Xbox') + values.string.append('Skype') + structured_snippet_ad_extension.Values=values + ad_extensions.AdExtension.append(structured_snippet_ad_extension) + + ad_extensions.AdExtension.append( + get_sample_sitelink2_ad_extensions()['AdExtension'] + if sitelink_migration_is_completed + else get_sample_site_links_ad_extensions()['AdExtension']) + + # Add all extensions to the account's ad extension library + ad_extension_identities=campaign_service.AddAdExtensions( + AccountId=authorization_data.account_id, + AdExtensions=ad_extensions + ) + + output_status_message("Added ad extensions.\n") + + # DeleteAdExtensionsAssociations, SetAdExtensionsAssociations, and GetAdExtensionsEditorialReasons + # operations each require a list of type AdExtensionIdToEntityIdAssociation. + ad_extension_id_to_entity_id_associations=campaign_service.factory.create('ArrayOfAdExtensionIdToEntityIdAssociation') + + # GetAdExtensionsByIds requires a list of type long. + ad_extension_ids=[] + + # Loop through the list of extension IDs and build any required data structures + # for subsequent operations. + + for ad_extension_identity in ad_extension_identities['AdExtensionIdentity']: + ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + ad_extension_id_to_entity_id_association.AdExtensionId=ad_extension_identity.Id + ad_extension_id_to_entity_id_association.EntityId=campaign_ids['long'][0] + ad_extension_id_to_entity_id_associations.AdExtensionIdToEntityIdAssociation.append(ad_extension_id_to_entity_id_association) + + ad_extension_ids.append(ad_extension_identity.Id) + + # Associate the specified ad extensions with the respective campaigns or ad groups. + campaign_service.SetAdExtensionsAssociations( + AccountId=authorization_data.account_id, + AdExtensionIdToEntityIdAssociations=ad_extension_id_to_entity_id_associations, + AssociationType='Campaign' + ) + + output_status_message("Set ad extension associations.\n") + + # Get editorial rejection reasons for the respective ad extension and entity associations. + ad_extension_editorial_reason_collection=campaign_service.GetAdExtensionsEditorialReasons( + AccountId=authorization_data.account_id, + AdExtensionIdToEntityIdAssociations=ad_extension_id_to_entity_id_associations, + AssociationType='Campaign' + ) + + ad_extensions_type_filter='AppAdExtension ' \ + 'CallAdExtension ' \ + 'CalloutAdExtension ' \ + 'ImageAdExtension ' \ + 'LocationAdExtension ' \ + 'ReviewAdExtension ' \ + 'StructuredSnippetAdExtension' + + ad_extensions_type_filter+=(' Sitelink2AdExtension' if sitelink_migration_is_completed else ' SiteLinksAdExtension') + + return_additional_fields='Scheduling' + + # Get the specified ad extensions from the account's ad extension library. + ad_extensions=campaign_service.factory.create('ArrayOfAdExtension') + ad_extensions=campaign_service.GetAdExtensionsByIds( + AccountId=authorization_data.account_id, + AdExtensionIds={'long': ad_extension_ids}, + AdExtensionType=ad_extensions_type_filter, + ReturnAdditionalFields=return_additional_fields + ) + + output_status_message("List of ad extensions that were added above:\n") + output_ad_extensions(ad_extensions, ad_extension_editorial_reason_collection) + + # Get only the location extensions and remove scheduling. + + adExtensionsTypeFilter = 'LocationAdExtension' + + ad_extensions=campaign_service.GetAdExtensionsByIds( + AccountId=authorization_data.account_id, + AdExtensionIds={'long': ad_extension_ids}, + AdExtensionType=ad_extensions_type_filter, + ReturnAdditionalFields=return_additional_fields + ) + + update_extensions=campaign_service.factory.create('ArrayOfAdExtension') + update_extension_ids = [] + + for extension in ad_extensions['AdExtension']: + + # GetAdExtensionsByIds will return a nil element if the request filters / conditions were not met. + if extension is not None and extension.Id is not None: + + # Remove read-only elements that would otherwise cause the update operation to fail. + update_extension = set_read_only_ad_extension_elements_to_none(extension) + + # If you set the Scheduling element null, any existing scheduling set for the ad extension will remain unchanged. + # If you set this to any non-null Schedule object, you are effectively replacing existing scheduling + # for the ad extension. In this example, we will remove any existing scheduling by setting this element + # to an empty Schedule object. + update_extension.Scheduling=campaign_service.factory.create('Schedule') + + update_extensions.AdExtension.append(update_extension) + update_extension_ids.append(update_extension.Id) + + output_status_message("Removing scheduling from the location ad extensions..\n"); + campaign_service.UpdateAdExtensions( + AccountId=authorization_data.account_id, + AdExtensions=update_extensions + ) + + ad_extensions=campaign_service.GetAdExtensionsByIds( + AccountId=authorization_data.account_id, + AdExtensionIds={'long': update_extension_ids}, + AdExtensionType=ad_extensions_type_filter, + ReturnAdditionalFields=return_additional_fields + ) + + output_status_message("List of ad extensions that were updated above:\n"); + output_ad_extensions(ad_extensions, None) + + + # Remove the specified associations from the respective campaigns or ad groups. + # The extesions are still available in the account's extensions library. + campaign_service.DeleteAdExtensionsAssociations( + AccountId=authorization_data.account_id, + AdExtensionIdToEntityIdAssociations=ad_extension_id_to_entity_id_associations, + AssociationType='Campaign' + ) + + output_status_message("Deleted ad extension associations.\n") + + # Deletes the ad extensions from the account's ad extension library. + campaign_service.DeleteAdExtensions( + AccountId=authorization_data.account_id, + AdExtensionIds={'long': ad_extension_ids}, + ) + + output_status_message("Deleted ad extensions.\n") + + campaign_service.DeleteCampaigns( + AccountId=authorization_data.account_id, + CampaignIds=campaign_ids + ) + + for campaign_id in campaign_ids['long']: + output_status_message("Deleted CampaignId {0}\n".format(campaign_id)) + + 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/BudgetOpportunities.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BudgetOpportunities.py index b116c243..e0edbe29 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BudgetOpportunities.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BudgetOpportunities.py @@ -106,13 +106,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BudgetOpportunities.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BudgetOpportunities.py.bak new file mode 100644 index 00000000..b116c243 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BudgetOpportunities.py.bak @@ -0,0 +1,348 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + adinsight_service=ServiceClient( + service='AdInsightService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=10 + ) + + 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 output_status_message(message): + print(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_budget_opportunities(budget_opportunities, campaign_id): + if budget_opportunities is not None and len(budget_opportunities) > 0: + for budget_opportunity in budget_opportunities['BudgetOpportunity']: + output_status_message("BudgetPoints: ") + for budget_point in budget_opportunity.BudgetPoints['BudgetPoint']: + output_budget_point(budget_point) + output_status_message("BudgetType: {0}".format(budget_opportunity.BudgetType)) + output_status_message("CampaignId: {0}".format(budget_opportunity.CampaignId)) + output_status_message("CurrentBudget: {0}".format(budget_opportunity.CurrentBudget)) + output_status_message("IncreaseInClicks: {0}".format(budget_opportunity.IncreaseInClicks)) + output_status_message("IncreaseInImpressions: {0}".format(budget_opportunity.IncreaseInImpressions)) + output_status_message("OpportunityKey: {0}".format(budget_opportunity.OpportunityKey)) + output_status_message("PercentageIncreaseInClicks: {0}".format(budget_opportunity.PercentageIncreaseInClicks)) + output_status_message("PercentageIncreaseInImpressions: {0}".format(budget_opportunity.PercentageIncreaseInImpressions)) + output_status_message("RecommendedBudget: {0}".format(budget_opportunity.RecommendedBudget)) + else: + output_status_message("There are no budget opportunities for CampaignId: {0}".format(campaign_id)) + +def output_budget_point(budget_point): + if budget_point is not None: + output_status_message("BudgetAmount: {0}".format(budget_point.BudgetAmount)) + output_status_message("BudgetPointType: {0}".format(budget_point.BudgetPointType)) + output_status_message("EstimatedWeeklyClicks: {0}".format(budget_point.EstimatedWeeklyClicks)) + output_status_message("EstimatedWeeklyCost: {0}".format(budget_point.EstimatedWeeklyCost)) + output_status_message("EstimatedWeeklyImpressions: {0}".format(budget_point.EstimatedWeeklyImpressions)) + + +# Main execution +if __name__ == '__main__': + + 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 + + # Get the budget opportunities for each campaign in the current authenticated account. + + campaign_types=['SearchAndContent', 'Shopping'] + campaigns=campaign_service.GetCampaignsByAccountId(authorization_data.account_id, campaign_types) + + for campaign in campaigns['Campaign']: + if campaign.Id is not None: + opportunities=adinsight_service.GetBudgetOpportunities(campaign.Id) + output_budget_opportunities(opportunities, campaign.Id) + + 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/BulkAdExtensions.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkAdExtensions.py index d2ab8967..ede0d927 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkAdExtensions.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkAdExtensions.py @@ -148,13 +148,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkAdExtensions.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkAdExtensions.py.bak new file mode 100644 index 00000000..d2ab8967 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkAdExtensions.py.bak @@ -0,0 +1,1557 @@ +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' + + APP_AD_EXTENSION_ID_KEY=-11 + CALL_AD_EXTENSION_ID_KEY=-12 + CALLOUT_AD_EXTENSION_ID_KEY=-13 + IMAGE_AD_EXTENSION_ID_KEY=-14 + LOCATION_AD_EXTENSION_ID_KEY=-15 + REVIEW_AD_EXTENSION_ID_KEY=-16 + SITE_LINK_AD_EXTENSION_ID_KEY=-17 + SITELINK2_AD_EXTENSION_ID_KEY=-17 + STRUCTURED_SNIPPET_AD_EXTENSION_ID_KEY=-18 + CAMPAIGN_ID_KEY=-123 + + SITELINK_MIGRATION = 'SiteLinkAdExtension' + + # 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_bulk_campaigns(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaign: \n") + output_status_message("AccountId: {0}".format(entity.account_id)) + 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 the Campaign Management Campaign Object + output_campaign(entity.campaign) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_campaign(campaign): + if campaign is not None: + output_status_message("BudgetType: {0}".format(campaign.BudgetType)) + if campaign.CampaignType is not None: + for campaign_type in campaign.CampaignType: + output_status_message("CampaignType: {0}".format(campaign_type)) + else: + output_status_message("CampaignType: None") + output_status_message("DailyBudget: {0}".format(campaign.DailyBudget)) + output_status_message("Description: {0}".format(campaign.Description)) + output_status_message("ForwardCompatibilityMap: ") + if campaign.ForwardCompatibilityMap is not None and len(campaign.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(campaign.Id)) + output_status_message("Name: {0}".format(campaign.Name)) + output_status_message("NativeBidAdjustment: {0}".format(campaign.NativeBidAdjustment)) + output_status_message("Settings: ") + for setting in campaign.Settings.Setting: + if setting.Type == 'ShoppingSetting': + output_status_message("\tShoppingSetting: ") + output_status_message("\t\tPriority: {0}".format(setting.Priority)) + output_status_message("\t\tSalesCountryCode: {0}".format(setting.SalesCountryCode)) + output_status_message("\t\tStoreId: {0}".format(setting.StoreId)) + output_status_message("Status: {0}".format(campaign.Status)) + output_status_message("TimeZone: {0}".format(campaign.TimeZone)) + output_status_message("TrackingUrlTemplate: {0}".format(campaign.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if campaign.UrlCustomParameters is not None and campaign.UrlCustomParameters.Parameters is not None: + for custom_parameter in campaign.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_bulk_app_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAppAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + output_status_message("Client Id: {0}".format(entity.client_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management AppAdExtension Object + output_app_ad_extension(entity.app_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_app_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignAppAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_call_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCallAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + output_status_message("Client Id: {0}".format(entity.client_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management CallAdExtension Object + output_call_ad_extension(entity.call_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_call_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignCallAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_callout_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCalloutAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + output_status_message("Client Id: {0}".format(entity.client_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management CalloutAdExtension Object + output_callout_ad_extension(entity.callout_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_callout_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignCalloutAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_location_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkLocationAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + output_status_message("Client Id: {0}".format(entity.client_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management LocationAdExtension Object + output_location_ad_extension(entity.location_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_location_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignLocationAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_review_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkReviewAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + output_status_message("Client Id: {0}".format(entity.client_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management ReviewAdExtension Object + output_review_ad_extension(entity.review_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_review_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignReviewAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_site_link_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkSiteLinkAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management SiteLinksAdExtension Object + output_site_links_ad_extension(entity.site_links_ad_extension) + + if entity.site_links is not None and len(entity.site_links) > 0: + output_bulk_site_links(entity.site_links) + + output_status_message('') + +def output_bulk_site_links(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkSiteLink: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + output_status_message("Ad Extension Id: {0}".format(entity.ad_extension_id)) + output_status_message("Client Id: {0}".format(entity.client_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Order: {0}".format(entity.order)) + output_status_message("Status: {0}".format(entity.status)) + output_status_message("Version: {0}".format(entity.version)) + + # Output the Campaign Management SiteLink Object + output_site_links([entity.site_link]) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_site_link_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignSiteLinkAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_sitelink2_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkSitelink2AdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management Sitelink2AdExtension Object + output_sitelink2_ad_extension(entity.sitelink2_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_sitelink2_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignSitelink2AdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_structured_snippet_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkStructuredSnippetAdExtension: \n") + output_status_message("Account Id: {0}".format(entity.account_id)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + + # Output the Campaign Management StructuredSnippetAdExtension Object + output_structured_snippet_ad_extension(entity.structured_snippet_ad_extension) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_structured_snippet_ad_extensions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignStructuredSnippetAdExtension: \n") + if entity.ad_extension_id_to_entity_id_association is not None: + output_status_message("AdExtensionId: {0}".format(entity.ad_extension_id_to_entity_id_association.AdExtensionId)) + output_status_message("EntityId: {0}".format(entity.ad_extension_id_to_entity_id_association.EntityId)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Client Id: {0}".format(entity.client_id)) + output_status_message("Editorial Status: {0}".format(entity.editorial_status)) + if entity.last_modified_time is not None: + output_status_message("LastModifiedTime: {0}".format(entity.last_modified_time)) + output_status_message("Status: {0}".format(entity.status)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_extension(extension): + if extension is not None: + output_status_message("Id: {0}".format(extension.Id)) + output_status_message("Type: {0}".format(extension.Type)) + output_status_message("ForwardCompatibilityMap: ") + if extension.ForwardCompatibilityMap is not None: + for pair in extension.ForwardCompatibilityMap['KeyValuePairOfstringstring']: + output_status_message("Key: {0}".format(pair.Key)) + output_status_message("Value: {0}".format(pair.Value)) + output_status_message("Scheduling: ") + # Scheduling is not emitted by default, so we must first test whether it exists. + if hasattr(extension, 'Scheduling') and extension.Scheduling is not None: + output_schedule(extension.Scheduling) + output_status_message("Status: {0}".format(extension.Status)) + output_status_message("Version: {0}".format(extension.Version)) + +def output_schedule(schedule): + if schedule is not None: + if schedule.DayTimeRanges is not None: + for day_time in schedule.DayTimeRanges['DayTime']: + output_status_message("Day: {0}".format(day_time.Day)) + output_status_message("EndHour: {0}".format(day_time.EndHour)) + output_status_message("EndMinute: {0}".format(day_time.EndMinute)) + output_status_message("StartHour: {0}".format(day_time.StartHour)) + output_status_message("StartMinute: {0}".format(day_time.StartMinute)) + if schedule.EndDate is not None: + output_status_message(("EndDate: {0}/{1}/{2}".format( + schedule.EndDate.Month, + schedule.EndDate.Day, + schedule.EndDate.Year))) + if schedule.StartDate is not None: + output_status_message(("StartDate: {0}/{1}/{2}".format( + schedule.StartDate.Month, + schedule.StartDate.Day, + schedule.StartDate.Year))) + use_searcher_time_zone = \ + True if (schedule.UseSearcherTimeZone is not None and schedule.UseSearcherTimeZone == True) else False + output_status_message("UseSearcherTimeZone: {0}".format(use_searcher_time_zone)) + +def output_app_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the AppAdExtension + output_status_message("AppPlatform: {0}".format(extension.AppPlatform)) + output_status_message("AppStoreId: {0}".format(extension.AppStoreId)) + output_status_message("DestinationUrl: {0}".format(extension.DestinationUrl)) + output_status_message("DevicePreference: {0}".format(extension.DevicePreference)) + output_status_message("DisplayText: {0}".format(extension.DisplayText)) + +def output_call_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the CallAdExtension + output_status_message("CountryCode: {0}".format(extension.CountryCode)) + output_status_message("DevicePreference: {0}".format(extension.DevicePreference)) + output_status_message("IsCallOnly: {0}".format(extension.IsCallOnly)) + output_status_message("IsCallTrackingEnabled: {0}".format(extension.IsCallTrackingEnabled)) + output_status_message("PhoneNumber: {0}".format(extension.PhoneNumber)) + output_status_message("RequireTollFreeTrackingNumber: {0}".format(extension.RequireTollFreeTrackingNumber)) + +def output_callout_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the CalloutAdExtension + output_status_message("Callout Text: {0}".format(extension.Text)) + +def output_image_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the ImageAdExtension + output_status_message("AlternativeText: {0}".format(extension.AlternativeText)) + output_status_message("Description: {0}".format(extension.Description)) + output_status_message("DestinationUrl: {0}".format(extension.DestinationUrl)) + output_status_message("FinalMobileUrls: ") + if extension.FinalMobileUrls is not None: + for final_mobile_url in extension.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if extension.FinalUrls is not None: + for final_url in extension.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("ImageMediaIds: ") + if extension.ImageMediaIds is not None: + for id in extension.ImageMediaIds['string']: + output_status_message("\t{0}".format(id)) + output_status_message("TrackingUrlTemplate: {0}".format(extension.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if extension.UrlCustomParameters is not None and extension.UrlCustomParameters.Parameters is not None: + for custom_parameter in extension.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_location_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the LocationAdExtension + if extension.Address is not None: + output_status_message("CityName: {0}".format(extension.Address.CityName)) + output_status_message("CountryCode: {0}".format(extension.Address.CountryCode)) + output_status_message("PostalCode: {0}".format(extension.Address.PostalCode)) + output_status_message("ProvinceCode: {0}".format(extension.Address.ProvinceCode)) + output_status_message("ProvinceName: {0}".format(extension.Address.ProvinceName)) + output_status_message("StreetAddress: {0}".format(extension.Address.StreetAddress)) + output_status_message("StreetAddress2: {0}".format(extension.Address.StreetAddress2)) + output_status_message("CompanyName: {0}".format(extension.CompanyName)) + output_status_message("GeoCodeStatus: {0}".format(extension.GeoCodeStatus)) + if extension.GeoPoint is not None: + output_status_message("GeoPoint: ") + output_status_message("LatitudeInMicroDegrees: {0}".format(extension.GeoPoint.LatitudeInMicroDegrees)) + output_status_message("LongitudeInMicroDegrees: {0}".format(extension.GeoPoint.LongitudeInMicroDegrees)) + output_status_message("IconMediaId: {0}".format(extension.IconMediaId)) + output_status_message("ImageMediaId: {0}".format(extension.ImageMediaId)) + output_status_message("PhoneNumber: {0}".format(extension.PhoneNumber)) + +def output_review_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the ReviewAdExtension + output_status_message("IsExact: {0}".format(extension.IsExact)) + output_status_message("Source: {0}".format(extension.Source)) + output_status_message("Text: {0}".format(extension.Text)) + output_status_message("Url: {0}".format(extension.Url)) + +def output_structured_snippet_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the StructuredSnippetAdExtension + output_status_message("Header: {0}".format(extension.Header)) + for value in extension.Values['string']: + output_status_message("\t{0}".format(value)) + +def output_site_links_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the SiteLinksAdExtension + output_site_links(extension.SiteLinks['SiteLink']) + +def output_site_links(site_links): + if site_links is not None: + for site_link in site_links: + output_status_message("Description1: {0}".format(site_link.Description1)) + output_status_message("Description2: {0}".format(site_link.Description2)) + output_status_message("DevicePreference: {0}".format(site_link.DevicePreference)) + output_status_message("DisplayText: {0}".format(site_link.DisplayText)) + output_status_message("DestinationUrl: {0}".format(site_link.DestinationUrl)) + output_status_message("FinalMobileUrls: ") + if site_link.FinalMobileUrls is not None: + for final_mobile_url in site_link.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if site_link.FinalUrls is not None: + for final_url in site_link.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("TrackingUrlTemplate: {0}".format(site_link.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if site_link.UrlCustomParameters is not None and site_link.UrlCustomParameters.Parameters is not None: + for custom_parameter in site_link.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_sitelink2_ad_extension(extension): + if extension is not None: + # Output inherited properties of the AdExtension base class. + output_ad_extension(extension) + + # Output properties that are specific to the Sitelink2AdExtension + output_status_message("Description1: {0}".format(extension.Description1)) + output_status_message("Description2: {0}".format(extension.Description2)) + output_status_message("DevicePreference: {0}".format(extension.DevicePreference)) + output_status_message("DisplayText: {0}".format(extension.DisplayText)) + output_status_message("DestinationUrl: {0}".format(extension.DestinationUrl)) + output_status_message("FinalMobileUrls: ") + if extension.FinalMobileUrls is not None: + for final_mobile_url in extension.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if extension.FinalUrls is not None: + for final_url in extension.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("TrackingUrlTemplate: {0}".format(extension.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if extension.UrlCustomParameters is not None and extension.UrlCustomParameters.Parameters is not None: + for custom_parameter in extension.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_account_migration_statuses_info(account_migration_statuses_info): + if account_migration_statuses_info is not None: + output_status_message("AccountId: {0}".format(account_migration_statuses_info.AccountId)) + for migration_status_info in account_migration_statuses_info['MigrationStatusInfo']: + output_migration_status_info(migration_status_info) + +def output_migration_status_info(migration_status_info): + if migration_status_info is not None and migration_status_info[1] is not None: + output_status_message("MigrationType: {0}".format(migration_status_info[1][0].MigrationType)) + output_status_message("StartTimeInUtc: {0}".format(migration_status_info[1][0].StartTimeInUtc)) + output_status_message("Status: {0}".format(migration_status_info[1][0].Status)) + +def get_sample_bulk_sitelink2_ad_extensions(account_id): + entities=[] + + for index in range(2): + bulk_sitelink2_ad_extension=BulkSitelink2AdExtension() + bulk_sitelink2_ad_extension.account_id=account_id + sitelink2_ad_extension=set_elements_to_none(campaign_service.factory.create('Sitelink2AdExtension')) + sitelink2_ad_extension.Description1="Simple & Transparent." + sitelink2_ad_extension.Description2="No Upfront Cost." + sitelink2_ad_extension.DisplayText = "Women's Shoe Sale " + str(index+1) + + # If you are currently using the Destination URL, you must upgrade to Final URLs. + # Here is an example of a DestinationUrl you might have used previously. + # sitelink2_ad_extension.DestinationUrl='http://www.contoso.com/womenshoesale/?season=spring&promocode=PROMO123' + + # To migrate from DestinationUrl to FinalUrls, you can set DestinationUrl + # to an empty string when updating the ad extension. If you are removing DestinationUrl, + # then FinalUrls is required. + # sitelink2_ad_extension.DestinationUrl="" + + # With FinalUrls you can separate the tracking template, custom parameters, and + # landing page URLs. + final_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_urls.string.append('http://www.contoso.com/womenshoesale') + sitelink2_ad_extension.FinalUrls=final_urls + + # Final Mobile URLs can also be used if you want to direct the user to a different page + # for mobile devices. + final_mobile_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_mobile_urls.string.append('http://mobile.contoso.com/womenshoesale') + sitelink2_ad_extension.FinalMobileUrls=final_mobile_urls + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + sitelink2_ad_extension.TrackingUrlTemplate=None + + # Set custom parameters that are specific to this ad extension, + # and can be used by the ad extension, ad group, campaign, or account level tracking template. + # In this example we are using the campaign level tracking template. + url_custom_parameters=campaign_service.factory.create('ns0:CustomParameters') + parameters=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter1=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter1.Key='promoCode' + custom_parameter1.Value='PROMO' + str(index+1) + parameters.CustomParameter.append(custom_parameter1) + custom_parameter2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter2.Key='season' + custom_parameter2.Value='summer' + parameters.CustomParameter.append(custom_parameter2) + url_custom_parameters.Parameters=parameters + sitelink2_ad_extension.UrlCustomParameters=url_custom_parameters + + sitelink2_ad_extension.Status=None + sitelink2_ad_extension.Id=SITELINK2_AD_EXTENSION_ID_KEY + bulk_sitelink2_ad_extension.sitelink2_ad_extension=sitelink2_ad_extension + + entities.append(bulk_sitelink2_ad_extension) + + bulk_campaign_sitelink2_ad_extension=BulkCampaignSitelink2AdExtension() + sitelink2_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + sitelink2_ad_extension_id_to_entity_id_association.AdExtensionId=SITELINK2_AD_EXTENSION_ID_KEY + sitelink2_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_sitelink2_ad_extension.ad_extension_id_to_entity_id_association=sitelink2_ad_extension_id_to_entity_id_association + + entities.append(bulk_campaign_sitelink2_ad_extension) + + return entities + +def get_sample_bulk_site_links_ad_extensions(account_id): + entities=[] + + bulk_site_link_ad_extension=BulkSiteLinkAdExtension() + bulk_site_link_ad_extension.account_id=account_id + site_links_ad_extension=set_elements_to_none(campaign_service.factory.create('SiteLinksAdExtension')) + site_links=campaign_service.factory.create('ArrayOfSiteLink') + + for index in range(2): + site_link=set_elements_to_none(campaign_service.factory.create('SiteLink')) + site_link.Description1="Simple & Transparent." + site_link.Description2="No Upfront Cost." + site_link.DisplayText = "Women's Shoe Sale " + str(index+1) + + # If you are currently using the Destination URL, you must upgrade to Final URLs. + # Here is an example of a DestinationUrl you might have used previously. + # site_link.DestinationUrl='http://www.contoso.com/womenshoesale/?season=spring&promocode=PROMO123' + + # To migrate from DestinationUrl to FinalUrls, you can set DestinationUrl + # to an empty string when updating the sitelink. If you are removing DestinationUrl, + # then FinalUrls is required. + # site_link.DestinationUrl="" + + # With FinalUrls you can separate the tracking template, custom parameters, and + # landing page URLs. + final_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_urls.string.append('http://www.contoso.com/womenshoesale') + site_link.FinalUrls=final_urls + + # Final Mobile URLs can also be used if you want to direct the user to a different page + # for mobile devices. + final_mobile_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_mobile_urls.string.append('http://mobile.contoso.com/womenshoesale') + site_link.FinalMobileUrls=final_mobile_urls + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + site_link.TrackingUrlTemplate=None + + # Set custom parameters that are specific to this sitelink, + # and can be used by the sitelink, ad group, campaign, or account level tracking template. + # In this example we are using the campaign level tracking template. + url_custom_parameters=campaign_service.factory.create('ns0:CustomParameters') + parameters=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter1=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter1.Key='promoCode' + custom_parameter1.Value='PROMO' + str(index+1) + parameters.CustomParameter.append(custom_parameter1) + custom_parameter2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter2.Key='season' + custom_parameter2.Value='summer' + parameters.CustomParameter.append(custom_parameter2) + url_custom_parameters.Parameters=parameters + site_link.UrlCustomParameters=url_custom_parameters + site_links.SiteLink.append(site_link) + + site_links_ad_extension.SiteLinks=site_links + site_links_ad_extension.Status=None + site_links_ad_extension.Id=SITE_LINK_AD_EXTENSION_ID_KEY + bulk_site_link_ad_extension.site_links_ad_extension=site_links_ad_extension + + entities.append(bulk_site_link_ad_extension) + + bulk_campaign_site_link_ad_extension=BulkCampaignSiteLinkAdExtension() + site_link_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + site_link_ad_extension_id_to_entity_id_association.AdExtensionId=SITE_LINK_AD_EXTENSION_ID_KEY + site_link_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_site_link_ad_extension.ad_extension_id_to_entity_id_association=site_link_ad_extension_id_to_entity_id_association + + entities.append(bulk_campaign_site_link_ad_extension) + + return entities + +def output_status_message(message): + print(message) + +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_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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +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 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 + + # To prepare for the sitelink ad extensions migration by the end of September 2017, you will need + # to determine whether the account has been migrated from SiteLinksAdExtension to Sitelink2AdExtension. + # All ad extension service operations available for both types of sitelinks; however you will + # need to determine which type to add, update, and retrieve. + + sitelink_migration_is_completed = False + + # Optionally you can find out which pilot features the customer is able to use. Even if the customer + # is in pilot for sitelink migrations, the accounts that it contains might not be migrated. + feature_pilot_flags = customer_service.GetCustomerPilotFeatures(authorization_data.customer_id) + + # The pilot flag value for Sitelink ad extension migration is 253. + # Pilot flags apply to all accounts within a given customer; however, + # each account goes through migration individually and has its own migration status. + if(253 in feature_pilot_flags['int']): + # Account migration status below will be either NotStarted, InProgress, or Completed. + output_status_message("Customer is in pilot for Sitelink migration.\n") + else: + # Account migration status below will be NotInPilot. + output_status_message("Customer is not in pilot for Sitelink migration.\n") + + # Even if you have multiple accounts per customer, each account will have its own + # migration status. This example checks one account using the provided AuthorizationData. + account_migration_statuses_infos = campaign_service.GetAccountMigrationStatuses( + {'long': authorization_data.account_id}, + SITELINK_MIGRATION + ) + + for account_migration_statuses_info in account_migration_statuses_infos['AccountMigrationStatusesInfo']: + output_account_migration_statuses_info(account_migration_statuses_info) + for migration_status_info in account_migration_statuses_info['MigrationStatusInfo']: + if migration_status_info[1][0].Status == 'Completed' and SITELINK_MIGRATION == migration_status_info[1][0].MigrationType: + sitelink_migration_is_completed = True + + + # Prepare the bulk entities that you want to upload. Each bulk entity contains the corresponding campaign management object, + # and additional elements needed to read from and write to a bulk file. + + bulk_campaign=BulkCampaign() + + # The client_id may be used to associate records in the bulk upload file with records in the results file. The value of this field + # is not used or stored by the server; it is simply copied from the uploaded record to the corresponding result record. + # Note: This bulk file Client Id is not related to an application Client Id for OAuth. + + bulk_campaign.client_id='YourClientIdGoesHere' + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + + # When using the Campaign Management service, the Id cannot be set. In the context of a BulkCampaign, the Id is optional + # and may be used as a negative reference key during bulk upload. For example the same negative reference key for the campaign Id + # will be used when associating ad extensions with the campaign. + + campaign.Id=CAMPAIGN_ID_KEY + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.Status='Paused' + + # DaylightSaving is not supported in the Bulk file schema. Whether or not you specify it in a BulkCampaign, + # the value is not written to the Bulk file, and by default DaylightSaving is set to true. + campaign.DaylightSaving='True' + + # Used with FinalUrls shown in the sitelinks that we will add below. + campaign.TrackingUrlTemplate="http://tracker.example.com/?season={_season}&promocode={_promocode}&u={lpurl}" + + bulk_campaign.campaign=campaign + + bulk_app_ad_extension=BulkAppAdExtension() + bulk_app_ad_extension.account_id=authorization_data.account_id + app_ad_extension=set_elements_to_none(campaign_service.factory.create('AppAdExtension')) + app_ad_extension.Id=APP_AD_EXTENSION_ID_KEY + app_ad_extension.AppPlatform='Windows' + app_ad_extension.AppStoreId='AppStoreIdGoesHere' + app_ad_extension.DisplayText='Contoso' + app_ad_extension.DestinationUrl='DestinationUrlGoesHere' + app_ad_extension.Status=None + bulk_app_ad_extension.app_ad_extension=app_ad_extension + + bulk_campaign_app_ad_extension=BulkCampaignAppAdExtension() + app_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + app_ad_extension_id_to_entity_id_association.AdExtensionId=APP_AD_EXTENSION_ID_KEY + app_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_app_ad_extension.ad_extension_id_to_entity_id_association=app_ad_extension_id_to_entity_id_association + + bulk_call_ad_extension=BulkCallAdExtension() + bulk_call_ad_extension.account_id=authorization_data.account_id + call_ad_extension=set_elements_to_none(campaign_service.factory.create('CallAdExtension')) + call_ad_extension.CountryCode="US" + call_ad_extension.PhoneNumber="2065550100" + call_ad_extension.IsCallOnly=False + call_ad_extension.Status=None + # For this example assume the call center is open Monday - Friday from 9am - 9pm + # in the account's time zone. + call_scheduling=set_elements_to_none(campaign_service.factory.create('Schedule')) + call_day_time_ranges=campaign_service.factory.create('ArrayOfDayTime') + call_monday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_monday.Day='Monday' + call_monday.StartHour=9 + call_monday.StartMinute='Zero' + call_monday.EndHour=21 + call_monday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_monday) + call_tuesday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_tuesday.Day='Tuesday' + call_tuesday.StartHour=9 + call_tuesday.StartMinute='Zero' + call_tuesday.EndHour=21 + call_tuesday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_tuesday) + call_wednesday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_wednesday.Day='Wednesday' + call_wednesday.StartHour=9 + call_wednesday.StartMinute='Zero' + call_wednesday.EndHour=21 + call_wednesday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_wednesday) + call_thursday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_thursday.Day='Thursday' + call_thursday.StartHour=9 + call_thursday.StartMinute='Zero' + call_thursday.EndHour=21 + call_thursday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_thursday) + call_friday=set_elements_to_none(campaign_service.factory.create('DayTime')) + call_friday.Day='Friday' + call_friday.StartHour=9 + call_friday.StartMinute='Zero' + call_friday.EndHour=21 + call_friday.EndMinute='Zero' + call_day_time_ranges.DayTime.append(call_friday) + call_scheduling.DayTimeRanges=call_day_time_ranges + call_scheduling_end_date=campaign_service.factory.create('Date') + call_scheduling_end_date.Day=31 + call_scheduling_end_date.Month=12 + call_scheduling_end_date.Year=strftime("%Y", gmtime()) + call_scheduling.EndDate=call_scheduling_end_date + call_scheduling.StartDate=None + call_ad_extension.Scheduling=call_scheduling + call_ad_extension.Id=CALL_AD_EXTENSION_ID_KEY + bulk_call_ad_extension.call_ad_extension=call_ad_extension + + bulk_campaign_call_ad_extension=BulkCampaignCallAdExtension() + call_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + call_ad_extension_id_to_entity_id_association.AdExtensionId=CALL_AD_EXTENSION_ID_KEY + call_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_call_ad_extension.ad_extension_id_to_entity_id_association=call_ad_extension_id_to_entity_id_association + + bulk_callout_ad_extension=BulkCalloutAdExtension() + bulk_callout_ad_extension.account_id=authorization_data.account_id + callout_ad_extension=set_elements_to_none(campaign_service.factory.create('CalloutAdExtension')) + callout_ad_extension.Text="Callout Text" + callout_ad_extension.Status=None + callout_ad_extension.Id=CALLOUT_AD_EXTENSION_ID_KEY + bulk_callout_ad_extension.callout_ad_extension=callout_ad_extension + + bulk_campaign_callout_ad_extension=BulkCampaignCalloutAdExtension() + callout_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + callout_ad_extension_id_to_entity_id_association.AdExtensionId=CALLOUT_AD_EXTENSION_ID_KEY + callout_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_callout_ad_extension.ad_extension_id_to_entity_id_association=callout_ad_extension_id_to_entity_id_association + + bulk_location_ad_extension=BulkLocationAdExtension() + bulk_location_ad_extension.account_id=authorization_data.account_id + location_ad_extension=set_elements_to_none(campaign_service.factory.create('LocationAdExtension')) + location_ad_extension.PhoneNumber="206-555-0100" + location_ad_extension.CompanyName="Contoso Shoes" + location_ad_extension.IconMediaId=None + location_ad_extension.ImageMediaId=None + location_ad_extension.Status=None + location_ad_extension.GeoCodeStatus=None + location_ad_extension.GeoPoint=None + address=campaign_service.factory.create('Address') + address.StreetAddress="1234 Washington Place" + address.StreetAddress2="Suite 1210" + address.CityName="Woodinville" + address.ProvinceName="WA" + address.CountryCode="US" + address.PostalCode="98608" + location_ad_extension.Address=address + location_scheduling=set_elements_to_none(campaign_service.factory.create('Schedule')) + location_day_time_ranges=campaign_service.factory.create('ArrayOfDayTime') + location_day_time=set_elements_to_none(campaign_service.factory.create('DayTime')) + location_day_time.Day='Saturday' + location_day_time.StartHour=9 + location_day_time.StartMinute='Zero' + location_day_time.EndHour=12 + location_day_time.EndMinute='Zero' + location_day_time_ranges.DayTime.append(location_day_time) + location_scheduling.DayTimeRanges=location_day_time_ranges + location_scheduling_end_date=campaign_service.factory.create('Date') + location_scheduling_end_date.Day=31 + location_scheduling_end_date.Month=12 + location_scheduling_end_date.Year=strftime("%Y", gmtime()) + location_scheduling.EndDate=location_scheduling_end_date + location_scheduling.StartDate=None + location_ad_extension.Scheduling=location_scheduling + location_ad_extension.Id=LOCATION_AD_EXTENSION_ID_KEY + bulk_location_ad_extension.location_ad_extension=location_ad_extension + + bulk_campaign_location_ad_extension=BulkCampaignLocationAdExtension() + location_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + location_ad_extension_id_to_entity_id_association.AdExtensionId=LOCATION_AD_EXTENSION_ID_KEY + location_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_location_ad_extension.ad_extension_id_to_entity_id_association=location_ad_extension_id_to_entity_id_association + + bulk_review_ad_extension=BulkReviewAdExtension() + bulk_review_ad_extension.account_id=authorization_data.account_id + review_ad_extension=set_elements_to_none(campaign_service.factory.create('ReviewAdExtension')) + review_ad_extension.IsExact=True + review_ad_extension.Source="Review Source Name" + review_ad_extension.Text="Review Text" + review_ad_extension.Url="http://review.contoso.com" # The Url of the third-party review. This is not your business Url. + review_ad_extension.Status=None + review_ad_extension.Id=REVIEW_AD_EXTENSION_ID_KEY + bulk_review_ad_extension.review_ad_extension=review_ad_extension + + bulk_campaign_review_ad_extension=BulkCampaignReviewAdExtension() + review_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + review_ad_extension_id_to_entity_id_association.AdExtensionId=REVIEW_AD_EXTENSION_ID_KEY + review_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_review_ad_extension.ad_extension_id_to_entity_id_association=review_ad_extension_id_to_entity_id_association + + bulk_structured_snippet_ad_extension=BulkStructuredSnippetAdExtension() + bulk_structured_snippet_ad_extension.account_id=authorization_data.account_id + structured_snippet_ad_extension=set_elements_to_none(campaign_service.factory.create('StructuredSnippetAdExtension')) + structured_snippet_ad_extension.Header = "Brands" + values=campaign_service.factory.create('ns4:ArrayOfstring') + values.string.append('Windows') + values.string.append('Xbox') + values.string.append('Skype') + structured_snippet_ad_extension.Values=values + structured_snippet_ad_extension.Id=STRUCTURED_SNIPPET_AD_EXTENSION_ID_KEY + bulk_structured_snippet_ad_extension.structured_snippet_ad_extension=structured_snippet_ad_extension + + bulk_campaign_structured_snippet_ad_extension=BulkCampaignStructuredSnippetAdExtension() + structured_snippet_ad_extension_id_to_entity_id_association=campaign_service.factory.create('AdExtensionIdToEntityIdAssociation') + structured_snippet_ad_extension_id_to_entity_id_association.AdExtensionId=STRUCTURED_SNIPPET_AD_EXTENSION_ID_KEY + structured_snippet_ad_extension_id_to_entity_id_association.EntityId=CAMPAIGN_ID_KEY + bulk_campaign_structured_snippet_ad_extension.ad_extension_id_to_entity_id_association=structured_snippet_ad_extension_id_to_entity_id_association + + + # Upload the entities created above. + # Dependent entities such as BulkCampaignCallAdExtension must be written after any dependencies, + # for example the BulkCampaign and BulkCallAdExtension. + + upload_entities=[] + upload_entities.append(bulk_campaign) + upload_entities.append(bulk_app_ad_extension) + upload_entities.append(bulk_campaign_app_ad_extension) + upload_entities.append(bulk_call_ad_extension) + upload_entities.append(bulk_campaign_call_ad_extension) + upload_entities.append(bulk_callout_ad_extension) + upload_entities.append(bulk_campaign_callout_ad_extension) + upload_entities.append(bulk_location_ad_extension) + upload_entities.append(bulk_campaign_location_ad_extension) + upload_entities.append(bulk_review_ad_extension) + upload_entities.append(bulk_campaign_review_ad_extension) + upload_entities.append(bulk_structured_snippet_ad_extension) + upload_entities.append(bulk_campaign_structured_snippet_ad_extension) + + if sitelink_migration_is_completed: + for entity in get_sample_bulk_sitelink2_ad_extensions(authorization_data.account_id): + upload_entities.append(entity) + else: + for entity in get_sample_bulk_site_links_ad_extensions(authorization_data.account_id): + upload_entities.append(entity) + + output_status_message("\nAdding campaign, ad extensions, and associations . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + campaign_results=[] + app_ad_extension_results=[] + call_ad_extension_results=[] + callout_ad_extension_results=[] + location_ad_extension_results=[] + review_ad_extension_results=[] + site_link_ad_extension_results=[] + sitelink2_ad_extension_results=[] + structured_snippet_ad_extension_results=[] + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + campaign_results.append(entity) + output_bulk_campaigns([entity]) + if isinstance(entity, BulkAppAdExtension): + app_ad_extension_results.append(entity) + output_bulk_app_ad_extensions([entity]) + if isinstance(entity, BulkCampaignAppAdExtension): + output_bulk_campaign_app_ad_extensions([entity]) + if isinstance(entity, BulkCallAdExtension): + call_ad_extension_results.append(entity) + output_bulk_call_ad_extensions([entity]) + if isinstance(entity, BulkCampaignCallAdExtension): + output_bulk_campaign_call_ad_extensions([entity]) + if isinstance(entity, BulkCalloutAdExtension): + callout_ad_extension_results.append(entity) + output_bulk_callout_ad_extensions([entity]) + if isinstance(entity, BulkCampaignCalloutAdExtension): + output_bulk_campaign_callout_ad_extensions([entity]) + if isinstance(entity, BulkLocationAdExtension): + location_ad_extension_results.append(entity) + output_bulk_location_ad_extensions([entity]) + if isinstance(entity, BulkCampaignLocationAdExtension): + output_bulk_campaign_location_ad_extensions([entity]) + if isinstance(entity, BulkReviewAdExtension): + review_ad_extension_results.append(entity) + output_bulk_review_ad_extensions([entity]) + if isinstance(entity, BulkCampaignReviewAdExtension): + output_bulk_campaign_review_ad_extensions([entity]) + if isinstance(entity, BulkSiteLinkAdExtension): + site_link_ad_extension_results.append(entity) + output_bulk_site_link_ad_extensions([entity]) + if isinstance(entity, BulkCampaignSiteLinkAdExtension): + output_bulk_campaign_site_link_ad_extensions([entity]) + if isinstance(entity, BulkSitelink2AdExtension): + sitelink2_ad_extension_results.append(entity) + output_bulk_sitelink2_ad_extensions([entity]) + if isinstance(entity, BulkCampaignSitelink2AdExtension): + output_bulk_campaign_sitelink2_ad_extensions([entity]) + if isinstance(entity, BulkStructuredSnippetAdExtension): + structured_snippet_ad_extension_results.append(entity) + output_bulk_structured_snippet_ad_extensions([entity]) + if isinstance(entity, BulkCampaignStructuredSnippetAdExtension): + output_bulk_campaign_structured_snippet_ad_extensions([entity]) + + + # Use only the location extension results and remove scheduling. + + upload_entities=[] + + for location_ad_extension_result in location_ad_extension_results: + if location_ad_extension_result.location_ad_extension.Id > 0: + # If you set the Scheduling element null, any existing scheduling set for the ad extension will remain unchanged. + # If you set this to any non-null Schedule object, you are effectively replacing existing scheduling + # for the ad extension. In this example, we will remove any existing scheduling by setting this element + # to an empty Schedule object. + location_ad_extension_result.location_ad_extension.Scheduling=set_elements_to_none(campaign_service.factory.create('Schedule')) + upload_entities.append(location_ad_extension_result); + + # Upload and write the output + + output_status_message("\nRemoving scheduling from location ad extensions . . .\n") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkLocationAdExtension): + output_bulk_location_ad_extensions([entity]) + + # Delete the campaign and ad extensions that were previously added. + # You should remove this region if you want to view the added entities in the + # Bing Ads web application or another tool. + + # You must set the Id field to the corresponding entity identifier, and the Status field to Deleted. + + # When you delete a BulkCampaign or BulkCallAdExtension, dependent entities such as BulkCampaignCallAdExtension + # are deleted without being specified explicitly. + + upload_entities=[] + + for campaign_result in campaign_results: + campaign_result.campaign.Status='Deleted' + upload_entities.append(campaign_result) + + for app_ad_extension_result in app_ad_extension_results: + app_ad_extension_result.app_ad_extension.Status='Deleted' + # By default the sample does not successfully create any app ad extensions, + # because you need to provide details such as the AppStoreId. + # You can uncomment the following line if you added an app ad extension above. + # upload_entities.append(app_ad_extension_result) + + for call_ad_extension_result in call_ad_extension_results: + call_ad_extension_result.call_ad_extension.Status='Deleted' + upload_entities.append(call_ad_extension_result) + + for callout_ad_extension_result in callout_ad_extension_results: + callout_ad_extension_result.callout_ad_extension.Status='Deleted' + upload_entities.append(callout_ad_extension_result) + + for location_ad_extension_result in location_ad_extension_results: + location_ad_extension_result.location_ad_extension.Status='Deleted' + upload_entities.append(location_ad_extension_result) + + for review_ad_extension_result in review_ad_extension_results: + review_ad_extension_result.review_ad_extension.Status='Deleted' + upload_entities.append(review_ad_extension_result) + + for site_link_ad_extension_result in site_link_ad_extension_results: + site_link_ad_extension_result.site_links_ad_extension.Status='Deleted' + upload_entities.append(site_link_ad_extension_result) + + for sitelink2_ad_extension_result in sitelink2_ad_extension_results: + sitelink2_ad_extension_result.sitelink2_ad_extension.Status='Deleted' + upload_entities.append(sitelink2_ad_extension_result) + + for structured_snippet_ad_extension_result in structured_snippet_ad_extension_results: + structured_snippet_ad_extension_result.structured_snippet_ad_extension.Status='Deleted' + upload_entities.append(structured_snippet_ad_extension_result) + + output_status_message("\nDeleting campaign and ad extensions . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + output_bulk_campaigns([entity]) + if isinstance(entity, BulkAppAdExtension): + output_bulk_app_ad_extensions([entity]) + if isinstance(entity, BulkCallAdExtension): + output_bulk_call_ad_extensions([entity]) + if isinstance(entity, BulkCalloutAdExtension): + output_bulk_callout_ad_extensions([entity]) + if isinstance(entity, BulkLocationAdExtension): + output_bulk_location_ad_extensions([entity]) + if isinstance(entity, BulkReviewAdExtension): + output_bulk_review_ad_extensions([entity]) + if isinstance(entity, BulkSiteLinkAdExtension): + output_bulk_site_link_ad_extensions([entity]) + if isinstance(entity, BulkSitelink2AdExtension): + output_bulk_sitelink2_ad_extensions([entity]) + if isinstance(entity, BulkStructuredSnippetAdExtension): + output_bulk_structured_snippet_ad_extensions([entity]) + + 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/BulkKeywordBidUpdate.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py index cd914f8f..1283bac1 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py @@ -135,13 +135,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py.bak new file mode 100644 index 00000000..cd914f8f --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordBidUpdate.py.bak @@ -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/BulkKeywordsAds.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordsAds.py index 305fb50c..d4d66196 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordsAds.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordsAds.py @@ -140,13 +140,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordsAds.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordsAds.py.bak new file mode 100644 index 00000000..305fb50c --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkKeywordsAds.py.bak @@ -0,0 +1,1046 @@ +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 +from decimal import Decimal + +# 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' + + BUDGET_ID_KEY=-20 + CAMPAIGN_ID_KEY=-123 + AD_GROUP_ID_KEY=-1234 + + # 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_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 output_bulk_budgets(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkBudget: \n") + output_status_message("AccountId: {0}".format(entity.account_id)) + 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_status_message("Status: {0}".format(entity.status)) + + # Output the Campaign Management Budget Object + output_budget(entity.budget) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_budget(budget): + if budget is not None: + output_status_message("Amount: {0}".format(budget.Amount)) + output_status_message("AssociationCount: {0}".format(budget.AssociationCount)) + output_status_message("BudgetType: {0}".format(budget.BudgetType)) + output_status_message("Id: {0}".format(budget.Id)) + output_status_message("Name: {0}\n".format(budget.Name)) + +def output_bulk_campaigns(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaign: \n") + output_status_message("AccountId: {0}".format(entity.account_id)) + 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 the Campaign Management Campaign Object + output_campaign(entity.campaign) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_campaign(campaign): + if campaign is not None: + if hasattr(campaign, 'BiddingScheme'): + output_bidding_scheme(campaign.BiddingScheme) + if hasattr(campaign, 'BudgetId'): + output_status_message("BudgetId: {0}".format(campaign.BudgetId)) + output_status_message("BudgetType: {0}".format(campaign.BudgetType)) + if campaign.CampaignType is not None: + for campaign_type in campaign.CampaignType: + output_status_message("CampaignType: {0}".format(campaign_type)) + else: + output_status_message("CampaignType: None") + output_status_message("DailyBudget: {0}".format(campaign.DailyBudget)) + output_status_message("Description: {0}".format(campaign.Description)) + output_status_message("ForwardCompatibilityMap: ") + if campaign.ForwardCompatibilityMap is not None and len(campaign.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap['KeyValuePairOfstringstring']: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(campaign.Id)) + output_status_message("Name: {0}".format(campaign.Name)) + output_status_message("NativeBidAdjustment: {0}".format(campaign.NativeBidAdjustment)) + output_status_message("Settings: ") + for setting in campaign.Settings.Setting: + if setting.Type == 'ShoppingSetting': + output_status_message("\tShoppingSetting: ") + output_status_message("\t\tPriority: {0}".format(setting.Priority)) + output_status_message("\t\tSalesCountryCode: {0}".format(setting.SalesCountryCode)) + output_status_message("\t\tStoreId: {0}".format(setting.StoreId)) + output_status_message("Status: {0}".format(campaign.Status)) + output_status_message("TimeZone: {0}".format(campaign.TimeZone)) + output_status_message("TrackingUrlTemplate: {0}".format(campaign.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if campaign.UrlCustomParameters is not None and campaign.UrlCustomParameters.Parameters is not None: + for custom_parameter in campaign.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_bulk_ad_groups(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAdGroup: \n") + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("ClientId: {0}".format(entity.client_id)) + output_status_message("IsExpired: {0}".format(entity.is_expired)) + + 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 the Campaign Management AdGroup Object + output_ad_group(entity.ad_group) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_group(ad_group): + if ad_group is not None: + output_status_message("AdDistribution: {0}".format(ad_group.AdDistribution)) + output_status_message("AdRotation: {0}".format( + ad_group.AdRotation.Type if ad_group.AdRotation is not None else None) + ) + output_status_message("BiddingModel: {0}".format(ad_group.BiddingModel)) + output_bidding_scheme(ad_group.BiddingScheme) + output_status_message("ForwardCompatibilityMap: ") + if ad_group.ForwardCompatibilityMap is not None and len(ad_group.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in ad_group.ForwardCompatibilityMap['KeyValuePairOfstringstring']: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(ad_group.Id)) + output_status_message("Language: {0}".format(ad_group.Language)) + output_status_message("Name: {0}".format(ad_group.Name)) + output_status_message("NativeBidAdjustment: {0}".format(ad_group.NativeBidAdjustment)) + output_status_message("Network: {0}".format(ad_group.Network)) + output_status_message("PricingModel: {0}".format(ad_group.PricingModel)) + output_status_message("RemarketingTargetingSetting: {0}".format(ad_group.RemarketingTargetingSetting)) + output_status_message("SearchBid: {0}".format( + ad_group.SearchBid.Amount if ad_group.SearchBid is not None else None) + ) + output_status_message("Status: {0}".format(ad_group.Status)) + output_status_message("TrackingUrlTemplate: {0}".format(ad_group.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if ad_group.UrlCustomParameters is not None and ad_group.UrlCustomParameters.Parameters is not None: + for custom_parameter in ad_group.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_bulk_expanded_text_ads(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkExpandedTextAd: \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 the Campaign Management ExpandedTextAd Object + output_expanded_text_ad(entity.expanded_text_ad) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad(ad): + if ad is not None: + output_status_message("DevicePreference: {0}".format(ad.DevicePreference)) + output_status_message("EditorialStatus: {0}".format(ad.EditorialStatus)) + output_status_message("FinalMobileUrls: ") + if ad.FinalMobileUrls is not None: + for final_mobile_url in ad.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if ad.FinalUrls is not None: + for final_url in ad.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("ForwardCompatibilityMap: ") + if ad.ForwardCompatibilityMap is not None and len(ad.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in 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(ad.Id)) + output_status_message("Status: {0}".format(ad.Status)) + output_status_message("TrackingUrlTemplate: {0}".format(ad.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if ad.UrlCustomParameters is not None and ad.UrlCustomParameters.Parameters is not None: + for custom_parameter in ad.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_expanded_text_ad(ad): + if ad is not None: + # Output inherited properties of the Ad base class. + output_ad(ad) + + # Output properties that are specific to the ExpandedTextAd + output_status_message("DisplayUrl: {0}".format(ad.DisplayUrl)) + output_status_message("Path1: {0}".format(ad.Path1)) + output_status_message("Path2: {0}".format(ad.Path2)) + output_status_message("Text: {0}".format(ad.Text)) + output_status_message("TitlePart1: {0}".format(ad.TitlePart1)) + output_status_message("TitlePart2: {0}".format(ad.TitlePart2)) + +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 keyword.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_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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +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_id=None + 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 + + # Let's create a new budget and share it with a new campaign. + + upload_entities=[] + + bulk_budget=BulkBudget() + bulk_budget.client_id='YourClientIdGoesHere' + budget=set_elements_to_none(campaign_service.factory.create('Budget')) + budget.Amount=50 + budget.BudgetType='DailyBudgetStandard' + budget.Id=BUDGET_ID_KEY + budget.Name="My Shared Budget " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + bulk_budget.budget=budget + upload_entities.append(bulk_budget) + + + bulk_campaign=BulkCampaign() + + # The client_id may be used to associate records in the bulk upload file with records in the results file. The value of this field + # is not used or stored by the server; it is simply copied from the uploaded record to the corresponding result record. + # Note: This bulk file Client Id is not related to an application Client Id for OAuth. + + bulk_campaign.client_id='YourClientIdGoesHere' + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + + # When using the Campaign Management service, the Id cannot be set. In the context of a BulkCampaign, the Id is optional + # and may be used as a negative reference key during bulk upload. For example the same negative reference key for the campaign Id + # will be used when adding new ad groups to this new campaign, or when associating ad extensions with the campaign. + + campaign.Id=CAMPAIGN_ID_KEY + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + + # You must choose to set either the shared budget ID or daily amount. + # You can set one or the other, but you may not set both. + campaign.BudgetId=BUDGET_ID_KEY + campaign.DailyBudget=None + campaign.BudgetType=None + + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.Status='Paused' + + # DaylightSaving is not supported in the Bulk file schema. Whether or not you specify it in a BulkCampaign, + # the value is not written to the Bulk file, and by default DaylightSaving is set to true. + campaign.DaylightSaving='True' + + # You can set your campaign bid strategy to Enhanced CPC (EnhancedCpcBiddingScheme) + # and then, at any time, set an individual ad group or keyword bid strategy to + # Manual CPC (ManualCpcBiddingScheme). + # For campaigns you can use either of the EnhancedCpcBiddingScheme or ManualCpcBiddingScheme objects. + # If you do not set this element, then ManualCpcBiddingScheme is used by default. + campaign_bidding_scheme=set_elements_to_none(campaign_service.factory.create('ns0:EnhancedCpcBiddingScheme')) + campaign.BiddingScheme=campaign_bidding_scheme + + # Used with FinalUrls shown in the expanded text ads that we will add below. + campaign.TrackingUrlTemplate="http://tracker.example.com/?season={_season}&promocode={_promocode}&u={lpurl}" + + bulk_campaign.campaign=campaign + + bulk_ad_group=BulkAdGroup() + bulk_ad_group.campaign_id=CAMPAIGN_ID_KEY + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + ad_group.Id=AD_GROUP_ID_KEY + ad_group.Name="Women's Red Shoes" + ad_group.AdDistribution='Search' + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + search_bid=campaign_service.factory.create('Bid') + search_bid.Amount=0.09 + ad_group.SearchBid=search_bid + ad_group.Language='English' + + # For ad groups you can use either of the InheritFromParentBiddingScheme or ManualCpcBiddingScheme objects. + # If you do not set this element, then InheritFromParentBiddingScheme is used by default. + ad_group_bidding_scheme=set_elements_to_none(campaign_service.factory.create('ns0:ManualCpcBiddingScheme')) + ad_group.BiddingScheme=ad_group_bidding_scheme + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + ad_group.TrackingUrlTemplate=None + + bulk_ad_group.ad_group=ad_group + + # In this example only the first 3 ads should succeed. + # The Title of the fourth ad is empty and not valid, + # and the fifth ad is a duplicate of the second ad + + bulk_expanded_text_ads=[] + + for index in range(5): + bulk_expanded_text_ad=BulkExpandedTextAd() + bulk_expanded_text_ad.ad_group_id=AD_GROUP_ID_KEY + expanded_text_ad=set_elements_to_none(campaign_service.factory.create('ExpandedTextAd')) + expanded_text_ad.TitlePart1='Contoso' + expanded_text_ad.TitlePart2='Fast & Easy Setup' + expanded_text_ad.Text='Huge Savings on red shoes.' + expanded_text_ad.Path1='seattle' + expanded_text_ad.Path2='shoe sale' + expanded_text_ad.Type='ExpandedText' + expanded_text_ad.Status=None + expanded_text_ad.EditorialStatus=None + + # With FinalUrls you can separate the tracking template, custom parameters, and + # landing page URLs. + final_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_urls.string.append('http://www.contoso.com/womenshoesale') + expanded_text_ad.FinalUrls=final_urls + + # Final Mobile URLs can also be used if you want to direct the user to a different page + # for mobile devices. + final_mobile_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_mobile_urls.string.append('http://mobile.contoso.com/womenshoesale') + expanded_text_ad.FinalMobileUrls=final_mobile_urls + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + expanded_text_ad.TrackingUrlTemplate=None + + # Set custom parameters that are specific to this ad, + # and can be used by the ad, ad group, campaign, or account level tracking template. + # In this example we are using the campaign level tracking template. + url_custom_parameters=campaign_service.factory.create('ns0:CustomParameters') + parameters=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter1=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter1.Key='promoCode' + custom_parameter1.Value='PROMO' + str(index) + parameters.CustomParameter.append(custom_parameter1) + custom_parameter2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter2.Key='season' + custom_parameter2.Value='summer' + parameters.CustomParameter.append(custom_parameter2) + url_custom_parameters.Parameters=parameters + expanded_text_ad.UrlCustomParameters=url_custom_parameters + bulk_expanded_text_ad.ad=expanded_text_ad + bulk_expanded_text_ads.append(bulk_expanded_text_ad) + + bulk_expanded_text_ads[1].ad.Title="Quick & Easy Setup" + bulk_expanded_text_ads[2].ad.Title="Fast & Simple Setup" + bulk_expanded_text_ads[3].ad.Title='' + bulk_expanded_text_ads[4].ad.Title="Quick & Easy Setup" + + # In this example only the second keyword should succeed. The Text of the first keyword exceeds the limit, + # and the third keyword is a duplicate of the second keyword. + + bulk_keywords=[] + + for index in range(3): + bulk_keyword=BulkKeyword() + bulk_keyword.ad_group_id=AD_GROUP_ID_KEY + keyword=set_elements_to_none(campaign_service.factory.create('Keyword')) + keyword.Bid=set_elements_to_none(campaign_service.factory.create('Bid')) + keyword.Bid.Amount=0.47 + keyword.Param2='10% Off' + keyword.MatchType='Broad' + keyword.Text='Brand-A Shoes' + + # For keywords you can use either of the InheritFromParentBiddingScheme or ManualCpcBiddingScheme objects. + # If you do not set this element, then InheritFromParentBiddingScheme is used by default. + keyword_bidding_scheme=set_elements_to_none(campaign_service.factory.create('ns0:InheritFromParentBiddingScheme')) + keyword.BiddingScheme=keyword_bidding_scheme + + bulk_keyword.keyword=keyword + bulk_keywords.append(bulk_keyword) + + bulk_keywords[0].keyword.Text=( + "Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes " + "Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes " + "Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes" + ) + + # Write the entities created above, to temporary memory. + # Dependent entities such as BulkKeyword must be written after any dependencies, + # for example the BulkCampaign and BulkAdGroup. + + upload_entities.append(bulk_campaign) + upload_entities.append(bulk_ad_group) + for bulk_expanded_text_ad in bulk_expanded_text_ads: + upload_entities.append(bulk_expanded_text_ad) + for bulk_keyword in bulk_keywords: + upload_entities.append(bulk_keyword) + + output_status_message("\nAdding campaign, budget, ad group, keywords, and ads . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + budget_results=[] + campaign_results=[] + adgroup_results=[] + keyword_results=[] + + for entity in download_entities: + if isinstance(entity, BulkBudget): + budget_results.append(entity) + output_bulk_budgets([entity]) + if isinstance(entity, BulkCampaign): + campaign_results.append(entity) + output_bulk_campaigns([entity]) + if isinstance(entity, BulkAdGroup): + adgroup_results.append(entity) + output_bulk_ad_groups([entity]) + if isinstance(entity, BulkExpandedTextAd): + output_bulk_expanded_text_ads([entity]) + if isinstance(entity, BulkKeyword): + keyword_results.append(entity) + output_bulk_keywords([entity]) + + + # Here is a simple example that updates the keyword bid to use the ad group bid. + + update_bulk_keyword=BulkKeyword() + update_bulk_keyword.ad_group_id=adgroup_results[0].ad_group.Id + update_keyword=campaign_service.factory.create('Keyword') + + update_keyword.Id=next((keyword_result.keyword.Id for keyword_result in keyword_results if + keyword_result.keyword.Id is not None and keyword_result.ad_group_id==update_bulk_keyword.ad_group_id), "None") + + # You can set the Bid.Amount property to change the keyword level bid. + update_keyword.Bid=campaign_service.factory.create('Bid') + update_keyword.Bid.Amount=0.46 + + # The keyword bid will not be updated if the Bid property is not specified or if you create + # an empty Bid. + #update_keyword.Bid=campaign_service.factory.create('Bid') + + # The keyword level bid will be deleted ("delete_value" will be written in the bulk upload file), and + # the keyword will effectively inherit the ad group level bid if you explicitly set the Bid property to None. + #update_keyword.Bid=None + + # It is important to note that the above behavior differs from the Bid settings that + # are used to update keywords with the Campaign Management servivce. + # When using the Campaign Management service with the Bing Ads Python SDK, if the + # Bid property is not specified or is set explicitly to None, your keyword bid will not be updated. + # For examples of how to use the Campaign Management service for keyword updates, please see KeywordsAds.py. + + update_bulk_keyword.keyword=update_keyword + + upload_entities=[] + + upload_entities.append(update_bulk_keyword) + + output_status_message("\nUpdating the keyword bid to use the ad group bid . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkKeyword): + output_bulk_keywords([entity]) + + # Here is a simple example that updates the campaign budget. + + download_parameters=DownloadParameters( + entities=['Budgets Campaigns'], + result_file_directory=FILE_DIRECTORY, + result_file_name=DOWNLOAD_FILE_NAME, + overwrite_result_file=True, + last_sync_time_in_utc=None + ) + + upload_entities=[] + get_budget_results=[] + get_campaign_results=[] + + # Download all campaigns and shared budgets in the account. + download_entities=download_file(download_parameters) + output_status_message("Downloaded all campaigns and shared budgets in the account.\n"); + for entity in download_entities: + if isinstance(entity, BulkBudget): + get_budget_results.append(entity) + output_bulk_budgets([entity]) + if isinstance(entity, BulkCampaign): + get_campaign_results.append(entity) + output_bulk_campaigns([entity]) + + # If the campaign has a shared budget you cannot update the Campaign budget amount, + # and you must instead update the amount in the Budget record. If you try to update + # the budget amount of a Campaign that has a shared budget, the service will return + # the CampaignServiceCannotUpdateSharedBudget error code. + for entity in get_budget_results: + if entity.budget.Id > 0: + # Increase budget by 20 % + entity.budget.Amount *= Decimal(1.2) + upload_entities.append(entity) + + for entity in get_campaign_results: + if entity.campaign.BudgetId == None or entity.campaign.BudgetId <= 0: + # Increase budget by 20 % + entity.campaign.DailyBudget *= 1.2 + + upload_entities.append(entity) + + if len(upload_entities) > 0: + output_status_message("Changed local campaign budget amounts. Starting upload.\n") + + download_entities=write_entities_and_upload_file(upload_entities) + for entity in download_entities: + if isinstance(entity, BulkBudget): + get_budget_results.append(entity) + output_bulk_budgets([entity]) + if isinstance(entity, BulkCampaign): + get_campaign_results.append(entity) + output_bulk_campaigns([entity]) + else: + output_status_message("No campaigns or shared budgets in account.\n") + + # Delete the campaign, ad group, keywords, and ads that were previously added. + # You should remove this region if you want to view the added entities in the + # Bing Ads web application or another tool. + + upload_entities=[] + + for budget_result in budget_results: + budget_result.status='Deleted' + upload_entities.append(budget_result) + + for campaign_result in campaign_results: + campaign_result.campaign.Status='Deleted' + upload_entities.append(campaign_result) + + output_status_message("\nDeleting campaign, budget, ad group, ads, and keywords . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkBudget): + output_bulk_budgets([entity]) + if isinstance(entity, BulkCampaign): + output_bulk_campaigns([entity]) + + 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/BulkProductPartitionBidUpdate.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkProductPartitionBidUpdate.py index d58579e6..878725c3 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkProductPartitionBidUpdate.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkProductPartitionBidUpdate.py @@ -135,13 +135,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkProductPartitionBidUpdate.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkProductPartitionBidUpdate.py.bak new file mode 100644 index 00000000..d58579e6 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkProductPartitionBidUpdate.py.bak @@ -0,0 +1,482 @@ +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_ad_group_product_partitions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAdGroupProductPartition: \n") + output_status_message("CampaignName: {0}".format(entity.campaign_name)) + output_status_message("AdGroupName: {0}".format(entity.ad_group_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 the Campaign Management AdGroupCriterion and ProductPartition Objects + output_ad_group_criterion_with_product_partition(entity.ad_group_criterion) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_group_criterion_with_product_partition(ad_group_criterion): + if ad_group_criterion is not None: + output_status_message("AdGroupId: {0}".format(ad_group_criterion.AdGroupId)) + output_status_message("AdGroupCriterion Id: {0}".format(ad_group_criterion.Id)) + output_status_message("AdGroupCriterion Type: {0}".format(ad_group_criterion.Type)) + + if ad_group_criterion.Type == 'BiddableAdGroupCriterion': + output_status_message("DestinationUrl: {0}".format(ad_group_criterion.DestinationUrl)) + + # Output the Campaign Management FixedBid Object + output_fixed_bid(ad_group_criterion.CriterionBid) + + # Output the Campaign Management ProductPartition Object + output_product_partition(ad_group_criterion.Criterion) + +def output_fixed_bid(fixed_bid): + if fixed_bid is not None and fixed_bid.Bid is not None: + output_status_message("Bid.Amount: {0}".format(fixed_bid.Bid.Amount)) + +def output_product_partition(product_partition): + if product_partition is not None: + output_status_message("ParentCriterionId: {0}".format(product_partition.ParentCriterionId)) + output_status_message("PartitionType: {0}".format(product_partition.PartitionType)) + if product_partition.Condition is not None: + output_status_message("Condition: ") + output_status_message("Operand: {0}".format(product_partition.Condition.Operand)) + output_status_message("Attribute: {0}".format(product_partition.Condition.Attribute)) + +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=['AdGroupProductPartitions'], + result_file_directory=FILE_DIRECTORY, + result_file_name=DOWNLOAD_FILE_NAME, + overwrite_result_file=True, + last_sync_time_in_utc=None + ) + + # Download all product partitions across all ad groups in the account. + download_entities=download_file(download_parameters) + output_status_message("Downloaded all product partitions across all ad groups in the account.\n"); + for entity in download_entities: + if isinstance(entity, BulkAdGroupProductPartition): + output_bulk_ad_group_product_partitions([entity]) + + + upload_entities=[] + + # Within the downloaded records, find all product partition leaf nodes that have bids. + for entity in download_entities: + if isinstance(entity, BulkAdGroupProductPartition) \ + and entity.ad_group_criterion.Type == 'BiddableAdGroupCriterion' \ + and entity.ad_group_criterion.Criterion.PartitionType == 'Unit': + # Increase all bids by some predetermined amount or percentage. + # Implement your own logic to update bids by varying amounts. + entity.ad_group_criterion.CriterionBid.Bid.Amount += 0.01 + upload_entities.append(entity) + + if upload_entities.count > 0: + output_status_message("Changed local bid of all product partitions. Starting upload.\n") + + download_entities=write_entities_and_upload_file(upload_entities) + for entity in download_entities: + if isinstance(entity, BulkAdGroupProductPartition): + output_bulk_ad_group_product_partitions([entity]) + else: + output_status_message("No product partitions 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/BulkRemarketingLists.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkRemarketingLists.py index cba4a66b..3fe233ce 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkRemarketingLists.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkRemarketingLists.py @@ -132,13 +132,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkRemarketingLists.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkRemarketingLists.py.bak new file mode 100644 index 00000000..cba4a66b --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkRemarketingLists.py.bak @@ -0,0 +1,756 @@ +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 + +# The length of the Remarketing Rule field can be large. You may need to adjust the +# field size limit to avoid the field larger than field limit csv error. + +import csv +csv.field_size_limit(sys.maxsize) + +# 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' + + CAMPAIGN_ID_KEY=-123 + AD_GROUP_ID_KEY=-1234 + + # 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, + ) + + 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_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_campaigns(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaign: \n") + output_status_message("AccountId: {0}".format(entity.account_id)) + 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 the Campaign Management Campaign Object + output_campaign(entity.campaign) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_campaign(campaign): + if campaign is not None: + output_bidding_scheme(campaign.BiddingScheme) + output_status_message("BudgetType: {0}".format(campaign.BudgetType)) + if campaign.CampaignType is not None: + for campaign_type in campaign.CampaignType: + output_status_message("CampaignType: {0}".format(campaign_type)) + else: + output_status_message("CampaignType: None") + output_status_message("DailyBudget: {0}".format(campaign.DailyBudget)) + output_status_message("Description: {0}".format(campaign.Description)) + output_status_message("ForwardCompatibilityMap: ") + if campaign.ForwardCompatibilityMap is not None and len(campaign.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(campaign.Id)) + output_status_message("Name: {0}".format(campaign.Name)) + output_status_message("NativeBidAdjustment: {0}".format(campaign.NativeBidAdjustment)) + output_status_message("Settings: ") + for setting in campaign.Settings.Setting: + if setting.Type == 'ShoppingSetting': + output_status_message("\tShoppingSetting: ") + output_status_message("\t\tPriority: {0}".format(setting.Priority)) + output_status_message("\t\tSalesCountryCode: {0}".format(setting.SalesCountryCode)) + output_status_message("\t\tStoreId: {0}".format(setting.StoreId)) + output_status_message("Status: {0}".format(campaign.Status)) + output_status_message("TimeZone: {0}".format(campaign.TimeZone)) + output_status_message("TrackingUrlTemplate: {0}".format(campaign.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if campaign.UrlCustomParameters is not None and campaign.UrlCustomParameters.Parameters is not None: + for custom_parameter in campaign.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_bulk_ad_groups(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAdGroup: \n") + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("ClientId: {0}".format(entity.client_id)) + output_status_message("IsExpired: {0}".format(entity.is_expired)) + + 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 the Campaign Management AdGroup Object + output_ad_group(entity.ad_group) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_group(ad_group): + if ad_group is not None: + output_status_message("AdDistribution: {0}".format(ad_group.AdDistribution)) + output_status_message("AdRotation: {0}".format( + ad_group.AdRotation.Type if ad_group.AdRotation is not None else None) + ) + output_status_message("BiddingModel: {0}".format(ad_group.BiddingModel)) + output_bidding_scheme(ad_group.BiddingScheme) + if ad_group.EndDate is not None: + output_status_message("EndDate: {0}/{1}/{2}".format( + ad_group.EndDate.Month, + ad_group.EndDate.Day, + ad_group.EndDate.Year)) + output_status_message("ForwardCompatibilityMap: ") + if ad_group.ForwardCompatibilityMap is not None and len(ad_group.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in ad_group.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(ad_group.Id)) + output_status_message("Language: {0}".format(ad_group.Language)) + output_status_message("Name: {0}".format(ad_group.Name)) + output_status_message("NativeBidAdjustment: {0}".format(ad_group.NativeBidAdjustment)) + output_status_message("Network: {0}".format(ad_group.Network)) + output_status_message("PricingModel: {0}".format(ad_group.PricingModel)) + output_status_message("RemarketingTargetingSetting: {0}".format(ad_group.RemarketingTargetingSetting)) + output_status_message("SearchBid: {0}".format( + ad_group.SearchBid.Amount if ad_group.SearchBid is not None else None) + ) + if ad_group.StartDate is not None: + output_status_message("StartDate: {0}/{1}/{2}".format( + ad_group.StartDate.Month, + ad_group.StartDate.Day, + ad_group.StartDate.Year)) + output_status_message("Status: {0}".format(ad_group.Status)) + output_status_message("TrackingUrlTemplate: {0}".format(ad_group.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if ad_group.UrlCustomParameters is not None and ad_group.UrlCustomParameters.Parameters is not None: + for custom_parameter in ad_group.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 not None: + output_status_message("BiddingScheme (Bid Strategy Type): {0}".format(bidding_scheme.Type)) + if bidding_scheme.Type == 'MaxClicksBiddingScheme': + output_status_message("\tMaxCpc: {0}".format(bidding_scheme.MaxCpc)) + elif bidding_scheme.Type == 'MaxConversionsBiddingScheme': + output_status_message("\tMaxCpc: {0}".format(bidding_scheme.MaxCpc)) + output_status_message("\tStartingBid: {0}".format(bidding_scheme.StartingBid)) + elif bidding_scheme.Type == 'TargetCpaBiddingScheme': + output_status_message("\tMaxCpc: {0}".format(bidding_scheme.MaxCpc)) + output_status_message("\tStartingBid: {0}".format(bidding_scheme.StartingBid)) + output_status_message("\tTargetCpa: {0}".format(bidding_scheme.TargetCpa)) + else: + output_status_message("Unknown bidding scheme") + +def output_bulk_remarketing_lists(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkRemarketingList: \n") + output_status_message("Status: {0}".format(entity.status)) + 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 the Campaign Management RemarketingList Object + output_remarketing_list(entity.remarketing_list) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_remarketing_list(remarketing_list): + if remarketing_list is not None: + output_status_message("Description: {0}".format(remarketing_list.Description)) + output_status_message("ForwardCompatibilityMap: ") + if remarketing_list.ForwardCompatibilityMap is not None and len(remarketing_list.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in remarketing_list.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(remarketing_list.Id)) + output_status_message("MembershipDuration: {0}".format(remarketing_list.MembershipDuration)) + output_status_message("Name: {0}".format(remarketing_list.Name)) + output_status_message("ParentId: {0}".format(remarketing_list.ParentId)) + output_status_message("Scope: {0}".format(remarketing_list.Scope)) + output_status_message("TagId: {0}".format(remarketing_list.TagId)) + +def output_bulk_ad_group_remarketing_lists(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAdGroupRemarketingListAssociation: \n") + 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 the Campaign Management AdGroupRemarketingListAssociation Object + output_ad_group_remarketing_list_association(entity.ad_group_remarketing_list_association) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_group_remarketing_list_association(ad_group_remarketing_list_association): + if ad_group_remarketing_list_association is not None: + output_status_message("AdGroupId: {0}".format(ad_group_remarketing_list_association.AdGroupId)) + output_status_message("BidAdjustment: {0}".format(ad_group_remarketing_list_association.BidAdjustment)) + output_status_message("Id: {0}".format(ad_group_remarketing_list_association.Id)) + output_status_message("RemarketingListId: {0}".format(ad_group_remarketing_list_association.RemarketingListId)) + output_status_message("Status: {0}".format(ad_group_remarketing_list_association.Status)) + +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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +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_id=None + 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=['RemarketingLists'], + result_file_directory=FILE_DIRECTORY, + result_file_name=DOWNLOAD_FILE_NAME, + overwrite_result_file=True, + last_sync_time_in_utc=None + ) + + download_entities=download_file(download_parameters) + remarketing_list_results=[] + output_status_message("Downloaded all remarketing lists that the current user can associate with ad groups.\n"); + for entity in download_entities: + if isinstance(entity, BulkRemarketingList): + remarketing_list_results.append(entity) + + output_bulk_remarketing_lists(remarketing_list_results) + + # You must already have at least one remarketing list. The Bing Ads API does not support + # remarketing list add, update, or delete operations. + if len(remarketing_list_results) < 1: + output_status_message("You do not have any remarketing lists that the current user can associate with ad groups.\n") + sys.exit(0) + + # Prepare the bulk entities that you want to upload. Each bulk entity contains the corresponding campaign management object, + # and additional elements needed to read from and write to a bulk file. + + bulk_campaign=BulkCampaign() + bulk_campaign.client_id='YourClientIdGoesHere' + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + campaign.Id=CAMPAIGN_ID_KEY + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.Status='Paused' + + # DaylightSaving is not supported in the Bulk file schema. Whether or not you specify it in a BulkCampaign, + # the value is not written to the Bulk file, and by default DaylightSaving is set to true. + campaign.DaylightSaving='True' + + bulk_campaign.campaign=campaign + + bulk_ad_group=BulkAdGroup() + + # The client_id may be used to associate records in the bulk upload file with records in the results file. The value of this field + # is not used or stored by the server; it is simply copied from the uploaded record to the corresponding result record. + # Note: This bulk file Client Id is not related to an application Client Id for OAuth. + + bulk_ad_group.client_id='YourClientIdGoesHere' + + bulk_ad_group.campaign_id=CAMPAIGN_ID_KEY + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + + # When using the Campaign Management service, the Id cannot be set. In the context of a BulkAdGroup, the Id is optional + # and may be used as a negative reference key during bulk upload. For example the same negative value set for the + # ad group Id will be used when associating this new ad group with a new ad group remarketing list association + # in the bulk_ad_group_remarketing_list object below. + ad_group.Id=AD_GROUP_ID_KEY + + ad_group.Name="Women's Red Shoes" + ad_group.AdDistribution='Search' + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + search_bid=campaign_service.factory.create('Bid') + search_bid.Amount=0.09 + ad_group.SearchBid=search_bid + ad_group.Language='English' + ad_group.TrackingUrlTemplate=None + + # Applicable for all remarketing lists that are associated with this ad group. TargetAndBid indicates + # that you want to show ads only to people included in the remarketing list, with the option to change + # the bid amount. Ads in this ad group will only show to people included in the remarketing list. + ad_group.RemarketingTargetingSetting='TargetAndBid' + + bulk_ad_group.ad_group=ad_group + + upload_entities=[] + upload_entities.append(bulk_campaign) + upload_entities.append(bulk_ad_group) + + # This example associates all of the remarketing lists with the new ad group. + + for bulk_remarketing_list in remarketing_list_results: + if bulk_remarketing_list.remarketing_list != None and bulk_remarketing_list.remarketing_list.Id != None: + bulk_ad_group_remarketing_list=BulkAdGroupRemarketingListAssociation() + bulk_ad_group_remarketing_list.ClientId = "MyBulkAdGroupRemarketingList " + str(bulk_remarketing_list.remarketing_list.Id) + ad_group_remarketing_list_association=set_elements_to_none(campaign_service.factory.create('AdGroupRemarketingListAssociation')) + ad_group_remarketing_list_association.AdGroupId=AD_GROUP_ID_KEY + ad_group_remarketing_list_association.BidAdjustment=90.00 + ad_group_remarketing_list_association.RemarketingListId=bulk_remarketing_list.remarketing_list.Id + ad_group_remarketing_list_association.Status='Paused' + bulk_ad_group_remarketing_list.ad_group_remarketing_list_association = ad_group_remarketing_list_association + + upload_entities.append(bulk_ad_group_remarketing_list) + + output_status_message("\nAdding campaign, ad group, and ad group remarketing list associations...\n") + download_entities=write_entities_and_upload_file(upload_entities) + + campaign_results=[] + ad_group_results=[] + ad_group_remarketing_list_results=[] + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + campaign_results.append(entity) + output_bulk_campaigns([entity]) + if isinstance(entity, BulkAdGroup): + ad_group_results.append(entity) + output_bulk_ad_groups([entity]) + if isinstance(entity, BulkAdGroupRemarketingListAssociation): + ad_group_remarketing_list_results.append([entity]) + output_bulk_ad_group_remarketing_lists([entity]) + + + # Delete the campaign, ad group, and ad group remarketing list associations that were previously added. + # The remarketing lists will not be deleted. + # You should remove this region if you want to view the added entities in the + # Bing Ads web application or another tool. + + upload_entities=[] + + for campaign_result in campaign_results: + campaign_result.campaign.Status='Deleted' + upload_entities.append(campaign_result) + + output_status_message("\nDeleting campaign, ad group, and ad group remarketing list associations . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + output_bulk_campaigns([entity]) + + 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/BulkServiceManagerDemo.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkServiceManagerDemo.py index 51b43f85..55b21fae 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkServiceManagerDemo.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkServiceManagerDemo.py @@ -148,13 +148,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkServiceManagerDemo.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkServiceManagerDemo.py.bak new file mode 100644 index 00000000..51b43f85 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkServiceManagerDemo.py.bak @@ -0,0 +1,493 @@ +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' + + # The maximum amount of time (in milliseconds) that you want to wait for the bulk download or upload. + TIMEOUT_IN_MILLISECONDS=3600000 + + 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_manager=BulkServiceManager( + authorization_data=authorization_data, + poll_interval_in_milliseconds=5000, + environment=ENVIRONMENT, + ) + + # In addition to BulkServiceManager, if you want to get performance data in the bulk download, + # then you need a Bulk ServiceClient to build the PerformanceStatsDateRange object. + + bulk_service=ServiceClient( + service='BulkService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=10, + ) + + 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_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 background_completion(download_parameters): + ''' + You can submit a download or upload request and the BulkServiceManager will automatically + return results. The BulkServiceManager abstracts the details of checking for result file + completion, and you don't have to write any code for results polling. + ''' + global bulk_service_manager + result_file_path = bulk_service_manager.download_file(download_parameters) + output_status_message("Download result file: {0}\n".format(result_file_path)) + +def submit_and_download(submit_download_parameters): + ''' + Submit the download request and then use the BulkDownloadOperation result to + track status until the download is complete e.g. either using + BulkDownloadOperation.track() or BulkDownloadOperation.get_status(). + ''' + global bulk_service_manager + bulk_download_operation = bulk_service_manager.submit_download(submit_download_parameters) + + # You may optionally cancel the track() operation after a specified time interval. + download_status = bulk_download_operation.track(timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS) + + # You can use BulkDownloadOperation.track() to poll until complete as shown above, + # or use custom polling logic with getStatus() as shown below. + #for i in range(10): + # time.sleep(bulk_service_manager.poll_interval_in_milliseconds / 1000.0) + + # download_status = bulk_download_operation.get_status() + + # if download_status.status == 'Completed': + # break + + result_file_path = bulk_download_operation.download_result_file( + result_file_directory = FILE_DIRECTORY, + result_file_name = DOWNLOAD_FILE_NAME, + decompress = True, + overwrite = True, # Set this value true if you want to overwrite the same file. + timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS # You may optionally cancel the download after a specified time interval. + ) + + output_status_message("Download result file: {0}\n".format(result_file_path)) + +def download_results(request_id, authorization_data): + ''' + If for any reason you have to resume from a previous application state, + you can use an existing download request identifier and use it + to download the result file. Use BulkDownloadOperation.track() to indicate that the application + should wait to ensure that the download status is completed. + ''' + bulk_download_operation = BulkDownloadOperation( + request_id = request_id, + authorization_data=authorization_data, + poll_interval_in_milliseconds=1000, + environment=ENVIRONMENT, + ) + + # Use track() to indicate that the application should wait to ensure that + # the download status is completed. + # You may optionally cancel the track() operation after a specified time interval. + bulk_operation_status = bulk_download_operation.track(timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS) + + result_file_path = bulk_download_operation.download_result_file( + result_file_directory = FILE_DIRECTORY, + result_file_name = DOWNLOAD_FILE_NAME, + decompress = True, + overwrite = True, # Set this value true if you want to overwrite the same file. + timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS # You may optionally cancel the download after a specified time interval. + ) + + output_status_message("Download result file: {0}".format(result_file_path)) + output_status_message("Status: {0}\n".format(bulk_operation_status.status)) + +# 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_id=None + 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 + + # In this example we will download all ads and keywords in the account. + entities=['Ads','Keywords'] + + # Optionally you can request performance data for the downloaded entities. + performance_stats_date_range=bulk_service.factory.create('PerformanceStatsDateRange') + performance_stats_date_range.PredefinedTime='LastFourWeeks' + + # DownloadParameters is used for Option A below. + download_parameters = DownloadParameters( + campaign_ids=None, + data_scope=['EntityData', 'EntityPerformanceData'], + performance_stats_date_range=performance_stats_date_range, + entities=entities, + file_type=FILE_TYPE, + last_sync_time_in_utc=None, + result_file_directory = FILE_DIRECTORY, + result_file_name = DOWNLOAD_FILE_NAME, + overwrite_result_file = True, # Set this value true if you want to overwrite the same file. + timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS # You may optionally cancel the download after a specified time interval. + ) + + # SubmitDownloadParameters is used for Option B and Option C below. + submit_download_parameters = SubmitDownloadParameters( + campaign_ids=None, + data_scope=['EntityData', 'EntityPerformanceData'], + performance_stats_date_range=performance_stats_date_range, + entities=entities, + file_type=FILE_TYPE, + last_sync_time_in_utc=None + ) + + #Option A - Background Completion with BulkServiceManager + #You can submit a download request and the BulkServiceManager will automatically + #return results. The BulkServiceManager abstracts the details of checking for result file + #completion, and you don't have to write any code for results polling. + + output_status_message("Awaiting Background Completion . . ."); + background_completion(download_parameters) + + #Option B - Submit and Download with BulkServiceManager + #Submit the download request and then use the BulkDownloadOperation result to + #track status yourself using BulkServiceManager.get_status(). + + output_status_message("Awaiting Submit and Download . . ."); + submit_and_download(submit_download_parameters) + + #Option C - Download Results with BulkServiceManager + #If for any reason you have to resume from a previous application state, + #you can use an existing download request identifier and use it + #to download the result file. Use track() to indicate that the application + #should wait to ensure that the download status is completed. + + #For example you might have previously retrieved a request ID using submit_download. + bulk_download_operation=bulk_service_manager.submit_download(submit_download_parameters); + request_id=bulk_download_operation.request_id; + + #Given the request ID above, you can resume the workflow and download the bulk file. + #The download request identifier is valid for two days. + #If you do not download the bulk file within two days, you must request it again. + output_status_message("Awaiting Download Results . . ."); + download_results(request_id, authorization_data) + + 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/BulkShoppingCampaigns.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkShoppingCampaigns.py index 90dd816c..92dc0713 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkShoppingCampaigns.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkShoppingCampaigns.py @@ -138,13 +138,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkShoppingCampaigns.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkShoppingCampaigns.py.bak new file mode 100644 index 00000000..90dd816c --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkShoppingCampaigns.py.bak @@ -0,0 +1,1419 @@ +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' + + CAMPAIGN_ID_KEY=-123 + AD_GROUP_ID_KEY=-1234 + + # 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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +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_campaigns(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaign: \n") + output_status_message("AccountId: {0}".format(entity.account_id)) + 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 the Campaign Management Campaign Object + output_campaign(entity.campaign) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_campaign(campaign): + if campaign is not None: + output_status_message("BudgetType: {0}".format(campaign.BudgetType)) + if campaign.CampaignType is not None: + for campaign_type in campaign.CampaignType: + output_status_message("CampaignType: {0}".format(campaign_type)) + else: + output_status_message("CampaignType: None") + output_status_message("DailyBudget: {0}".format(campaign.DailyBudget)) + output_status_message("Description: {0}".format(campaign.Description)) + output_status_message("ForwardCompatibilityMap: ") + if campaign.ForwardCompatibilityMap is not None and len(campaign.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(campaign.Id)) + output_status_message("Name: {0}".format(campaign.Name)) + output_status_message("NativeBidAdjustment: {0}".format(campaign.NativeBidAdjustment)) + output_status_message("Settings: ") + for setting in campaign.Settings.Setting: + if setting.Type == 'ShoppingSetting': + output_status_message("\tShoppingSetting: ") + output_status_message("\t\tPriority: {0}".format(setting.Priority)) + output_status_message("\t\tSalesCountryCode: {0}".format(setting.SalesCountryCode)) + output_status_message("\t\tStoreId: {0}".format(setting.StoreId)) + output_status_message("Status: {0}".format(campaign.Status)) + output_status_message("TimeZone: {0}".format(campaign.TimeZone)) + +def output_bulk_ad_groups(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAdGroup: \n") + output_status_message("CampaignId: {0}".format(entity.campaign_id)) + output_status_message("CampaignName: {0}".format(entity.campaign_name)) + output_status_message("ClientId: {0}".format(entity.client_id)) + output_status_message("IsExpired: {0}".format(entity.is_expired)) + 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 the Campaign Management AdGroup Object + output_ad_group(entity.ad_group) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_group(ad_group): + if ad_group is not None: + output_status_message("AdDistribution: {0}".format(ad_group.AdDistribution)) + output_status_message("AdRotation Type: {0}".format( + ad_group.AdRotation.Type if ad_group.AdRotation is not None else None)) + output_status_message("BiddingModel: {0}".format(ad_group.BiddingModel.value)) + if ad_group.EndDate is not None: + output_status_message("EndDate: {0}/{1}/{2}".format( + ad_group.EndDate.Month, + ad_group.EndDate.Day, + ad_group.EndDate.Year)) + output_status_message("ForwardCompatibilityMap: ") + if ad_group.ForwardCompatibilityMap is not None and len(ad_group.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(ad_group.Id)) + output_status_message("Language: {0}".format(ad_group.Language)) + output_status_message("Name: {0}".format(ad_group.Name)) + output_status_message("NativeBidAdjustment: {0}".format(ad_group.NativeBidAdjustment)) + output_status_message("Network: {0}".format(ad_group.Network)) + output_status_message("PricingModel: {0}".format(ad_group.PricingModel)) + output_status_message("SearchBid: {0}".format( + ad_group.SearchBid.Amount if ad_group.SearchBid is not None else None)) + if ad_group.StartDate is not None: + output_status_message("StartDate: {0}/{1}/{2}".format( + ad_group.StartDate.Month, + ad_group.StartDate.Day, + ad_group.StartDate.Year)) + output_status_message("Status: {0}".format(ad_group.Status)) + +def output_bulk_product_ads(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkProductAd: \n") + output_status_message("AdGroupId: {0}".format(entity.ad_group_id)) + output_status_message("AdGroupName: {0}".format(entity.ad_group_name)) + output_status_message("CampaignName: {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 the Campaign Management ProductAd Object + output_product_ad(entity.ad) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_product_ad(ad): + if ad is not None: + output_status_message("AdDistribution: {0}".format(ad.DevicePreference)) + output_status_message("BiddingModel: {0}".format(ad.EditorialStatus)) + output_status_message("ForwardCompatibilityMap: ") + if ad.ForwardCompatibilityMap is not None and len(ad.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(ad.Id)) + output_status_message("PromotionalText: {0}".format(ad.PromotionalText)) + output_status_message("Status: {0}".format(ad.Status)) + +def output_bulk_campaign_product_scopes(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignProductScope: \n") + output_status_message("CampaignName: {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 the Campaign Management CampaignCriterion and ProductScope Objects + output_campaign_criterion_with_product_scope(entity.campaign_criterion) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_campaign_criterion_with_product_scope(campaign_criterion): + if campaign_criterion is not None: + output_status_message("BidAdjustment: {0}".format(campaign_criterion.BidAdjustment)) + output_status_message("CampaignId: {0}".format(campaign_criterion.CampaignId)) + output_status_message("ForwardCompatibilityMap: ") + if campaign_criterion.ForwardCompatibilityMap is not None and len(campaign_criterion.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign_criterion.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("CampaignCriterion Id: {0}".format(campaign_criterion.Id)) + output_status_message("CampaignCriterion Type: {0}".format(campaign_criterion.Type)) + + # Output the Campaign Management ProductScope Object + output_product_scope(campaign_criterion.Criterion) + +def output_product_scope(product_scope): + if product_scope is not None: + output_status_message("Product Conditions: ") + if product_scope.Conditions is not None and len(product_scope.Conditions) > 0: + for condition in product_scope.Conditions.ProductCondition: + output_status_message("Operand: {0}".format(condition.Operand)) + output_status_message("Attribute: {0}".format(condition.Attribute)) + +def output_bulk_ad_group_product_partitions(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkAdGroupProductPartition: \n") + output_status_message("CampaignName: {0}".format(entity.campaign_name)) + output_status_message("AdGroupName: {0}".format(entity.ad_group_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 the Campaign Management AdGroupCriterion and ProductPartition Objects + output_ad_group_criterion_with_product_partition(entity.ad_group_criterion) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_ad_group_criterion_with_product_partition(ad_group_criterion): + if ad_group_criterion is not None: + output_status_message("AdGroupId: {0}".format(ad_group_criterion.AdGroupId)) + output_status_message("AdGroupCriterion Id: {0}".format(ad_group_criterion.Id)) + output_status_message("AdGroupCriterion Type: {0}".format(ad_group_criterion.Type)) + + if ad_group_criterion.Type == 'BiddableAdGroupCriterion': + output_status_message("DestinationUrl: {0}".format(ad_group_criterion.DestinationUrl)) + + # Output the Campaign Management FixedBid Object + output_fixed_bid(ad_group_criterion.CriterionBid) + + # Output the Campaign Management ProductPartition Object + output_product_partition(ad_group_criterion.Criterion) + +def output_fixed_bid(fixed_bid): + if fixed_bid is not None and fixed_bid.Bid is not None: + output_status_message("Bid.Amount: {0}".format(fixed_bid.Bid.Amount)) + +def output_product_partition(product_partition): + if product_partition is not None: + output_status_message("ParentCriterionId: {0}".format(product_partition.ParentCriterionId)) + output_status_message("PartitionType: {0}".format(product_partition.PartitionType)) + if product_partition.Condition is not None: + output_status_message("Condition: ") + output_status_message("Operand: {0}".format(product_partition.Condition.Operand)) + output_status_message("Attribute: {0}".format(product_partition.Condition.Attribute)) + +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 + +def apply_bulk_product_partition_actions(upload_entities): + download_entities=write_entities_and_upload_file(upload_entities) + + bulk_ad_group_product_partitions=[] + + for entity in download_entities: + if isinstance(entity, BulkAdGroupProductPartition): + bulk_ad_group_product_partitions.append(entity) + output_bulk_ad_group_product_partitions([entity]) + + return bulk_ad_group_product_partitions + +def get_bulk_ad_group_product_partition_tree(ad_group_id): + download_parameters=DownloadParameters( + entities=['AdGroupProductPartitions'], + result_file_directory=FILE_DIRECTORY, + result_file_name=DOWNLOAD_FILE_NAME, + overwrite_result_file=True, + last_sync_time_in_utc=None + ) + download_entities=download_file(download_parameters) + + bulk_ad_group_product_partitions=[] + + for entity in download_entities: + if isinstance(entity, BulkAdGroupProductPartition) and entity.ad_group_criterion is not None and entity.ad_group_criterion.AdGroupId == ad_group_id: + bulk_ad_group_product_partitions.append(entity) + + return bulk_ad_group_product_partitions + +def output_product_partitions(bulk_ad_group_product_partitions): + """ + Outputs the list of BulkAdGroupProductPartition which each contain an AdGroupCriterion, formatted as a tree. + Each AdGroupCriterion must be either a BiddableAdGroupCriterion or NegativeAdGroupCriterion. + + :param bulk_ad_group_product_partitions: The list of BulkAdGroupProductPartition to output formatted as a tree. + :type bulk_ad_group_product_partitions: BulkAdGroupProductPartition[] + + """ + + # Set up the tree for output + + child_branches={} + tree_root=None + + for bulk_ad_group_product_partition in bulk_ad_group_product_partitions: + ad_group_criterion=bulk_ad_group_product_partition.ad_group_criterion + partition=ad_group_criterion.Criterion + child_branches[ad_group_criterion.Id]=[] + + # The product partition with ParentCriterionId set to null is the root node. + if partition.ParentCriterionId is not None: + child_branches[partition.ParentCriterionId].append(bulk_ad_group_product_partition) + else: + tree_root=bulk_ad_group_product_partition + + # Outputs the tree root node and any children recursively + output_product_partition_tree(tree_root, child_branches, 0) + +def output_product_partition_tree(node, child_branches, tree_level): + """ + Outputs the details of the specified product partition node, + and passes any children to itself recursively. + + :param node: The node to output, whether a Subdivision or Unit. + :type node: BulkAdGroupProductPartition + :param child_branches: The child branches or nodes if any exist. + :type child_branches: dict{long, BulkAdGroupProductPartition[]} + :param tree_level: The number of descendents from the tree root node. + Used by this operation to format the tree structure output. + :type tree_level: int + + """ + + if node is None: + return + + ad_group_criterion=node.ad_group_criterion + + pad='' + for i in range(0, tree_level): + pad=pad + '\t' + output_status_message("{0}{1}".format( + pad, + ad_group_criterion.Criterion.PartitionType) + ) + + output_status_message("{0}ParentCriterionId: {1}".format( + pad, + ad_group_criterion.Criterion.ParentCriterionId) + ) + + output_status_message("{0}Id: {1}".format( + pad, + ad_group_criterion.Id) + ) + + if ad_group_criterion.Criterion.PartitionType == 'Unit': + if ad_group_criterion.Type == 'BiddableAdGroupCriterion': + output_status_message("{0}Bid Amount: {1}".format( + pad, + ad_group_criterion.CriterionBid.Bid.Amount) + ) + elif ad_group_criterion.Type == 'NegativeAdGroupCriterion': + output_status_message("{0}Not Bidding on this Condition".format( + pad) + ) + + + null_attribute="(All other)" if ad_group_criterion.Criterion.ParentCriterionId is not None else "(Tree Root)" + output_status_message("{0}Attribute: {1}".format( + pad, + null_attribute if ad_group_criterion.Criterion.Condition.Attribute is None else ad_group_criterion.Criterion.Condition.Attribute) + ) + + output_status_message("{0}Operand: {1}\n".format( + pad, + ad_group_criterion.Criterion.Condition.Operand) + ) + + for child_node in child_branches[ad_group_criterion.Id]: + output_product_partition_tree(child_node, child_branches, tree_level + 1) + +def get_node_by_client_id(product_partitions, client_id=None): + """ + Returns the root node of a tree. This operation assumes that a complete + product partition tree is provided for one ad group. The node that has + null ParentCriterionId is the root node. + + :param product_partitions: The list of BulkAdGroupProductPartition that make up the product partition tree. + :type product_partitions: BulkAdGroupProductPartition[] + :return: The BulkAdGroupProductPartition corresponding to the specified Client Id. + :rtype: BulkAdGroupProductPartition + + """ + + client_node=None + for product_partition in product_partitions: + if product_partition.client_id == client_id: + client_node=product_partition + break + + return client_node + + +class ProductPartitionHelper: + """ + Helper class used to maintain a list of product partition actions for an ad group. + The list of partition actions can be uploaded to the Bulk service. + """ + + def __init__(self, + ad_group_id): + """ + Initialize an instance of this class. + + :param ad_group_id: Each criterion is associated with the same ad group. + :type ad_group_id: long + + """ + + self._ad_group_id=ad_group_id + self._reference_id=-1 + self._partition_actions=[] + + @property + def partition_actions(self): + """ + The list of BulkAdGroupProductPartition that can be uploaded to the Bulk service + + :rtype: BulkAdGroupProductPartition[] + """ + + return self._partition_actions + + def add_subdivision(self, parent, condition, client_id=None): + """ + Sets the Add action for a new BiddableAdGroupCriterion corresponding to the specified ProductCondition, + and adds it to the helper's list of BulkAdGroupProductPartition. + + :param parent: The parent of the product partition subdivision that you want to add. + :type parent: BulkAdGroupProductPartition + :param condition: The condition or product filter for the new product partition. + :type condition: ProductCondition + :param client_id: The Client Id in the bulk upload file corresponding to the product partition. + :type client_id: string + :return: The BulkAdGroupProductPartition that was added to the list of partition_actions. + :rtype: BulkAdGroupProductPartition + """ + + biddable_ad_group_criterion=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + product_partition=set_elements_to_none(campaign_service.factory.create('ProductPartition')) + # If the root node is a unit, it would not have a parent + product_partition.ParentCriterionId=parent.ad_group_criterion.Id if parent is not None and parent.ad_group_criterion is not None else None + product_partition.Condition=condition + product_partition.PartitionType='Subdivision' + biddable_ad_group_criterion.Criterion=product_partition + biddable_ad_group_criterion.CriterionBid=None + biddable_ad_group_criterion.AdGroupId=self._ad_group_id + biddable_ad_group_criterion.Status=None + if hasattr(biddable_ad_group_criterion, 'EditorialStatus'): + biddable_ad_group_criterion.EditorialStatus=None + biddable_ad_group_criterion.Id=self._reference_id + self._reference_id=self._reference_id + self._reference_id-=1 + + partition_action=BulkAdGroupProductPartition() + partition_action.client_id=client_id + partition_action.ad_group_criterion=biddable_ad_group_criterion + self._partition_actions.append(partition_action) + + return partition_action + + def add_unit(self, parent, condition, bid_amount, is_negative=False, client_id=None): + """ + Sets the Add action for a new AdGroupCriterion corresponding to the specified ProductCondition, + and adds it to the helper's list of BulkAdGroupProductPartition. + + :param parent: The parent of the product partition unit that you want to add. + :type parent: BulkAdGroupProductPartition + :param condition: The condition or product filter for the new product partition. + :type condition: ProductCondition + :param bid_amount: The bid amount for the new product partition. + :type bid_amount: double + :param is_negative: (Optional) Indicates whether or not to add a NegativeAdGroupCriterion. + The default value is False, in which case a BiddableAdGroupCriterion will be added. + :type is_negative: bool + :param client_id: The Client Id in the bulk upload file corresponding to the product partition. + :type client_id: string + :return: The BulkAdGroupProductPartition that was added to the list of partition_actions. + :rtype: BulkAdGroupProductPartition + """ + + ad_group_criterion=None + if is_negative: + ad_group_criterion=set_elements_to_none(campaign_service.factory.create('NegativeAdGroupCriterion')) + else: + ad_group_criterion=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + fixed_bid=campaign_service.factory.create('FixedBid') + bid=campaign_service.factory.create('Bid') + bid.Amount=bid_amount + fixed_bid.Bid=bid + ad_group_criterion.CriterionBid=fixed_bid + + ad_group_criterion.AdGroupId=self._ad_group_id + if hasattr(ad_group_criterion, 'EditorialStatus'): + ad_group_criterion.EditorialStatus=None + ad_group_criterion.Status=None + + product_partition=set_elements_to_none(campaign_service.factory.create('ProductPartition')) + # If the root node is a unit, it would not have a parent + product_partition.ParentCriterionId=parent.ad_group_criterion.Id if parent is not None and parent.ad_group_criterion is not None else None + product_partition.Condition=condition + product_partition.PartitionType='Unit' + ad_group_criterion.Criterion=product_partition + + partition_action=BulkAdGroupProductPartition() + partition_action.client_id=client_id + partition_action.ad_group_criterion=ad_group_criterion + self._partition_actions.append(partition_action) + + return partition_action + + def delete_partition(self, bulk_ad_group_product_partition): + """ + Sets the Delete action for the specified AdGroupCriterion, + and adds it to the helper's list of BulkAdGroupProductPartition. + + :param bulk_ad_group_product_partition: The BulkAdGroupProductPartition whose product partition you want to delete. + :type bulk_ad_group_product_partition: BulkAdGroupProductPartition + """ + + if bulk_ad_group_product_partition is not None and bulk_ad_group_product_partition.ad_group_criterion is not None: + bulk_ad_group_product_partition.ad_group_criterion.AdGroupId=self._ad_group_id + bulk_ad_group_product_partition.ad_group_criterion.Status='Deleted' + if hasattr(bulk_ad_group_product_partition.ad_group_criterion, 'EditorialStatus'): + bulk_ad_group_product_partition.ad_group_criterion.EditorialStatus=None + self._partition_actions.append(bulk_ad_group_product_partition) + + def update_partition(self, bulk_ad_group_product_partition): + """ + Sets the Update action for the specified BiddableAdGroupCriterion, + and adds it to the helper's list of BulkAdGroupProductPartition. + You can only update the CriterionBid and DestinationUrl elements + of the BiddableAdGroupCriterion. + When working with product partitions, youu cannot update the Criterion (ProductPartition). + To update a ProductPartition, you must delete the existing node (delete_partition) and + add a new one (add_unit or add_subdivision) during the same upload. + + :param bulk_ad_group_product_partition: The BulkAdGroupProductPartition to update. + :type bulk_ad_group_product_partition: BulkAdGroupProductPartition + """ + + if bulk_ad_group_product_partition is not None and bulk_ad_group_product_partition.ad_group_criterion is not None: + bulk_ad_group_product_partition.ad_group_criterion.AdGroupId=self._ad_group_id + bulk_ad_group_product_partition.ad_group_criterion.Status=None + if hasattr(bulk_ad_group_product_partition.ad_group_criterion, 'EditorialStatus'): + bulk_ad_group_product_partition.ad_group_criterion.EditorialStatus=None + self._partition_actions.append(bulk_ad_group_product_partition) + + +# 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 + + # You will need to use the Campaign Management service to get the Bing Merchant Center Store Id. This will be used + # when creating a new Bing Shopping Campaign. + # For other operations such as adding product conditions, you can manage Bing Shopping Campaigns solely with the Bulk Service. + + # Get a list of all Bing Merchant Center stores associated with your CustomerId + + stores=campaign_service.GetBMCStoresByCustomerId()['BMCStore'] + if stores is None: + output_status_message( + "You do not have any BMC stores registered for CustomerId {0}.\n".format(authorization_data.customer_id) + ) + sys.exit(0) + + # Add a new Bing Shopping campaign that will be associated with a ProductScope criterion. + # - Set the CampaignType element of the Campaign to Shopping. + # - Create a ShoppingSetting instance and set its Priority (0, 1, or 2), SalesCountryCode, and StoreId elements. + # Add this shopping setting to the Settings list of the Campaign. + + bulk_campaign=BulkCampaign() + + # The client_id may be used to associate records in the bulk upload file with records in the results file. The value of this field + # is not used or stored by the server; it is simply copied from the uploaded record to the corresponding result record. + # Note: This bulk file Client Id is not related to an application Client Id for OAuth. + + bulk_campaign.client_id='YourClientIdGoesHere' + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + + # When using the Campaign Management service, the Id cannot be set. In the context of a BulkCampaign, the Id is optional + # and may be used as a negative reference key during bulk upload. For example the same negative reference key for the campaign Id + # will be used when associating this new campaign with a new campaign product scope in the BulkCampaignProductScope object below. + + campaign.Id=CAMPAIGN_ID_KEY + settings=campaign_service.factory.create('ArrayOfSetting') + setting=set_elements_to_none(campaign_service.factory.create('ShoppingSetting')) + setting.Priority=0 + setting.SalesCountryCode ='US' + setting.StoreId=stores[0].Id + settings.Setting.append(setting) + campaign.Settings=settings + campaign.CampaignType=['Shopping'] + campaign.Name='Bing Shopping Campaign ' + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description='Bing Shopping Campaign Example.' + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving='true' + campaign.Status='Paused' + bulk_campaign.campaign=campaign + + # Optionally, you can create a ProductScope criterion that will be associated with your Bing Shopping campaign. + # Use the product scope criterion to include a subset of your product catalog, for example a specific brand, + # category, or product type. A campaign can only be associated with one ProductScope, which contains a list + # of up to 7 ProductCondition. You'll also be able to specify more specific product conditions for each ad group. + + bulk_campaign_product_scope=BulkCampaignProductScope() + bulk_campaign_product_scope.status='Active' + campaign_criterion=set_elements_to_none(campaign_service.factory.create('CampaignCriterion')) + product_scope=set_elements_to_none(campaign_service.factory.create('ProductScope')) + conditions=campaign_service.factory.create('ArrayOfProductCondition') + condition_new=campaign_service.factory.create('ProductCondition') + condition_new.Operand='Condition' + condition_new.Attribute='New' + conditions.ProductCondition.append(condition_new) + condition_custom_label_0=campaign_service.factory.create('ProductCondition') + condition_custom_label_0.Operand='CustomLabel0' + condition_custom_label_0.Attribute='MerchantDefinedCustomLabel' + conditions.ProductCondition.append(condition_custom_label_0) + product_scope.Conditions=conditions + campaign_criterion.CampaignId=CAMPAIGN_ID_KEY + campaign_criterion.Criterion=product_scope + bulk_campaign_product_scope.campaign_criterion=campaign_criterion + + # Specify one or more ad groups. + + bulk_ad_group=BulkAdGroup() + bulk_ad_group.campaign_id=CAMPAIGN_ID_KEY + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + ad_group.Id=AD_GROUP_ID_KEY + ad_group.Name="Product Categories" + ad_group.AdDistribution=['Search'] + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + ad_group.Status='Paused' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + search_bid=campaign_service.factory.create('Bid') + search_bid.Amount=0.09 + ad_group.SearchBid=search_bid + ad_group.Language='English' + + bulk_ad_group.ad_group=ad_group + + + #Create a product ad. You must add at least one ProductAd to the corresponding ad group. + #A ProductAd is not used directly for delivered ad copy. Instead, the delivery engine generates + #product ads from the product details that it finds in your Bing Merchant Center store's product catalog. + #The primary purpose of the ProductAd object is to provide promotional text that the delivery engine + #adds to the product ads that it generates. For example, if the promotional text is set to + #'Free shipping on $99 purchases', the delivery engine will set the product ad's description to + #'Free shipping on $99 purchases.' + + bulk_product_ad=BulkProductAd() + bulk_product_ad.ad_group_id=AD_GROUP_ID_KEY + ads=campaign_service.factory.create('ArrayOfAd') + product_ad=set_elements_to_none(campaign_service.factory.create('ProductAd')) + product_ad.PromotionalText='Free shipping on $99 purchases.' + product_ad.Type='Product' + bulk_product_ad.ad=product_ad + + upload_entities=[] + upload_entities.append(bulk_campaign) + upload_entities.append(bulk_campaign_product_scope) + upload_entities.append(bulk_ad_group) + upload_entities.append(bulk_product_ad) + + download_entities=write_entities_and_upload_file(upload_entities) + + # Write the upload output + + campaign_results=[] + campaign_product_scope_results=[] + ad_group_results=[] + product_ad_results=[] + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + campaign_results.append(entity) + output_bulk_campaigns([entity]) + if isinstance(entity, BulkCampaignProductScope): + campaign_product_scope_results.append(entity) + output_bulk_campaign_product_scopes([entity]) + if isinstance(entity, BulkAdGroup): + ad_group_results.append(entity) + output_bulk_ad_groups([entity]) + if isinstance(entity, BulkProductAd): + product_ad_results.append(entity) + output_bulk_product_ads([entity]) + + ad_group_id=ad_group_results.pop(0).ad_group.Id + + # Bid on all products + + helper=ProductPartitionHelper(ad_group_id) + + root_condition=set_elements_to_none(campaign_service.factory.create('ProductCondition')) + root_condition.Operand='All' + root_condition.Attribute=None + + root=helper.add_unit( + None, + root_condition, + 0.35, + False, + "root" + ) + + output_status_message("Applying only the root as a Unit with a bid . . . \n") + apply_bulk_product_partition_actions_results=apply_bulk_product_partition_actions(helper.partition_actions) + + product_partitions=get_bulk_ad_group_product_partition_tree(ad_group_id) + + output_status_message("The ad group's product partition only has a tree root node: \n") + output_product_partitions(product_partitions) + + # Let's update the bid of the root Unit we just added. + + updated_root=get_node_by_client_id(apply_bulk_product_partition_actions_results, "root") + fixed_bid=campaign_service.factory.create('FixedBid') + bid=campaign_service.factory.create('Bid') + bid.Amount=0.45 + fixed_bid.Bid=bid + updated_root.ad_group_criterion.CriterionBid=fixed_bid + + helper=ProductPartitionHelper(ad_group_id) + helper.update_partition(updated_root) + + output_status_message("Updating the bid for the tree root node . . . \n") + apply_bulk_product_partition_actions_results=apply_bulk_product_partition_actions(helper.partition_actions) + + product_partitions=get_bulk_ad_group_product_partition_tree(ad_group_id) + + output_status_message("Updated the bid for the tree root node: \n") + output_product_partitions(product_partitions) + + + #Now we will overwrite any existing tree root, and build a product partition group tree structure in multiple steps. + #You could build the entire tree in a single call since there are less than 5,000 nodes; however, + #we will build it in steps to demonstrate how to use the results from ApplyProductPartitionActions to update the tree. + + #For a list of validation rules, see the Bing Shopping Campaigns technical guide: + #https://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-bing-shopping-campaigns.aspx + + + helper=ProductPartitionHelper(ad_group_id) + + #Check whether a root node exists already. + + existing_root=get_node_by_client_id(apply_bulk_product_partition_actions_results, "root") + if existing_root is not None: + existing_root.client_id="deletedroot" + helper.delete_partition(existing_root) + + root_condition=campaign_service.factory.create('ProductCondition') + root_condition.Operand='All' + root_condition.Attribute=None + root=helper.add_subdivision( + None, + root_condition, + "root" + ) + + + #The direct children of any node must have the same Operand. + #For this example we will use CategoryL1 nodes as children of the root. + #For a list of valid CategoryL1 through CategoryL5 values, see the Bing Category Taxonomy: + #http://advertise.bingads.microsoft.com/en-us/WWDocs/user/search/en-us/Bing_Category_Taxonomy.txt + + animals_condition=campaign_service.factory.create('ProductCondition') + animals_condition.Operand='CategoryL1' + animals_condition.Attribute='Animals & Pet Supplies' + animals_subdivision=helper.add_subdivision( + root, + animals_condition, + "animals_subdivision" + ) + + + #If you use a CategoryL2 node, it must be a descendant (child or later) of a CategoryL1 node. + #In other words you cannot have a CategoryL2 node as parent of a CategoryL1 node. + #For this example we will a CategoryL2 node as child of the CategoryL1 Animals & Pet Supplies node. + + pet_supplies_condition=campaign_service.factory.create('ProductCondition') + pet_supplies_condition.Operand='CategoryL2' + pet_supplies_condition.Attribute='Pet Supplies' + pet_supplies_subdivision=helper.add_subdivision( + animals_subdivision, + pet_supplies_condition, + "pet_supplies_subdivision" + ) + + brand_a_condition=campaign_service.factory.create('ProductCondition') + brand_a_condition.Operand='Brand' + brand_a_condition.Attribute='Brand A' + brand_a=helper.add_unit( + pet_supplies_subdivision, + brand_a_condition, + 0.35, + False, + "brand_a" + ) + + + #If you won't bid on Brand B, set the helper method's bidAmount to '0' and isNegative to True. + #The helper method will create a NegativeAdGroupCriterion and apply the condition. + + brand_b_condition=campaign_service.factory.create('ProductCondition') + brand_b_condition.Operand='Brand' + brand_b_condition.Attribute='Brand B' + brand_b=helper.add_unit( + pet_supplies_subdivision, + brand_b_condition, + 0, + True, + "brand_b" + ) + + other_brands_condition=campaign_service.factory.create('ProductCondition') + other_brands_condition.Operand='Brand' + other_brands_condition.Attribute=None + other_brands=helper.add_unit( + pet_supplies_subdivision, + other_brands_condition, + 0.35, + False, + "other_brands" + ) + + other_pet_supplies_condition=campaign_service.factory.create('ProductCondition') + other_pet_supplies_condition.Operand='CategoryL2' + other_pet_supplies_condition.Attribute=None + other_pet_supplies=helper.add_unit( + animals_subdivision, + other_pet_supplies_condition, + 0.35, + False, + "other_pet_supplies" + ) + + electronics_condition=campaign_service.factory.create('ProductCondition') + electronics_condition.Operand='CategoryL1' + electronics_condition.Attribute='Electronics' + electronics=helper.add_unit( + root, + electronics_condition, + 0.35, + False, + "electronics" + ) + + other_categoryL1_condition=campaign_service.factory.create('ProductCondition') + other_categoryL1_condition.Operand='CategoryL1' + other_categoryL1_condition.Attribute=None + other_categoryL1=helper.add_unit( + root, + other_categoryL1_condition, + 0.35, + False, + "other_categoryL1" + ) + + output_status_message("Applying product partitions to the ad group . . . \n") + apply_bulk_product_partition_actions_results=apply_bulk_product_partition_actions(helper.partition_actions) + + product_partitions=get_bulk_ad_group_product_partition_tree(ad_group_id) + + + #The product partition group tree now has 9 nodes. + + #All other (Root Node) + #| + #+-- Animals & Pet Supplies (CategoryL1) + #| | + #| +-- Pet Supplies (CategoryL2) + #| | | + #| | +-- Brand A + #| | | + #| | +-- Brand B + #| | | + #| | +-- All other (Brand) + #| | + #| +-- All other (CategoryL2) + #| + #+-- Electronics (CategoryL1) + #| + #+-- All other (CategoryL1) + + output_status_message("The product partition group tree now has 9 nodes: \n") + output_product_partitions(product_partitions) + + + #Let's replace the Electronics (CategoryL1) node created above with an Electronics (CategoryL1) node that + #has children i.e. Brand C (Brand), Brand D (Brand), and All other (Brand) as follows: + + #Electronics (CategoryL1) + #| + #+-- Brand C (Brand) + #| + #+-- Brand D (Brand) + #| + #+-- All other (Brand) + + helper=ProductPartitionHelper(ad_group_id) + + + #To replace a node we must know its Id and its ParentCriterionId. In this case the parent of the node + #we are replacing is All other (Root Node), and was created at Index 1 of the previous ApplyProductPartitionActions call. + #The node that we are replacing is Electronics (CategoryL1), and was created at Index 8. + + root_id=get_node_by_client_id(apply_bulk_product_partition_actions_results, "root").ad_group_criterion.Id + #electronics.Id=get_node_by_client_id(apply_bulk_product_partition_actions_results, "electronics").ad_group_criterion.Id + electronics.ad_group_criterion.Id=get_node_by_client_id(apply_bulk_product_partition_actions_results, "electronics").ad_group_criterion.Id + helper.delete_partition(electronics) + + parent=BulkAdGroupProductPartition() + parent.ad_group_criterion=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + parent.ad_group_criterion.Id=root_id + + electronics_subdivision_condition=campaign_service.factory.create('ProductCondition') + electronics_subdivision_condition.Operand='CategoryL1' + electronics_subdivision_condition.Attribute='Electronics' + electronics_subdivision=helper.add_subdivision( + parent, + electronics_subdivision_condition, + "electronics_subdivision" + ) + + brand_c_condition=campaign_service.factory.create('ProductCondition') + brand_c_condition.Operand='Brand' + brand_c_condition.Attribute='Brand C' + brand_c=helper.add_unit( + electronics_subdivision, + brand_c_condition, + 0.35, + False, + "brand_c" + ) + + brand_d_condition=campaign_service.factory.create('ProductCondition') + brand_d_condition.Operand='Brand' + brand_d_condition.Attribute='Brand D' + brand_d=helper.add_unit( + electronics_subdivision, + brand_d_condition, + 0.35, + False, + "brand_d" + ) + + other_electronics_brands_condition=campaign_service.factory.create('ProductCondition') + other_electronics_brands_condition.Operand='Brand' + other_electronics_brands_condition.Attribute=None + other_electronics_brands=helper.add_unit( + electronics_subdivision, + other_electronics_brands_condition, + 0.35, + False, + "other_electronics_brands" + ) + + output_status_message( + "Updating the product partition group to refine Electronics (CategoryL1) with 3 child nodes . . . \n" + ) + apply_bulk_product_partition_actions_results=apply_bulk_product_partition_actions(helper.partition_actions) + + product_partitions=get_bulk_ad_group_product_partition_tree(ad_group_id) + + + #The product partition group tree now has 12 nodes, including the children of Electronics (CategoryL1): + + #All other (Root Node) + #| + #+-- Animals & Pet Supplies (CategoryL1) + #| | + #| +-- Pet Supplies (CategoryL2) + #| | | + #| | +-- Brand A + #| | | + #| | +-- Brand B + #| | | + #| | +-- All other (Brand) + #| | + #| +-- All other (CategoryL2) + #| + #+-- Electronics (CategoryL1) + #| | + #| +-- Brand C (Brand) + #| | + #| +-- Brand D (Brand) + #| | + #| +-- All other (Brand) + #| + #+-- All other (CategoryL1) + + output_status_message( + "The product partition group tree now has 12 nodes, including the children of Electronics (CategoryL1): \n" + ) + output_product_partitions(product_partitions) + + # Delete the campaign, ad group, criterion, and ad that were previously added. + # You should remove this region if you want to view the added entities in the + # Bing Ads web application or another tool. + + upload_entities=[] + + for campaign_result in campaign_results: + campaign_result.campaign.Status='Deleted' + upload_entities.append(campaign_result) + + output_status_message("\nDeleting campaign, ad group, criterion, and product ad . . .") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + output_bulk_campaigns([entity]) + + 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/BulkTargets.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkTargets.py index 4a8d8363..1c113700 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkTargets.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkTargets.py @@ -137,13 +137,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkTargets.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkTargets.py.bak new file mode 100644 index 00000000..4a8d8363 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/BulkTargets.py.bak @@ -0,0 +1,859 @@ +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)ng.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' + + CAMPAIGN_ID_KEY=-123 + + # 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_campaigns(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaign: \n") + output_status_message("AccountId: {0}".format(entity.account_id)) + 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 the Campaign Management Campaign Object + output_campaign(entity.campaign) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_campaign(campaign): + if campaign is not None: + output_status_message("BudgetType: {0}".format(campaign.BudgetType)) + if campaign.CampaignType is not None: + for campaign_type in campaign.CampaignType: + output_status_message("CampaignType: {0}".format(campaign_type)) + else: + output_status_message("CampaignType: None") + output_status_message("DailyBudget: {0}".format(campaign.DailyBudget)) + output_status_message("Description: {0}".format(campaign.Description)) + output_status_message("ForwardCompatibilityMap: ") + if campaign.ForwardCompatibilityMap is not None and len(campaign.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in campaign.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(campaign.Id)) + output_status_message("Name: {0}".format(campaign.Name)) + output_status_message("NativeBidAdjustment: {0}".format(campaign.NativeBidAdjustment)) + output_status_message("Settings: ") + for setting in campaign.Settings.Setting: + if setting.Type == 'ShoppingSetting': + output_status_message("\tShoppingSetting: ") + output_status_message("\t\tPriority: {0}".format(setting.Priority)) + output_status_message("\t\tSalesCountryCode: {0}".format(setting.SalesCountryCode)) + output_status_message("\t\tStoreId: {0}".format(setting.StoreId)) + output_status_message("Status: {0}".format(campaign.Status)) + output_status_message("TimeZone: {0}".format(campaign.TimeZone)) + output_status_message("TrackingUrlTemplate: {0}".format(campaign.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if campaign.UrlCustomParameters is not None and campaign.UrlCustomParameters.Parameters is not None: + for custom_parameter in campaign.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_bulk_campaign_day_time_targets(bulk_entities): + for entity in bulk_entities: + # If the BulkCampaignDayTimeTarget object was created by the application, and not read from a bulk file, + # then there will be no BulkCampaignDayTimeTargetBid objects. For example if you want to print the + # BulkCampaignDayTimeTarget prior to upload. + if len(entity.bids) == 0 and entity.day_time_target is not None: + output_status_message("BulkCampaignDayTimeTarget: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}\n".format(entity.target_id)) + + for bid in entity.day_time_target.Bids['DayTimeTargetBid']: + output_status_message("Campaign Management DayTimeTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("Day: {0}".format(bid.Day)) + output_status_message("From Hour: {0}".format(bid.FromHour)) + output_status_message("From Minute: {0}".format(bid.FromMinute)) + output_status_message("To Hour: {0}".format(bid.ToHour)) + output_status_message("To Minute: {0}".format(bid.ToMinute)) + else: + output_bulk_campaign_day_time_target_bids(entity.bids) + + output_status_message('') + +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_campaign_day_time_target_bids(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignDayTimeTargetBid: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}\n".format(entity.target_id)) + + output_status_message("Campaign Management DayTimeTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(entity.day_time_target_bid.BidAdjustment)) + output_status_message("Day: {0}".format(entity.day_time_target_bid.Day)) + output_status_message("From Hour: {0}".format(entity.day_time_target_bid.FromHour)) + output_status_message("From Minute: {0}".format(entity.day_time_target_bid.FromMinute)) + output_status_message("To Hour: {0}".format(entity.day_time_target_bid.ToHour)) + output_status_message("To Minute: {0}".format(entity.day_time_target_bid.ToMinute)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_location_targets(bulk_entities): + for entity in bulk_entities: + # If the BulkCampaignLocationTarget object was created by the application, and not read from a bulk file, + # then there will be no BulkCampaignLocationTargetBid objects. For example if you want to print the + # BulkCampaignLocationTarget prior to upload. + if len(entity.bids) == 0: + output_status_message("BulkCampaignLocationTarget: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}".format(entity.target_id)) + output_status_message("Intent Option: {0}\n".format(entity.location_target.IntentOption)) + + if entity.city_target is not None: + for bid in entity.city_target.Bids['CityTargetBid']: + output_status_message("Campaign Management CityTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("City: {0}".format(bid.City)) + output_status_message("Location Is Excluded: {0}".format(bid.IsExcluded)) + if entity.country_target is not None: + for bid in entity.country_target.Bids['CountryTargetBid']: + output_status_message("Campaign Management CountryTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("CountryAndRegion : {0}".format(bid.CountryAndRegion )) + output_status_message("Location Is Excluded: {0}".format(bid.IsExcluded)) + if entity.metro_area_target is not None: + for bid in entity.metro_area_target.Bids['MetroAreaTargetBid']: + output_status_message("Campaign Management MetroAreaTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("MetroArea: {0}".format(bid.MetroArea)) + output_status_message("Location Is Excluded: {0}".format(bid.IsExcluded)) + if entity.postal_code_target is not None: + for bid in entity.postal_code_target.Bids['PostalCodeTargetBid']: + output_status_message("Campaign Management PostalCodeTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("PostalCode: {0}".format(bid.PostalCode)) + output_status_message("Location Is Excluded: {0}".format(bid.IsExcluded)) + if entity.state_target is not None: + for bid in entity.state_target.Bids['StateTargetBid']: + output_status_message("Campaign Management StateTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("State : {0}".format(bid.State )) + output_status_message("Location Is Excluded: {0}".format(bid.IsExcluded)) + else: + output_bulk_campaign_location_target_bids(entity.bids) + + output_status_message('') + +def output_bulk_campaign_location_target_bids(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignLocationTargetBid: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}".format(entity.target_id)) + output_status_message("Intent Option: {0}\n".format(entity.intent_option)) + + output_status_message("BidAdjustment: {0}".format(entity.bid_adjustment)) + output_status_message("LocationType: {0}".format(entity.location_type)) + output_status_message("Location : {0}".format(entity.location)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_radius_targets(bulk_entities): + for entity in bulk_entities: + # If the BulkCampaignRadiusTarget object was created by the application, and not read from a bulk file, + # then there will be no BulkCampaignRadiusTargetBid objects. For example if you want to print the + # BulkCampaignRadiusTarget prior to upload. + if len(entity.bids) == 0 and entity.radius_target is not None: + output_status_message("BulkCampaignRadiusTarget: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}".format(entity.target_id)) + output_status_message("Intent Option: {0}\n".format(entity.intent_option)) + + for bid in entity.radius_target.Bids['RadiusTargetBid']: + output_status_message("Campaign Management RadiusTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("LatitudeDegrees: {0}".format(bid.LatitudeDegrees)) + output_status_message("LongitudeDegrees: {0}".format(bid.LongitudeDegrees)) + output_status_message("Radius: {0}".format(bid.Radius)) + output_status_message("Radius Unit: {0}".format(bid.RadiusUnit)) + else: + output_bulk_campaign_radius_target_bids(entity.bids) + + output_status_message('') + +def output_bulk_campaign_radius_target_bids(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignRadiusTargetBid: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}".format(entity.target_id)) + output_status_message("Intent Option: {0}\n".format(entity.intent_option)) + + output_status_message("Campaign Management RadiusTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(entity.radius_target_bid.BidAdjustment)) + output_status_message("LatitudeDegrees: {0}".format(entity.radius_target_bid.LatitudeDegrees)) + output_status_message("LongitudeDegrees: {0}".format(entity.radius_target_bid.LongitudeDegrees)) + output_status_message("Radius: {0}".format(entity.radius_target_bid.Radius)) + output_status_message("RadiusUnit: {0}".format(entity.radius_target_bid.RadiusUnit)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_message('') + +def output_bulk_campaign_device_os_targets(bulk_entities): + for entity in bulk_entities: + # If the BulkCampaignDeviceOsTarget object was created by the application, and not read from a bulk file, + # then there will be no BulkCampaignDeviceOsTargetBid objects. For example if you want to print the + # BulkCampaignDeviceOsTarget prior to upload. + if len(entity.bids) == 0 and entity.device_os_target is not None: + output_status_message("BulkCampaignDeviceOsTarget: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}".format(entity.target_id)) + + for bid in entity.device_os_target.Bids['DeviceOSTargetBid']: + output_status_message("Campaign Management DeviceOSTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("DeviceName: {0}".format(bid.DeviceName)) + else: + output_bulk_campaign_device_os_target_bids(entity.bids) + + output_status_message('') + +def output_bulk_campaign_device_os_target_bids(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaignDeviceOsTargetBid: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign_name)) + output_status_message("Campaign Id: {0}".format(entity.campaign_id)) + output_status_message("Target Id: {0}".format(entity.target_id)) + + output_status_message("Campaign Management DeviceOSTargetBid Object: ") + output_status_message("BidAdjustment: {0}".format(entity.device_os_target_bid.BidAdjustment)) + output_status_message("DeviceName: {0}".format(entity.device_os_target_bid.DeviceName)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +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 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 + + # Prepare the bulk entities that you want to upload. Each bulk entity contains the corresponding campaign management object, + # and additional elements needed to read from and write to a bulk file. + + bulk_campaign=BulkCampaign() + + # The client_id may be used to associate records in the bulk upload file with records in the results file. The value of this field + # is not used or stored by the server; it is simply copied from the uploaded record to the corresponding result record. + # Note: This bulk file Client Id is not related to an application Client Id for OAuth. + + bulk_campaign.client_id='YourClientIdGoesHere' + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + + # When using the Campaign Management service, the Id cannot be set. In the context of a BulkCampaign, the Id is optional + # and may be used as a negative reference key during bulk upload. For example the same negative reference key for the campaign Id + # will be used when adding new ad groups to this new campaign, or when associating ad extensions with the campaign. + + campaign.Id=CAMPAIGN_ID_KEY + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.Status='Paused' + + # DaylightSaving is not supported in the Bulk file schema. Whether or not you specify it in a BulkCampaign, + # the value is not written to the Bulk file, and by default DaylightSaving is set to true. + campaign.DaylightSaving='True' + + bulk_campaign.campaign=campaign + + bulk_campaign_day_time_target=BulkCampaignDayTimeTarget() + bulk_campaign_day_time_target.campaign_id=CAMPAIGN_ID_KEY + day_time_target=campaign_service.factory.create('DayTimeTarget') + day_time_target_bid_0=campaign_service.factory.create('DayTimeTargetBid') + day_time_target_bid_0.BidAdjustment=10 + day_time_target_bid_0.Day='Friday' + day_time_target_bid_0.FromHour=11 + day_time_target_bid_0.FromMinute='Zero' + day_time_target_bid_0.ToHour=13 + day_time_target_bid_0.ToMinute='Fifteen' + day_time_target_bid_1=campaign_service.factory.create('DayTimeTargetBid') + day_time_target_bid_1.BidAdjustment=20 + day_time_target_bid_1.Day='Saturday' + day_time_target_bid_1.FromHour=11 + day_time_target_bid_1.FromMinute='Zero' + day_time_target_bid_1.ToHour=13 + day_time_target_bid_1.ToMinute='Fifteen' + day_time_target.Bids.DayTimeTargetBid.append(day_time_target_bid_0) + day_time_target.Bids.DayTimeTargetBid.append(day_time_target_bid_1) + bulk_campaign_day_time_target.day_time_target=day_time_target + + bulk_campaign_location_target=BulkCampaignLocationTarget() + bulk_campaign_location_target.campaign_id=CAMPAIGN_ID_KEY + city_target=campaign_service.factory.create('CityTarget') + city_target_bid=campaign_service.factory.create('CityTargetBid') + city_target_bid.BidAdjustment=10 + city_target_bid.City="Toronto, Toronto ON CA" + city_target_bid.IsExcluded=False + city_target.Bids.CityTargetBid.append(city_target_bid) + country_target=campaign_service.factory.create('CountryTarget') + country_target_bid=campaign_service.factory.create('CountryTargetBid') + country_target_bid.BidAdjustment=15 + country_target_bid.CountryAndRegion="CA" + country_target_bid.IsExcluded=False + country_target.Bids.CountryTargetBid.append(country_target_bid) + metro_area_target=campaign_service.factory.create('MetroAreaTarget') + metro_area_target_bid=campaign_service.factory.create('MetroAreaTargetBid') + metro_area_target_bid.BidAdjustment=15 + metro_area_target_bid.MetroArea="Seattle-Tacoma, WA, WA US" + metro_area_target_bid.IsExcluded=False + metro_area_target.Bids.MetroAreaTargetBid.append(metro_area_target_bid) + postal_code_target=campaign_service.factory.create('PostalCodeTarget') + postal_code_target_bid=campaign_service.factory.create('PostalCodeTargetBid') + postal_code_target_bid.BidAdjustment=15 + postal_code_target_bid.PostalCode="98052, WA US" + postal_code_target_bid.IsExcluded=False + postal_code_target.Bids.PostalCodeTargetBid.append(postal_code_target_bid) + state_target=campaign_service.factory.create('StateTarget') + state_target_bid=campaign_service.factory.create('StateTargetBid') + state_target_bid.BidAdjustment=15 + state_target_bid.State="US-WA" + state_target_bid.IsExcluded=False + state_target.Bids.StateTargetBid.append(state_target_bid) + location_target=campaign_service.factory.create('LocationTarget') + location_target.IntentOption='PeopleIn' + location_target.CityTarget=city_target + location_target.CountryTarget=country_target + location_target.MetroAreaTarget=metro_area_target + location_target.PostalCodeTarget=postal_code_target + location_target.StateTarget=state_target + bulk_campaign_location_target.location_target=location_target + + bulk_campaign_radius_target=BulkCampaignRadiusTarget() + bulk_campaign_radius_target.campaign_id=CAMPAIGN_ID_KEY + bulk_campaign_radius_target.intent_option='PeopleInOrSearchingForOrViewingPages' + radius_target=campaign_service.factory.create('RadiusTarget') + radius_target_bid=campaign_service.factory.create('RadiusTargetBid') + radius_target_bid.BidAdjustment=10 + radius_target_bid.LatitudeDegrees=47.755367 + radius_target_bid.LongitudeDegrees=-122.091827 + radius_target_bid.Radius=11 + radius_target_bid.RadiusUnit='Kilometers' + radius_target.Bids.RadiusTargetBid.append(radius_target_bid) + bulk_campaign_radius_target.radius_target=radius_target + + bulk_campaign_device_os_target=BulkCampaignDeviceOsTarget() + bulk_campaign_device_os_target.campaign_id=CAMPAIGN_ID_KEY + device_os_target=campaign_service.factory.create('DeviceOSTarget') + device_os_target_bid=campaign_service.factory.create('DeviceOSTargetBid') + device_os_target_bid.BidAdjustment = 20 + device_os_target_bid.DeviceName='Tablets' + device_os_target.Bids.DeviceOSTargetBid.append(device_os_target_bid) + bulk_campaign_device_os_target.device_os_target=device_os_target + + output_bulk_campaign_day_time_targets([bulk_campaign_day_time_target]) + output_bulk_campaign_location_targets([bulk_campaign_location_target]) + output_bulk_campaign_radius_targets([bulk_campaign_radius_target]) + output_bulk_campaign_device_os_targets([bulk_campaign_device_os_target]) + + # Upload the entities created above. + + upload_entities=[] + upload_entities.append(bulk_campaign) + upload_entities.append(bulk_campaign_day_time_target) + upload_entities.append(bulk_campaign_location_target) + upload_entities.append(bulk_campaign_radius_target) + upload_entities.append(bulk_campaign_device_os_target) + + output_status_message("\nAdding campaign and targets . . .\n") + download_entities=write_entities_and_upload_file(upload_entities) + + campaign_results=[] + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + campaign_results.append(entity) + output_bulk_campaigns([entity]) + if isinstance(entity, BulkCampaignDayTimeTarget): + output_bulk_campaign_day_time_targets([entity]) + if isinstance(entity, BulkCampaignLocationTarget): + output_bulk_campaign_location_targets([entity]) + if isinstance(entity, BulkCampaignRadiusTarget): + output_bulk_campaign_radius_targets([entity]) + if isinstance(entity, BulkCampaignDeviceOsTarget): + output_bulk_campaign_device_os_targets([entity]) + + # If the campaign was successfully added in the previous upload, let's append a new device bid. + if(len(campaign_results) > 0): + # In this example we want to keep the Tablets bid that was uploaded previously, so we will upload the BulkCampaignDeviceOsTargetBid. + # Each BulkCampaignDeviceOsTargetBid instance corresponds to one Campaign DeviceOS Target record in the bulk file. + # If instead you want to replace all existing device target bids for the specified campaign, then you should upload + # a BulkCampaignDeviceOsTarget. If you write a BulkCampaignDeviceOsTarget to the file (for example see the previous upload above), + # then an additional Campaign DeviceOS Target record is included automatically with Status set to Deleted. + bulk_campaign_device_os_target_bid=BulkCampaignDeviceOsTargetBid() + bulk_campaign_device_os_target_bid.campaign_id=campaign_results[0].campaign.Id + # You can specify ClientId for BulkCampaignDeviceOsTargetBid, but not for BulkCampaignDeviceOsTarget. + bulk_campaign_device_os_target_bid.client_id = "My BulkCampaignDeviceOsTargetBid" + device_os_target_bid=campaign_service.factory.create('DeviceOSTargetBid') + device_os_target_bid.BidAdjustment = 20 + device_os_target_bid.DeviceName='Smartphones' + bulk_campaign_device_os_target_bid.device_os_target_bid=device_os_target_bid + + upload_entities=[] + upload_entities.append(bulk_campaign_device_os_target_bid) + + output_status_message("\nAdding Smartphones device target for campaign . . .\n") + download_entities=write_entities_and_upload_file(upload_entities) + + campaign_results=[] + + for entity in download_entities: + if isinstance(entity, BulkCampaignDeviceOsTargetBid): + output_bulk_campaign_device_os_target_bids([entity]) + + # Delete the campaign and target associations that were previously added. + # You should remove this region if you want to view the added entities in the + # Bing Ads web application or another tool. + + # You must set the Id field of the Campaign record that you want to delete, and the Status field to Deleted. + # In this example the Id is already set i.e. via the upload result captured above. + # When you delete a BulkCampaign, the dependent entities such as BulkCampaignDeviceOsTarget + # are deleted without being specified explicitly. + + upload_entities=[] + + for campaign_result in campaign_results: + campaign_result.campaign.Status='Deleted' + upload_entities.append(campaign_result) + + output_status_message("\nDeleting campaign and target associations . . .\n") + download_entities=write_entities_and_upload_file(upload_entities) + + for entity in download_entities: + if isinstance(entity, BulkCampaign): + output_bulk_campaigns([entity]) + + 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/ConsoleGettingStarted.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConsoleGettingStarted.py index 1ffa9f89..a74b96a2 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConsoleGettingStarted.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConsoleGettingStarted.py @@ -122,13 +122,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConsoleGettingStarted.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConsoleGettingStarted.py.bak new file mode 100644 index 00000000..1ffa9f89 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConsoleGettingStarted.py.bak @@ -0,0 +1,417 @@ +from bingads.service_client import ServiceClient +from bingads.authorization 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + # The directory for the bulk files. + FILE_DIRECTORY='c:/bulk/' + + # The name of the bulk upload result file. + RESULT_FILE_NAME='result.csv' + + # 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_bulk_campaigns(bulk_entities): + for entity in bulk_entities: + output_status_message("BulkCampaign: \n") + output_status_message("Campaign Name: {0}".format(entity.campaign.Name)) + output_status_message("Campaign Id: {0}".format(entity.campaign.Id)) + + if entity.has_errors: + output_bulk_errors(entity.errors) + + output_status_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('') + +# Main execution +if __name__ == '__main__': + + 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 + + + #To discover all SOAP elements accessible for each service, you can print the soap client. + #For example, print(campaign_service.soap_client) will return Campaign, AdGroup, TextAd, Keyword, etc. + + #You could use the Campaign Management ServiceClient to add a Campaign as follows: + #add_campaigns_response=campaign_service.AddCampaigns( + # AccountId=authorization_data.account_id, + # Campaigns=campaigns + #) + + #BulkEntity-derived classes can also contain the SOAP objects, for example BulkCampaign can contain a Campaign. + #As shown below, you can use the BulkServiceManager to upload a BulkCampaign. + #You should take advantage of the Bulk service to efficiently manage ads and keywords for all campaigns in an account. + + CAMPAIGN_ID_KEY=-123 + + bulk_campaign=BulkCampaign() + + #The client_id may be used to associate records in the bulk upload file with records in the results file. The value of this field + #is not used or stored by the server; it is simply copied from the uploaded record to the corresponding result record. + #Note: This bulk file Client Id is not related to an application Client Id for OAuth. + + bulk_campaign.client_id='YourClientIdGoesHere' + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + + #When using the Campaign Management service, the Id cannot be set. In the context of a BulkCampaign, the Id is optional + #and may be used as a negative reference key during bulk upload. For example the same negative reference key for the campaign Id + #will be used when adding new ad groups to this new campaign, or when associating ad extensions with the campaign. + + campaign.Id=CAMPAIGN_ID_KEY + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.Status='Paused' + bulk_campaign.campaign=campaign + + bulk_campaigns=[ bulk_campaign ] + + entity_upload_parameters=EntityUploadParameters( + result_file_directory=FILE_DIRECTORY, + result_file_name=RESULT_FILE_NAME, + entities=bulk_campaigns, + overwrite_result_file=True, + response_mode='ErrorsAndResults' + ) + + output_status_message("Adding a BulkCampaign...\n") + bulk_entities=list(bulk_service.upload_entities(entity_upload_parameters)) + + # Output the upload result entities + for entity in bulk_entities: + if isinstance(entity, BulkCampaign): + output_bulk_campaigns([entity]) + + 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/ConversionGoals.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConversionGoals.py index 7415cb52..e8f5edcb 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConversionGoals.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConversionGoals.py @@ -99,13 +99,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConversionGoals.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConversionGoals.py.bak new file mode 100644 index 00000000..7415cb52 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ConversionGoals.py.bak @@ -0,0 +1,653 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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) +logging.getLogger('suds.transport.http').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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_uet_tag(uet_tag): + if uet_tag is not None: + output_status_message("Description: {0}".format(uet_tag.Description)) + output_status_message("Id: {0}".format(uet_tag.Id)) + output_status_message("Name: {0}".format(uet_tag.Name)) + output_status_message("TrackingNoScript: {0}".format(uet_tag.TrackingNoScript)) + output_status_message("TrackingScript: {0}".format(uet_tag.TrackingScript)) + output_status_message("TrackingStatus: {0}\n".format(uet_tag.TrackingStatus)) + +def output_conversion_goal(conversion_goal): + if conversion_goal is not None: + output_status_message("ConversionWindowInMinutes: {0}".format(conversion_goal.ConversionWindowInMinutes)) + output_status_message("CountType: {0}".format(conversion_goal.CountType)) + output_status_message("Id: {0}".format(conversion_goal.Id)) + output_status_message("Name: {0}".format(conversion_goal.Name)) + output_conversion_goal_revenue(conversion_goal.Revenue) + output_status_message("Scope: {0}".format(conversion_goal.Scope)) + output_status_message("Status: {0}".format(conversion_goal.Status)) + output_status_message("TagId: {0}".format(conversion_goal.TagId)) + output_status_message("TrackingStatus: {0}".format(conversion_goal.TrackingStatus)) + output_status_message("Type: {0}".format(conversion_goal.Type)) + + if conversion_goal.Type == 'AppInstall': + output_status_message("AppPlatform: {0}".format(conversion_goal.AppPlatform)) + output_status_message("AppStoreId: {0}\n".format(conversion_goal.AppStoreId)) + elif conversion_goal.Type == 'Duration': + output_status_message("MinimumDurationInSeconds: {0}\n".format(conversion_goal.MinimumDurationInSeconds)) + elif conversion_goal.Type == 'Event': + output_status_message("ActionExpression: {0}".format(conversion_goal.ActionExpression)) + output_status_message("ActionOperator: {0}".format(conversion_goal.ActionOperator)) + output_status_message("CategoryExpression: {0}".format(conversion_goal.CategoryExpression)) + output_status_message("CategoryOperator: {0}".format(conversion_goal.CategoryOperator)) + output_status_message("LabelExpression: {0}".format(conversion_goal.LabelExpression)) + output_status_message("LabelOperator: {0}".format(conversion_goal.LabelOperator)) + output_status_message("Value: {0}".format(conversion_goal.Value)) + output_status_message("ValueOperator: {0}\n".format(conversion_goal.ValueOperator)) + elif conversion_goal.Type == 'PagesViewedPerVisit': + output_status_message("MinimumPagesViewed: {0}\n".format(conversion_goal.MinimumPagesViewed)) + elif conversion_goal.Type == 'Url': + output_status_message("UrlExpression: {0}".format(conversion_goal.UrlExpression)) + output_status_message("UrlOperator: {0}\n".format(conversion_goal.UrlOperator)) + +def output_conversion_goal_revenue(conversion_goal_revenue): + if conversion_goal_revenue is not None: + output_status_message("CurrencyCode: {0}".format(conversion_goal_revenue.CurrencyCode)) + output_status_message("Type: {0}".format(conversion_goal_revenue.Type)) + output_status_message("Value: {0}".format(conversion_goal_revenue.Value)) + +def output_partial_errors(partial_errors): + if not hasattr(partial_errors, 'BatchError'): + return None + output_status_message("BatchError (PartialErrors) item:\n") + for error in partial_errors['BatchError']: + output_status_message("\tIndex: {0}".format(error.Index)) + output_status_message("\tCode: {0}".format(error.Code)) + output_status_message("\tErrorCode: {0}".format(error.ErrorCode)) + output_status_message("\tMessage: {0}\n".format(error.Message)) + + # In the case of an EditorialError, more details are available + if error.Type == "EditorialError" and error.ErrorCode == "CampaignServiceEditorialValidationError": + output_status_message("\tDisapprovedText: {0}".format(error.DisapprovedText)) + output_status_message("\tLocation: {0}".format(error.Location)) + output_status_message("\tPublisherCountry: {0}".format(error.PublisherCountry)) + output_status_message("\tReasonCode: {0}\n".format(error.ReasonCode)) + +# Main execution +if __name__ == '__main__': + + 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 + + # Before you can track conversions or target audiences using a remarketing list, + # you need to create a UET tag in Bing Ads (web application or API) and then + # add the UET tag tracking code to every page of your website. For more information, please see + # Universal Event Tracking at https://msdn.microsoft.com/library/bing-ads-universal-event-tracking-guide.aspx. + + # First you should call the GetUetTagsByIds operation to check whether a tag has already been created. + # You can leave the TagIds element null or empty to request all UET tags available for the customer. + + uet_tags = campaign_service.GetUetTagsByIds(None).UetTags + + # If you do not already have a UET tag that can be used, or if you need another UET tag, + # call the AddUetTags service operation to create a new UET tag. If the call is successful, + # the tracking script that you should add to your website is included in a corresponding + # UetTag within the response message. + + if uet_tags is None or len(uet_tags) < 1: + uet_tags=campaign_service.factory.create('ArrayOfUetTag') + uet_tag=set_elements_to_none(campaign_service.factory.create('UetTag')) + uet_tag.Description="My First Uet Tag" + uet_tag.Name="New Uet Tag" + uet_tags.UetTag.append(uet_tag) + + uet_tags = campaign_service.AddUetTags(uet_tags).UetTags + + if uet_tags is None or len(uet_tags) < 1: + output_status_message( + string.Format("You do not have any UET tags registered for CustomerId {0}.\n", authorizationData.CustomerId) + ) + sys.exit(0) + + output_status_message("List of all UET Tags:\n") + for uet_tag in uet_tags['UetTag']: + output_uet_tag(uet_tag) + + + # After you retreive the tracking script from the AddUetTags or GetUetTagsByIds operation, + # the next step is to add the UET tag tracking code to your website. We recommend that you, + # or your website administrator, add it to your entire website in either the head or body sections. + # If your website has a master page, then that is the best place to add it because you add it once + # and it is included on all pages. For more information, please see + # Universal Event Tracking at https://msdn.microsoft.com/library/bing-ads-universal-event-tracking-guide.aspx. + + + # We will use the same UET tag for the remainder of this example. + + tag_id = uet_tags['UetTag'][0].Id + + # Optionally you can update the name and description of a UetTag with the UpdateUetTags operation. + + output_status_message("UET Tag BEFORE update:\n") + output_uet_tag(uet_tags['UetTag'][0]) + + update_uet_tags=campaign_service.factory.create('ArrayOfUetTag') + update_uet_tag=set_elements_to_none(campaign_service.factory.create('UetTag')) + update_uet_tag.Description="Updated Uet Tag Description" + update_uet_tag.Name="Updated Uet Tag Name " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + update_uet_tag.Id=tag_id + update_uet_tags.UetTag.append(update_uet_tag) + + campaign_service.UpdateUetTags(update_uet_tags) + + uet_tags = campaign_service.GetUetTagsByIds({'long': tag_id }).UetTags + + output_status_message("UET Tag AFTER update:\n") + output_uet_tag(uet_tags['UetTag'][0]) + + # Add conversion goals that depend on the UET Tag Id retreived above. + # Please note that you cannot delete conversion goals. If you want to stop + # tracking conversions for the goal, you can set the goal status to Paused. + + conversion_goals=campaign_service.factory.create('ArrayOfConversionGoal') + + duration_goal=set_elements_to_none(campaign_service.factory.create('DurationGoal')) + duration_goal.ConversionWindowInMinutes = 30 + duration_goal.CountType = 'All' + duration_goal.MinimumDurationInSeconds = 60 + duration_goal.Name = "My Duration Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + duration_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + duration_goal_revenue.Type='FixedValue' + duration_goal_revenue.Value=5.00 + duration_goal_revenue.CurrencyCode=None + duration_goal.Revenue = duration_goal_revenue + duration_goal.Scope = 'Account' + duration_goal.Status = 'Active' + duration_goal.TagId = tag_id + conversion_goals.ConversionGoal.append(duration_goal) + + event_goal=set_elements_to_none(campaign_service.factory.create('EventGoal')) + # The type of user interaction you want to track. + event_goal.ActionExpression = "play" + event_goal.ActionOperator = 'Contains' + # The category of event you want to track. + event_goal.CategoryExpression = "video" + event_goal.CategoryOperator = 'Contains' + event_goal.ConversionWindowInMinutes = 30 + event_goal.CountType = 'All' + # The name of the element that caused the action. + event_goal.LabelExpression = "trailer" + event_goal.LabelOperator = 'Contains' + event_goal.Name = "My Event Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + event_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + event_goal_revenue.Type='FixedValue' + event_goal_revenue.Value=5.00 + event_goal_revenue.CurrencyCode=None + event_goal.Revenue = event_goal_revenue + event_goal.Scope = 'Account' + event_goal.Status = 'Active' + event_goal.TagId = tag_id + # A numerical value associated with that event. + # Could be length of the video played etc. + event_goal.Value = 5.00 + event_goal.ValueOperator = 'Equals' + conversion_goals.ConversionGoal.append(event_goal) + + pages_viewed_per_visit_goal=set_elements_to_none(campaign_service.factory.create('PagesViewedPerVisitGoal')) + pages_viewed_per_visit_goal.ConversionWindowInMinutes = 30 + pages_viewed_per_visit_goal.CountType = 'All' + pages_viewed_per_visit_goal.MinimumPagesViewed = 5 + pages_viewed_per_visit_goal.Name = "My Pages Viewed Per Visit Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + pages_viewed_per_visit_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + pages_viewed_per_visit_goal_revenue.Type='FixedValue' + pages_viewed_per_visit_goal_revenue.Value=5.00 + pages_viewed_per_visit_goal_revenue.CurrencyCode=None + pages_viewed_per_visit_goal.Revenue = pages_viewed_per_visit_goal_revenue + pages_viewed_per_visit_goal.Scope = 'Account' + pages_viewed_per_visit_goal.Status = 'Active' + pages_viewed_per_visit_goal.TagId = tag_id + conversion_goals.ConversionGoal.append(pages_viewed_per_visit_goal) + + url_goal=set_elements_to_none(campaign_service.factory.create('UrlGoal')) + url_goal.ConversionWindowInMinutes = 30 + url_goal.CountType = 'All' + url_goal.Name = "My Url Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + url_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + url_goal_revenue.Type='FixedValue' + url_goal_revenue.Value=5.00 + url_goal_revenue.CurrencyCode=None + url_goal.Revenue = url_goal_revenue + url_goal.Scope = 'Account' + url_goal.Status = 'Active' + url_goal.TagId = tag_id + url_goal.UrlExpression = "contoso" + url_goal.UrlOperator = 'Contains' + conversion_goals.ConversionGoal.append(url_goal) + + app_install_goal=set_elements_to_none(campaign_service.factory.create('AppInstallGoal')) + # You must provide a valid app platform and app store identifier, + # otherwise this goal will not be added successfully. + app_install_goal.AppPlatform = "Windows" + app_install_goal.AppStoreId = "AppStoreIdGoesHere" + app_install_goal.ConversionWindowInMinutes = 30 + app_install_goal.CountType = 'All' + app_install_goal.Name = "My App Install Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + app_install_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + app_install_goal_revenue.Type='FixedValue' + app_install_goal_revenue.Value=5.00 + app_install_goal_revenue.CurrencyCode=None + app_install_goal.Revenue = app_install_goal_revenue + # Account scope is not supported for app install goals. You can + # set scope to Customer or don't set it for the same result. + app_install_goal.Scope = 'Customer' + app_install_goal.Status = 'Active' + # The TagId is inherited from the ConversionGoal base class, + # however, App Install goals do not use a UET tag. + app_install_goal.TagId = None + conversion_goals.ConversionGoal.append(app_install_goal) + + add_conversion_goals_response=campaign_service.AddConversionGoals(ConversionGoals=conversion_goals) + + # Find the conversion goals that were added successfully. + + conversion_goal_ids = [] + for goal_id in add_conversion_goals_response.ConversionGoalIds['long']: + if goal_id is not None: + conversion_goal_ids.append(goal_id) + + output_status_message("List of errors returned from AddConversionGoals (if any):\n"); + output_partial_errors(add_conversion_goals_response.PartialErrors); + + conversion_goal_types='AppInstall ' \ + 'Duration ' \ + 'Event ' \ + 'PagesViewedPerVisit ' \ + 'Url' + + get_conversion_goals = campaign_service.GetConversionGoalsByIds( + ConversionGoalIds={'long': conversion_goal_ids}, + ConversionGoalTypes=conversion_goal_types + ).ConversionGoals + + output_status_message("List of conversion goals BEFORE update:\n") + for conversion_goal in get_conversion_goals['ConversionGoal']: + output_conversion_goal(conversion_goal) + + update_conversion_goals=campaign_service.factory.create('ArrayOfConversionGoal') + + update_duration_goal=set_elements_to_none(campaign_service.factory.create('DurationGoal')) + update_duration_goal.ConversionWindowInMinutes = 60 + update_duration_goal.CountType = 'Unique' + # You can change the conversion goal type e.g. in this example an event goal + # had been created above at index 1. Now we are using the returned identifier + # at index 1 to update the type from EventGoal to DurationGoal. + update_duration_goal.Id=conversion_goal_ids[1] + update_duration_goal.MinimumDurationInSeconds = 120 + update_duration_goal.Name = "My Updated Duration Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + update_duration_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + update_duration_goal_revenue.Type='FixedValue' + update_duration_goal_revenue.Value=10.00 + update_duration_goal_revenue.CurrencyCode=None + update_duration_goal.Revenue = update_duration_goal_revenue + # The Scope cannot be updated, even if you update the goal type. + # You can either send the same value or leave Scope empty. + update_duration_goal.Scope = 'Account' + update_duration_goal.Status = 'Paused' + # You can update the tag as needed. In this example we will explicitly use the same UET tag. + # To keep the UET tag unchanged, you can also leave this element nil or empty. + update_duration_goal.TagId = tag_id + update_conversion_goals.ConversionGoal.append(update_duration_goal) + + update_event_goal=set_elements_to_none(campaign_service.factory.create('EventGoal')) + # For both add and update conversion goal operations, you must include one or more + # of the following events: + # ActionExpression, CategoryExpression, LabelExpression, or Value. + + # For example if you do not include ActionExpression during update, + # any existing ActionOperator and ActionExpression settings will be deleted. + update_event_goal.ActionExpression = None + update_event_goal.ActionOperator = None + update_event_goal.CategoryExpression = "video" + update_event_goal.CategoryOperator = 'Equals' + update_event_goal.Id = conversion_goal_ids[0] + # You cannot update the operator unless you also include the expression. + # The following attempt to update LabelOperator will result in an error. + update_event_goal.LabelExpression = None + update_event_goal.LabelOperator = 'Equals' + update_event_goal.Name = "My Updated Event Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + update_event_goal_revenue=set_elements_to_none(campaign_service.factory.create('ConversionGoalRevenue')) + update_event_goal_revenue.Type='VariableValue' + update_event_goal_revenue.Value=5.00 + update_event_goal_revenue.CurrencyCode=None + update_event_goal.Revenue = update_event_goal_revenue + # You must specify the previous settings unless you want + # them replaced during the update conversion goal operation. + update_event_goal.Value = None + update_event_goal.ValueOperator = 'GreaterThan' + update_conversion_goals.ConversionGoal.append(update_event_goal) + + update_pages_viewed_per_visit_goal=set_elements_to_none(campaign_service.factory.create('PagesViewedPerVisitGoal')) + update_pages_viewed_per_visit_goal.Id = conversion_goal_ids[2] + update_pages_viewed_per_visit_goal.Name = "My Updated Pages Viewed Per Visit Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + update_conversion_goals.ConversionGoal.append(update_pages_viewed_per_visit_goal) + + update_url_goal=set_elements_to_none(campaign_service.factory.create('UrlGoal')) + update_url_goal.Id = conversion_goal_ids[3] + update_url_goal.Name = "My Updated Url Goal " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + # If not specified during update, the previous Url settings are retained. + update_url_goal.UrlExpression = None + update_url_goal.UrlOperator = 'BeginsWith' + update_conversion_goals.ConversionGoal.append(update_url_goal) + + update_conversion_goals_response = campaign_service.UpdateConversionGoals(update_conversion_goals) + + output_status_message("List of errors returned from UpdateConversionGoals (if any):\n"); + output_partial_errors(update_conversion_goals_response.PartialErrors); + + get_conversion_goals = campaign_service.GetConversionGoalsByIds( + ConversionGoalIds={'long': conversion_goal_ids}, + ConversionGoalTypes=conversion_goal_types + ).ConversionGoals + + output_status_message("List of conversion goals AFTER update:\n") + for conversion_goal in get_conversion_goals['ConversionGoal']: + output_conversion_goal(conversion_goal) + + output_status_message("Program execution completed") + + except WebFault as ex: + output_webfault_errors(ex) + except Exception as ex: + output_status_message(ex) \ No newline at end of file diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py index 254a326b..48f7ee01 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py @@ -112,13 +112,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" @@ -321,20 +321,20 @@ def download_locations_file(): get_geo_locations_file_url_response = campaign_service.GetGeoLocationsFileUrl(VERSION, LANGUAGE_LOCALE) - request=urllib2.Request(get_geo_locations_file_url_response.FileUrl) - response=urllib2.urlopen(request) + request=urllib.request.Request(get_geo_locations_file_url_response.FileUrl) + response=urllib.request.urlopen(request) if response.getcode() == 200: download_locations_file() - print("Downloaded the geographical locations to {0}.\n".format(LOCAL_FILE)) + print(("Downloaded the geographical locations to {0}.\n".format(LOCAL_FILE))) output_status_message("Program execution completed") - except urllib2.URLError as ex: + except urllib.error.URLError as ex: if hasattr(ex, 'code'): - print("Error code: {0}".format(ex.code)) + print(("Error code: {0}".format(ex.code))) elif hasattr(ex, 'reason'): - print("Reason: {0}".format(ex.reason)) + print(("Reason: {0}".format(ex.reason))) except WebFault as ex: output_webfault_errors(ex) diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py.bak new file mode 100644 index 00000000..254a326b --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/GeographicalLocations.py.bak @@ -0,0 +1,342 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 import * + +import urllib.request as urllib2 +import sys +import webbrowser +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) +#logging.getLogger('suds.transport.http').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 full path to the local machine's copy of the geographical locations file. + LOCAL_FILE="c:\geolocations\geolocations.csv" + + # The language and locale of the geographical locations file available for download. + # This example uses 'en' (English). Supported locales are 'zh-Hant' (Traditional Chinese), 'en' (English), 'fr' (French), + # 'de' (German), 'it' (Italian), 'pt-BR' (Portuguese - Brazil), and 'es' (Spanish). + + LANGUAGE_LOCALE = "en" + + # The only supported file format version is 1.0. + + VERSION = "1.0" + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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_status_message(message): + print(message) + +def download_locations_file(): + CHUNK=16 * 1024 + with open(LOCAL_FILE, 'wb') as f: + while True: + chunk=response.read(CHUNK) + if not chunk: break + f.write(chunk) + f.flush() + +# Main execution +if __name__ == '__main__': + 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 + + get_geo_locations_file_url_response = campaign_service.GetGeoLocationsFileUrl(VERSION, LANGUAGE_LOCALE) + + request=urllib2.Request(get_geo_locations_file_url_response.FileUrl) + response=urllib2.urlopen(request) + + if response.getcode() == 200: + download_locations_file() + print("Downloaded the geographical locations to {0}.\n".format(LOCAL_FILE)) + + output_status_message("Program execution completed") + + except urllib2.URLError as ex: + if hasattr(ex, 'code'): + print("Error code: {0}".format(ex.code)) + elif hasattr(ex, 'reason'): + print("Reason: {0}".format(ex.reason)) + + 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 8ad65b87..08980aa3 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py @@ -103,13 +103,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py.bak new file mode 100644 index 00000000..8ad65b87 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/KeywordsAds.py.bak @@ -0,0 +1,957 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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) +#logging.getLogger('suds.transport.http').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' + + ALL_AD_TYPES={ + 'AdType': ['AppInstall', 'DynamicSearch', 'ExpandedText', 'Product', 'Text'] + } + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def set_read_only_campaign_elements_to_none(campaign): + if campaign is not None: + campaign.CampaignType=None + campaign.Settings=None + campaign.Status=None + +def output_campaign(campaign): + if campaign is not None: + if hasattr(campaign, 'BiddingScheme'): + output_bidding_scheme(campaign.BiddingScheme) + if hasattr(campaign, 'BudgetId'): + output_status_message("BudgetId: {0}".format(campaign.BudgetId)) + output_status_message("BudgetType: {0}".format(campaign.BudgetType)) + output_status_message("CampaignType: {0}".format(campaign.CampaignType)) + output_status_message("DailyBudget: {0}".format(campaign.DailyBudget)) + output_status_message("Description: {0}".format(campaign.Description)) + output_status_message("ForwardCompatibilityMap: "); + if campaign.ForwardCompatibilityMap is not None: + for pair in campaign.ForwardCompatibilityMap['KeyValuePairOfstringstring']: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(campaign.Id)) + output_status_message("Name: {0}".format(campaign.Name)) + output_status_message("Settings:"); + if campaign.Settings is not None: + for setting in campaign.Settings['Setting']: + if setting.Type == 'ShoppingSetting': + output_status_message("ShoppingSetting:"); + output_status_message("Priority: {0}".format(setting.Priority)) + output_status_message("SalesCountryCode: {0}".format(setting.SalesCountryCode)) + output_status_message("StoreId: {0}".format(setting.StoreId)) + output_status_message("Status: {0}".format(campaign.Status)) + output_status_message("TrackingUrlTemplate: {0}".format(campaign.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: "); + if campaign.UrlCustomParameters is not None and campaign.UrlCustomParameters.Parameters is not None: + for custom_parameter in campaign.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + output_status_message("TimeZone: {0}\n".format(campaign.TimeZone)) + +def output_bidding_scheme(bidding_scheme): + if bidding_scheme is None: + return + if bidding_scheme.Type == 'EnhancedCpc': + output_status_message("BiddingScheme Type: EnhancedCpc") + elif bidding_scheme.Type == 'InheritFromParent': + output_status_message("BiddingScheme Type: InheritFromParent") + elif bidding_scheme.Type == 'ManualCpc': + output_status_message("BiddingScheme Type: ManualCpc") + elif bidding_scheme.Type == 'MaxClicks': + output_status_message("BiddingScheme Type: MaxClicks") + elif bidding_scheme.Type == 'MaxConversions': + output_status_message("BiddingScheme Type: MaxConversions") + elif bidding_scheme.Type == 'TargetCpa': + output_status_message("BiddingScheme Type: TargetCpa") + +def output_budget(budget): + if budget is not None: + output_status_message("Amount: {0}".format(budget.Amount)) + output_status_message("AssociationCount: {0}".format(budget.AssociationCount)) + output_status_message("BudgetType: {0}".format(budget.BudgetType)) + output_status_message("Id: {0}".format(budget.Id)) + output_status_message("Name: {0}\n".format(budget.Name)) + +def output_campaign_ids(campaign_ids): + for id in campaign_ids['long']: + output_status_message("Campaign successfully added and assigned CampaignId {0}\n".format(id)) + +def output_ad_group_ids(ad_group_ids): + for id in ad_group_ids['long']: + output_status_message("AdGroup successfully added and assigned AdGroupId {0}\n".format(id)) + +def output_ad_results(ads, ad_ids, ad_errors): + # Since len(ads['Ad']) and len(ad_errors['long']) usually differ, we will need to adjust the ad_errors index + # as successful indices are counted. + success_count=0 + + # There is an issue in the SUDS 0.6 library, where if Index 0 of ArrayOfNullableOflong is empty it is not returned. + # In this case we will need to adjust the index used to access ad_ids. + suds_index_0_gap=len(ads['Ad']) - len(ad_ids['long']) + + error_index=0 + + for ad_index in range(len(ads['Ad'])): + if ad_errors is not None \ + and ad_errors['BatchError'] is not None \ + and ad_index < len(ad_errors['BatchError']) + success_count \ + and ad_index == ad_errors['BatchError'][error_index].Index: + # One ad may have multiple errors, for example editorial errors in multiple publisher countries + while(error_index < len(ad_errors['BatchError']) and ad_errors['BatchError'][error_index].Index == ad_index): + output_status_message("Ad[{0}] not added due to the following error:".format(ad_index)) + output_status_message("Index: {0}".format(ad_errors['BatchError'][error_index].Index)) + output_status_message("Code: {0}".format(ad_errors['BatchError'][error_index].Code)) + output_status_message("ErrorCode: {0}".format(ad_errors['BatchError'][error_index].ErrorCode)) + output_status_message("Message: {0}".format(ad_errors['BatchError'][error_index].Message)) + if ad_errors['BatchError'][error_index].Type == 'EditorialError': + output_status_message("DisapprovedText: {0}".format(ad_errors['BatchError'][error_index].DisapprovedText)) + output_status_message("Location: {0}".format(ad_errors['BatchError'][error_index].Location)) + output_status_message("PublisherCountry: {0}".format(ad_errors['BatchError'][error_index].PublisherCountry)) + output_status_message("ReasonCode: {0}".format(ad_errors['BatchError'][error_index].ReasonCode)) + error_index=error_index + 1 + elif ad_ids['long'][ad_index - suds_index_0_gap] is not None: + output_status_message("Ad[{0}] successfully added and assigned AdId {1}".format( + ad_index, + ad_ids['long'][ad_index - suds_index_0_gap])) + success_count=success_count + 1 + + if ads['Ad'][ad_index].Type == 'ExpandedText': + output_expanded_text_ad(ads['Ad'][ad_index]) + elif ads['Ad'][ad_index].Type == 'Product': + output_product_ad(ads['Ad'][ad_index]) + elif ads['Ad'][ad_index].Type == 'Text': + output_text_ad(ads['Ad'][ad_index]) + else: + output_status_message("Unknown Ad Type") + + output_status_message('') + +def output_ad(ad): + if ad is not None: + output_status_message("DevicePreference: {0}".format(ad.DevicePreference)) + output_status_message("EditorialStatus: {0}".format(ad.EditorialStatus)) + output_status_message("FinalMobileUrls: ") + if ad.FinalMobileUrls is not None: + for final_mobile_url in ad.FinalMobileUrls['string']: + output_status_message("\t{0}".format(final_mobile_url)) + output_status_message("FinalUrls: ") + if ad.FinalUrls is not None: + for final_url in ad.FinalUrls['string']: + output_status_message("\t{0}".format(final_url)) + output_status_message("ForwardCompatibilityMap: ") + if ad.ForwardCompatibilityMap is not None and len(ad.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in 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(ad.Id)) + output_status_message("Status: {0}".format(ad.Status)) + output_status_message("TrackingUrlTemplate: {0}".format(ad.TrackingUrlTemplate)) + output_status_message("UrlCustomParameters: ") + if ad.UrlCustomParameters is not None and ad.UrlCustomParameters.Parameters is not None: + for custom_parameter in ad.UrlCustomParameters.Parameters['CustomParameter']: + output_status_message("\tKey: {0}".format(custom_parameter.Key)) + output_status_message("\tValue: {0}".format(custom_parameter.Value)) + +def output_expanded_text_ad(ad): + if ad is not None: + # Output inherited properties of the Ad base class. + output_ad(ad) + + # Output properties that are specific to the ExpandedTextAd + output_status_message("DisplayUrl: {0}".format(ad.DisplayUrl)) + output_status_message("Path1: {0}".format(ad.Path1)) + output_status_message("Path2: {0}".format(ad.Path2)) + output_status_message("Text: {0}".format(ad.Text)) + output_status_message("TitlePart1: {0}".format(ad.TitlePart1)) + output_status_message("TitlePart2: {0}".format(ad.TitlePart2)) + +def output_product_ad(ad): + if ad is not None: + # Output inherited properties of the Ad base class. + output_ad(ad) + + # Output properties that are specific to the ProductAd + output_status_message("PromotionalText: {0}".format(ad.PromotionalText)) + +def output_text_ad(ad): + if ad is not None: + # Output inherited properties of the Ad base class. + output_ad(ad) + + # Output properties that are specific to the TextAd + output_status_message("DestinationUrl: {0}".format(ad.DestinationUrl)) + output_status_message("DisplayUrl: {0}".format(ad.DisplayUrl)) + output_status_message("Text: {0}".format(ad.Text)) + output_status_message("Title: {0}".format(ad.Title)) + +def output_keyword_results(keywords, keyword_ids, keyword_errors): + # Since len(keywords['Keyword']) and len(keyword_errors['long']) differ, we will need to adjust the keyword_errors index + # as successful indices are counted. + success_count=0 + + # There is an issue in the SUDS 0.6 library, where if Index 0 of ArrayOfNullableOflong is empty it is not returned. + # In this case we will need to adjust the index used to access keyword_ids. + suds_index_0_gap=len(keywords['Keyword']) - len(keyword_ids['long']) + + error_index=0 + + for keyword_index in range(len(keywords['Keyword'])): + attribute_value=keywords['Keyword'][keyword_index].Text + + if keyword_errors \ + and keyword_index < len(keyword_errors['BatchError']) + success_count \ + and keyword_index == keyword_errors['BatchError'][error_index].Index: + # One keyword may have multiple errors, for example editorial errors in multiple publisher countries + while(error_index < len(keyword_errors['BatchError']) and keyword_errors['BatchError'][error_index].Index == keyword_index): + output_status_message("Keyword[{0}] ({1}) not added due to the following error:".format(keyword_index, attribute_value)) + output_status_message("Index: {0}".format(keyword_errors['BatchError'][error_index].Index)) + output_status_message("Code: {0}".format(keyword_errors['BatchError'][error_index].Code)) + output_status_message("ErrorCode: {0}".format(keyword_errors['BatchError'][error_index].ErrorCode)) + output_status_message("Message: {0}".format(keyword_errors['BatchError'][error_index].Message)) + if keyword_errors['BatchError'][error_index].Type == 'EditorialError': + output_status_message("DisapprovedText: {0}".format(keyword_errors['BatchError'][error_index].DisapprovedText)) + output_status_message("Location: {0}".format(keyword_errors['BatchError'][error_index].Location)) + output_status_message("PublisherCountry: {0}".format(keyword_errors['BatchError'][error_index].PublisherCountry)) + output_status_message("ReasonCode: {0}".format(keyword_errors['BatchError'][error_index].ReasonCode)) + error_index=error_index + 1 + elif keyword_ids['long'][keyword_index - suds_index_0_gap] is not None: + output_status_message("Keyword[{0}] ({1}) successfully added and assigned KeywordId {2}".format( + keyword_index, + attribute_value, + keyword_ids['long'][keyword_index - suds_index_0_gap])) + success_count=success_count + 1 + output_status_message('') + +# Main execution +if __name__ == '__main__': + + 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 + + # Let's create a new budget and share it with a new campaign. + + budgets = campaign_service.factory.create('ArrayOfBudget') + budget=set_elements_to_none(campaign_service.factory.create('Budget')) + budget.Amount = 50 + budget.BudgetType = 'DailyBudgetStandard' + budget.Name = "My Shared Budget " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + budgets.Budget.append(budget) + + add_budgets_response = campaign_service.AddBudgets(budgets) + budget_ids={ + 'long': add_budgets_response.BudgetIds['long'] if add_budgets_response.BudgetIds['long'] else None + } + + # Specify one or more campaigns. + + campaigns=campaign_service.factory.create('ArrayOfCampaign') + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + + # You must choose to set either the shared budget ID or daily amount. + # You can set one or the other, but you may not set both. + campaign.BudgetId = budget_ids['long'][0] if len(budget_ids['long']) > 0 else None + campaign.DailyBudget = None if len(budget_ids['long']) > 0 else 50 + campaign.BudgetType = 'DailyBudgetStandard' + + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving=True # Accepts 'true','false', True, or False + campaign.Status='Paused' + + # You can set your campaign bid strategy to Enhanced CPC (EnhancedCpcBiddingScheme) + # and then, at any time, set an individual ad group or keyword bid strategy to + # Manual CPC (ManualCpcBiddingScheme). + # For campaigns you can use either of the EnhancedCpcBiddingScheme or ManualCpcBiddingScheme objects. + # If you do not set this element, then ManualCpcBiddingScheme is used by default. + campaign_bidding_scheme=set_elements_to_none(campaign_service.factory.create('ns0:EnhancedCpcBiddingScheme')) + campaign.BiddingScheme=campaign_bidding_scheme + + # Used with FinalUrls shown in the expanded text ads that we will add below. + campaign.TrackingUrlTemplate="http://tracker.example.com/?season={_season}&promocode={_promocode}&u={lpurl}" + + campaigns.Campaign.append(campaign) + + ad_groups=campaign_service.factory.create('ArrayOfAdGroup') + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + ad_group.Name="Women's Red Shoes" + ad_group.AdDistribution='Search' + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + ad_group.Network='OwnedAndOperatedAndSyndicatedSearch' + ad_group.Status='Paused' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + search_bid=campaign_service.factory.create('Bid') + search_bid.Amount=0.09 + ad_group.SearchBid=search_bid + ad_group.RemarketingTargetingSetting='TargetAndBid' + ad_group.Language='English' + + # For ad groups you can use either of the InheritFromParentBiddingScheme or ManualCpcBiddingScheme objects. + # If you do not set this element, then InheritFromParentBiddingScheme is used by default. + ad_group_bidding_scheme=set_elements_to_none(campaign_service.factory.create('ns0:ManualCpcBiddingScheme')) + ad_group.BiddingScheme=ad_group_bidding_scheme + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + ad_group.TrackingUrlTemplate=None + + ad_groups.AdGroup.append(ad_group) + + # In this example only the first 3 ads should succeed. + # The TitlePart2 of the fourth ad is empty and not valid, + # and the fifth ad is a duplicate of the second ad. + + ads=campaign_service.factory.create('ArrayOfAd') + + for index in range(5): + expanded_text_ad=set_elements_to_none(campaign_service.factory.create('ExpandedTextAd')) + expanded_text_ad.TitlePart1='Contoso' + expanded_text_ad.TitlePart2='Fast & Easy Setup' + expanded_text_ad.Text='Huge Savings on red shoes.' + expanded_text_ad.Path1='seattle' + expanded_text_ad.Path2='shoe sale' + expanded_text_ad.Type='ExpandedText' + + # With FinalUrls you can separate the tracking template, custom parameters, and + # landing page URLs. + final_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_urls.string.append('http://www.contoso.com/womenshoesale') + expanded_text_ad.FinalUrls=final_urls + + # Final Mobile URLs can also be used if you want to direct the user to a different page + # for mobile devices. + final_mobile_urls=campaign_service.factory.create('ns4:ArrayOfstring') + final_mobile_urls.string.append('http://mobile.contoso.com/womenshoesale') + expanded_text_ad.FinalMobileUrls=final_mobile_urls + + # You could use a tracking template which would override the campaign level + # tracking template. Tracking templates defined for lower level entities + # override those set for higher level entities. + # In this example we are using the campaign level tracking template. + expanded_text_ad.TrackingUrlTemplate=None, + + # Set custom parameters that are specific to this ad, + # and can be used by the ad, ad group, campaign, or account level tracking template. + # In this example we are using the campaign level tracking template. + url_custom_parameters=campaign_service.factory.create('ns0:CustomParameters') + parameters=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter1=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter1.Key='promoCode' + custom_parameter1.Value='PROMO' + str(index) + parameters.CustomParameter.append(custom_parameter1) + custom_parameter2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter2.Key='season' + custom_parameter2.Value='summer' + parameters.CustomParameter.append(custom_parameter2) + url_custom_parameters.Parameters=parameters + expanded_text_ad.UrlCustomParameters=url_custom_parameters + ads.Ad.append(expanded_text_ad) + + ads.Ad[1].TitlePart2="Quick & Easy Setup" + ads.Ad[2].TitlePart2="Fast & Simple Setup" + ads.Ad[3].TitlePart2='' + ads.Ad[4].TitlePart2="Quick & Easy Setup" + + # In this example only the second keyword should succeed. The Text of the first keyword exceeds the limit, + # and the third keyword is a duplicate of the second keyword. + + keywords=campaign_service.factory.create('ArrayOfKeyword') + + for index in range(3): + keyword=set_elements_to_none(campaign_service.factory.create('Keyword')) + keyword.Bid=campaign_service.factory.create('Bid') + keyword.Bid.Amount=0.47 + keyword.Param2='10% Off' + keyword.MatchType='Broad' + keyword.Text='Brand-A Shoes' + + # For keywords you can use either of the InheritFromParentBiddingScheme or ManualCpcBiddingScheme objects. + # If you do not set this element, then InheritFromParentBiddingScheme is used by default. + keyword_bidding_scheme=set_elements_to_none(campaign_service.factory.create('ns0:InheritFromParentBiddingScheme')) + keyword.BiddingScheme=keyword_bidding_scheme + + keywords.Keyword.append(keyword) + + keywords.Keyword[0].Text=( + "Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes " + "Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes " + "Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes Brand-A Shoes" + ) + + # Add the campaign, ad group, keywords, and ads + + add_campaigns_response=campaign_service.AddCampaigns( + AccountId=authorization_data.account_id, + Campaigns=campaigns + ) + campaign_ids={ + 'long': add_campaigns_response.CampaignIds['long'] if add_campaigns_response.CampaignIds['long'] else None + } + output_campaign_ids(campaign_ids) + + add_ad_groups_response=campaign_service.AddAdGroups( + CampaignId=campaign_ids['long'][0], + AdGroups=ad_groups + ) + ad_group_ids={ + 'long': add_ad_groups_response.AdGroupIds['long'] if add_ad_groups_response.AdGroupIds['long'] else None + } + output_ad_group_ids(ad_group_ids) + + add_ads_response=campaign_service.AddAds( + AdGroupId=ad_group_ids['long'][0], + Ads=ads + ) + ad_ids={ + 'long': add_ads_response.AdIds['long'] if add_ads_response.AdIds['long'] else None + } + ad_errors={ + 'BatchError': add_ads_response.PartialErrors['BatchError'] if add_ads_response.PartialErrors else None + } + output_ad_results(ads, ad_ids, ad_errors) + + add_keywords_response=campaign_service.AddKeywords( + AdGroupId=ad_group_ids['long'][0], + Keywords=keywords + ) + keyword_ids={ + 'long': add_keywords_response.KeywordIds['long'] if add_keywords_response.KeywordIds else None + } + keyword_errors={ + 'BatchError': add_keywords_response.PartialErrors['BatchError'] if add_keywords_response.PartialErrors else None + } + + 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 + # the budget amount of a campaign that has a shared budget, the service will return + # the CampaignServiceCannotUpdateSharedBudget error code. + + get_campaigns=campaign_service.GetCampaignsByAccountId( + AccountId=authorization_data.account_id, + CampaignType=['SearchAndContent Shopping'], + ReturnAdditionalFields=['BiddingScheme BudgetId'] + ) + + update_campaigns = campaign_service.factory.create('ArrayOfCampaign') + update_budgets = campaign_service.factory.create('ArrayOfBudget') + get_campaign_ids = [] + get_budget_ids = [] + + # Increase existing budgets by 20% + for campaign in get_campaigns['Campaign']: + + # If the campaign has a shared budget, let's add the budget ID to the list we will update later. + if campaign is not None and hasattr(campaign, 'BudgetId') and campaign.BudgetId > 0: + get_budget_ids.append(campaign.BudgetId) + # If the campaign has its own budget, let's add it to the list of campaigns to update later. + elif campaign is not None: + update_campaigns.Campaign.append(campaign) + + # Update shared budgets in Budget objects. + if get_budget_ids is not None and len(get_budget_ids) > 0: + distinct_budget_ids = {'long': list(set(get_budget_ids))} + get_budgets = campaign_service.GetBudgetsByIds( + BudgetIds=distinct_budget_ids + ).Budgets + + output_status_message("List of shared budgets BEFORE update:\n") + for budget in get_budgets['Budget']: + output_status_message("Budget:") + output_budget(budget) + + output_status_message("List of campaigns that share each budget:\n"); + get_campaign_id_collection = campaign_service.GetCampaignIdsByBudgetIds(distinct_budget_ids).CampaignIdCollection + + index=0 + + for index in range(len(get_campaign_id_collection['IdCollection'])): + output_status_message("BudgetId: {0}".format(distinct_budget_ids['long'][index])) + output_status_message("Campaign Ids:"); + if get_campaign_id_collection['IdCollection'][index] is not None: + for id in get_campaign_id_collection['IdCollection'][index].Ids['long']: + output_status_message("\t{0}".format(id)) + index=index+1 + + for budget in get_budgets['Budget']: + if budget is not None: + # Increase budget by 20 % + budget.Amount *= 1.2 + update_budgets.Budget.append(budget) + campaign_service.UpdateBudgets(Budgets=update_budgets) + + get_budgets = campaign_service.GetBudgetsByIds( + BudgetIds=distinct_budget_ids + ).Budgets + + output_status_message("List of shared budgets AFTER update:\n"); + for budget in get_budgets['Budget']: + output_status_message("Budget:") + output_budget(budget) + + # Update unshared budgets in Campaign objects. + if update_campaigns is not None: + + # The UpdateCampaigns operation only accepts 100 Campaign objects per call. + # To simply the example we will update the first 100. + update_100_campaigns = update_campaigns['Campaign'][:100] + update_campaigns = campaign_service.factory.create('ArrayOfCampaign') + for campaign in update_100_campaigns: + update_campaigns.Campaign.append(campaign) + + output_status_message("List of campaigns with unshared budget BEFORE budget update:\n"); + for campaign in update_campaigns['Campaign']: + output_status_message("Campaign:"); + output_campaign(campaign) + set_read_only_campaign_elements_to_none(campaign) + + # Increase budget by 20 % + campaign.DailyBudget *= 1.2 + + get_campaign_ids.append(campaign.Id) + + campaign_service.UpdateCampaigns( + AccountId=authorization_data.account_id, + Campaigns=update_campaigns + ); + + get_campaigns=campaign_service.GetCampaignsByIds( + AccountId=authorization_data.account_id, + CampaignIds={'long': get_campaign_ids}, + CampaignType=['SearchAndContent Shopping'], + ReturnAdditionalFields=['BiddingScheme BudgetId'] + ).Campaigns + + output_status_message("List of campaigns with unshared budget AFTER budget update:\n"); + for campaign in get_campaigns['Campaign']: + output_status_message("Campaign:"); + output_campaign(campaign) + + # Update the Text for the 3 successfully created ads, and update some UrlCustomParameters. + update_ads=campaign_service.factory.create('ArrayOfAd') + + # Set the UrlCustomParameters element to null or empty to retain any + # existing custom parameters. + expanded_text_ad_0=set_elements_to_none(campaign_service.factory.create('ExpandedTextAd')) + expanded_text_ad_0.Id=ad_ids['long'][0] + expanded_text_ad_0.Text='Huge Savings on All Red Shoes.' + expanded_text_ad_0.UrlCustomParameters=None + expanded_text_ad_0.Type='ExpandedText' + update_ads.Ad.append(expanded_text_ad_0) + + # To remove all custom parameters, set the Parameters element of the + # CustomParameters object to null or empty. + expanded_text_ad_1=set_elements_to_none(campaign_service.factory.create('ExpandedTextAd')) + expanded_text_ad_1.Id=ad_ids['long'][1] + expanded_text_ad_1.Text='Huge Savings on All Red Shoes.' + url_custom_parameters_1=campaign_service.factory.create('ns0:CustomParameters') + parameters_1=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter_1=campaign_service.factory.create('ns0:CustomParameter') + # Set the CustomParameter to None or leave unspecified to have the same effect + #custom_parameter_1=None + parameters_1.CustomParameter.append(custom_parameter_1) + url_custom_parameters_1.Parameters=parameters_1 + expanded_text_ad_1.UrlCustomParameters=url_custom_parameters_1 + expanded_text_ad_1.Type='ExpandedText' + update_ads.Ad.append(expanded_text_ad_1) + + # To remove a subset of custom parameters, specify the custom parameters that + # you want to keep in the Parameters element of the CustomParameters object. + expanded_text_ad_2=set_elements_to_none(campaign_service.factory.create('ExpandedTextAd')) + expanded_text_ad_2.Id=ad_ids['long'][2] + expanded_text_ad_2.Text='Huge Savings on All Red Shoes.' + url_custom_parameters_2=campaign_service.factory.create('ns0:CustomParameters') + parameters_2=campaign_service.factory.create('ns0:ArrayOfCustomParameter') + custom_parameter_2=campaign_service.factory.create('ns0:CustomParameter') + custom_parameter_2.Key='promoCode' + custom_parameter_2.Value='updatedpromo' + parameters_2.CustomParameter.append(custom_parameter_2) + url_custom_parameters_2.Parameters=parameters_2 + expanded_text_ad_2.UrlCustomParameters=url_custom_parameters_2 + expanded_text_ad_2.Type='ExpandedText' + update_ads.Ad.append(expanded_text_ad_2) + + # As an exercise you can step through using the debugger and view the results. + + campaign_service.GetAdsByAdGroupId( + AdGroupId=ad_group_ids['long'][0], + AdTypes=ALL_AD_TYPES + ) + campaign_service.UpdateAds( + AdGroupId=ad_group_ids['long'][0], + Ads=update_ads + ) + campaign_service.GetAdsByAdGroupId( + AdGroupId=ad_group_ids['long'][0], + AdTypes=ALL_AD_TYPES + ) + + update_keywords=campaign_service.factory.create('ArrayOfKeyword') + update_keyword=set_elements_to_none(campaign_service.factory.create('Keyword')) + update_keyword.Id=keyword_ids['long'][0] + + # You can set the Bid.Amount property to change the keyword level bid. + update_keyword.Bid=campaign_service.factory.create('Bid') + update_keyword.Bid.Amount=0.46 + + # When using the Campaign Management service with the Bing Ads Python SDK, + # if you want to inherit the ad group level bid, instead of using the keyword level bid, + # the service expects that you would have set the Bid.Amount null (new empty Bid). However, + # it is important to note that SUDS (used by the Bing Ads Python SDK) does not allow the + # Bid.Amount property to be null, so you will need to delete the keyword and then add a new + # keyword without the Bid set, or with Bid set to None. + + # We recommend that you use the BulkServiceManager for keyword updates, i.e. upload BulkKeyword entities. + # With the BulkKeyword upload, you won't have to delete and add keywords to inherit from the ad group level bid, + # and you also have the flexibility of updating millions of keywords across all campaigns in your account. + # For examples of how to use the Bulk service for keyword updates, please see BulkKeywordsAds.py. + + # When using the Campaign Management service with the Bing Ads Python SDK, + # if the Bid property is not specified or is null, your keyword bid will not be updated. + #update_keyword.Bid=None + + update_keywords.Keyword.append(update_keyword) + + campaign_service.GetKeywordsByAdGroupId( + AdGroupId=ad_group_ids['long'][0], + ReturnAdditionalFields=['BiddingScheme'] + ) + campaign_service.UpdateKeywords( + AdGroupId=ad_group_ids['long'][0], + Keywords=update_keywords + ) + campaign_service.GetKeywordsByAdGroupId( + AdGroupId=ad_group_ids['long'][0], + ReturnAdditionalFields=['BiddingScheme'] + ) + + # Delete the campaign, ad group, keyword, and ad that were previously added. + # You should remove this line if you want to view the added entities in the + # Bing Ads web application or another tool. + + campaign_service.DeleteCampaigns( + AccountId=authorization_data.account_id, + CampaignIds=campaign_ids + ) + for campaign_id in campaign_ids['long']: + output_status_message("Deleted CampaignId {0}\n".format(campaign_id)) + + # This sample will attempt to delete the budget that was created above. + if len(budget_ids['long']) > 0: + campaign_service.DeleteBudgets( + BudgetIds=budget_ids + ) + for budget_id in budget_ids['long']: + output_status_message("Deleted BudgetId {0}\n".format(budget_id)) + + output_status_message("Program execution completed") + + except WebFault as ex: + output_webfault_errors(ex) + except Exception as ex: + output_status_message(ex) \ No newline at end of file diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py index ccdb5f0d..9466c283 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py @@ -98,13 +98,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py.bak new file mode 100644 index 00000000..ccdb5f0d --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/NegativeKeywords.py.bak @@ -0,0 +1,745 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_campaign_ids(campaign_ids): + for id in campaign_ids['long']: + output_status_message("Campaign successfully added and assigned CampaignId {0}\n".format(id)) + +def output_negative_keyword_ids(id_collections): + if not hasattr(id_collections, 'IdCollection'): + return None + for index in range(0, len(id_collections['IdCollection'])): + output_status_message("NegativeKeyword Ids at entity index {0}:\n".format(index)) + for id in id_collections['IdCollection'][index].Ids['long']: + output_status_message("\tId: {0}\n".format(id)) + +def output_entity_negative_keywords(entity_negative_keywords): + if not hasattr(entity_negative_keywords, 'EntityNegativeKeyword'): + return None + output_status_message("EntityNegativeKeyword item:\n") + for entity_negative_keyword in entity_negative_keywords['EntityNegativeKeyword']: + output_status_message("\tEntityId: {0}".format(entity_negative_keyword.EntityId)) + output_status_message("\tEntityType: {0}\n".format(entity_negative_keyword.EntityType)) + output_negative_keywords(entity_negative_keyword.NegativeKeywords) + +def get_and_output_shared_entity_identifiers(shared_entity_type): + shared_entities=campaign_service.GetSharedEntitiesByAccountId(SharedEntityType=shared_entity_type) + if not hasattr(shared_entities, 'SharedEntity'): + return None + shared_entity_ids=[] + index=0 + for shared_entity in shared_entities['SharedEntity']: + shared_entity_ids.append(shared_entity.Id) + output_status_message( + "SharedEntity[{0}] ({1}) has SharedEntity Id {2}.\n".format( + index, + shared_entity.Name, + shared_entity.Id + ) + ) + index=index+1 + return shared_entity_ids + +def output_negative_keywords(negative_keywords): + if not hasattr(negative_keywords, 'NegativeKeyword'): + return None + output_status_message("NegativeKeyword item:\n") + for negative_keyword in negative_keywords['NegativeKeyword']: + output_status_message("\tText: {0}".format(negative_keyword.Text)) + output_status_message("\tId: {0}".format(negative_keyword.Id)) + output_status_message("\tMatchType: {0}\n".format(negative_keyword.MatchType)) + +def output_negative_keyword_results( + shared_list_id, + shared_list_items, + shared_list_item_ids, + partial_errors + ): + if not hasattr(shared_list_items, 'SharedListItem'): + return None + for index in range(0, len(shared_list_items['SharedListItem'])-1): + # Determine if the SharedListItem is a NegativeKeyword. + if shared_list_items['SharedListItem'][index].Type == 'NegativeKeyword': + # Determine if the corresponding index has a valid identifier + if shared_list_item_ids['long'][index] > 0: + output_status_message( + "NegativeKeyword[{0}] ({1}) successfully added to NegativeKeywordList ({2}) and assigned Negative Keyword Id {3}.".format( + index, + shared_list_items['SharedListItem'][index].Text, + shared_list_id, + shared_list_item_ids['long'][index] + ) + ) + else: + output_status_message("SharedListItem is not a NegativeKeyword.") + output_partial_errors(partial_errors) + +def output_shared_entity_associations(associations): + if not hasattr(associations, 'SharedEntityAssociation'): + return None + output_status_message("SharedEntityAssociation item:\n") + for shared_entity_association in associations['SharedEntityAssociation']: + output_status_message("\tEntityId: {0}".format(shared_entity_association.EntityId)) + output_status_message("\tEntityType: {0}".format(shared_entity_association.EntityType)) + output_status_message("\tSharedEntityId: {0}".format(shared_entity_association.SharedEntityId)) + output_status_message("\tSharedEntityType: {0}\n".format(shared_entity_association.SharedEntityType)) + +def output_partial_errors(partial_errors): + if not hasattr(partial_errors, 'BatchError'): + return None + output_status_message("BatchError (PartialErrors) item:\n") + for error in partial_errors['BatchError']: + output_status_message("\tIndex: {0}".format(error.Index)) + output_status_message("\tCode: {0}".format(error.Code)) + output_status_message("\tErrorCode: {0}".format(error.ErrorCode)) + output_status_message("\tMessage: {0}\n".format(error.Message)) + + # In the case of an EditorialError, more details are available + if error.Type == "EditorialError" and error.ErrorCode == "CampaignServiceEditorialValidationError": + output_status_message("\tDisapprovedText: {0}".format(error.DisapprovedText)) + output_status_message("\tLocation: {0}".format(error.Location)) + output_status_message("\tPublisherCountry: {0}".format(error.PublisherCountry)) + output_status_message("\tReasonCode: {0}\n".format(error.ReasonCode)) + +def output_nested_partial_errors(nested_partial_errors): + if not hasattr(nested_partial_errors, 'BatchErrorCollection'): + return None + output_status_message("BatchErrorCollection (NestedPartialErrors) item:\n") + for collection in nested_partial_errors['BatchErrorCollection']: + # The top level list index corresponds to the campaign or ad group index identifier. + if collection is not None: + if hasattr(collection, 'Code'): + output_status_message("\tIndex: {0}".format(collection.Index)) + output_status_message("\tCode: {0}".format(collection.Code)) + output_status_message("\tErrorCode: {0}".format(collection.ErrorCode)) + output_status_message("\tMessage: {0}\n".format(collection.Message)) + + # The nested list of batch errors would include any errors specific to the negative keywords + # that you attempted to add or remove from the campaign or ad group. + for error in collection.BatchErrors['BatchError']: + output_status_message("\tIndex: {0}".format(error.Index)) + output_status_message("\tCode: {0}".format(error.Code)) + output_status_message("\tErrorCode: {0}".format(error.ErrorCode)) + output_status_message("\tMessage: {0}\n".format(error.Message)) + + # In the case of an EditorialError, more details are available + if error.Type == "EditorialError" and error.ErrorCode == "CampaignServiceEditorialValidationError": + output_status_message("\tDisapprovedText: {0}".format(error.DisapprovedText)) + output_status_message("\tLocation: {0}".format(error.Location)) + output_status_message("\tPublisherCountry: {0}".format(error.PublisherCountry)) + output_status_message("\tReasonCode: {0}\n".format(error.ReasonCode)) + +# Main execution +if __name__ == '__main__': + + 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 + + # Add a campaign that will later be associated with negative keywords. + + campaigns=campaign_service.factory.create('ArrayOfCampaign') + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving='true' # Accepts 'true', 'false', True, or False + campaign.Status='Paused' + campaigns.Campaign.append(campaign) + + add_campaigns_response=campaign_service.AddCampaigns( + AccountId=authorization_data.account_id, + Campaigns=campaigns + ) + campaign_ids={ + 'long': add_campaigns_response.CampaignIds['long'] if add_campaigns_response.CampaignIds['long'] else None + } + + output_campaign_ids(campaign_ids) + + # You may choose to associate an exclusive set of negative keywords to an individual campaign + # or ad group. An exclusive set of negative keywords cannot be shared with other campaigns + # or ad groups. This example only associates negative keywords with a campaign. + + entity_negative_keywords=campaign_service.factory.create('ArrayOfEntityNegativeKeyword') + entity_negative_keyword=set_elements_to_none(campaign_service.factory.create('EntityNegativeKeyword')) + entity_negative_keyword.EntityId=campaign_ids['long'][0] + entity_negative_keyword.EntityType="Campaign" + negative_keywords=campaign_service.factory.create('ArrayOfNegativeKeyword') + negative_keyword=campaign_service.factory.create('NegativeKeyword') + negative_keyword.MatchType='Phrase' + negative_keyword.Text="auto" + negative_keywords.NegativeKeyword.append(negative_keyword) + entity_negative_keyword.NegativeKeywords=negative_keywords + entity_negative_keywords.EntityNegativeKeyword.append(entity_negative_keyword) + + add_negative_keywords_to_entities_response=campaign_service.AddNegativeKeywordsToEntities( + EntityNegativeKeywords=entity_negative_keywords + ) + + output_negative_keyword_ids(add_negative_keywords_to_entities_response.NegativeKeywordIds) + output_nested_partial_errors(add_negative_keywords_to_entities_response.NestedPartialErrors) + + if add_negative_keywords_to_entities_response.NestedPartialErrors is None \ + and len(add_negative_keywords_to_entities_response.NestedPartialErrors['BatchErrorCollection']) == 0: + output_status_message("Added an exclusive set of negative keywords to the Campaign.\n") + output_negative_keyword_ids(add_negative_keywords_to_entities_response.NegativeKeywordIds) + else: + output_nested_partial_errors(add_negative_keywords_to_entities_response.NestedPartialErrors) + + get_negative_keywords_by_entity_ids_response=campaign_service.GetNegativeKeywordsByEntityIds( + EntityIds=campaign_ids, + EntityType="Campaign", + ParentEntityId=authorization_data.account_id + ) + + output_entity_negative_keywords(get_negative_keywords_by_entity_ids_response.EntityNegativeKeywords) + output_partial_errors(get_negative_keywords_by_entity_ids_response.PartialErrors) + + if hasattr(get_negative_keywords_by_entity_ids_response.PartialErrors, 'BatchError'): + output_partial_errors(get_negative_keywords_by_entity_ids_response.PartialErrors) + else: + output_status_message("Retrieved an exclusive set of negative keywords for the Campaign.\n") + output_entity_negative_keywords(get_negative_keywords_by_entity_ids_response.EntityNegativeKeywords) + ''' + if get_negative_keywords_by_entity_ids_response.PartialErrors is None \ + and len(get_negative_keywords_by_entity_ids_response.PartialErrors) == 0: + output_status_message("Retrieved an exclusive set of negative keywords for the Campaign.\n") + output_entity_negative_keywords(get_negative_keywords_by_entity_ids_response.EntityNegativeKeywords) + else: + output_partial_errors(get_negative_keywords_by_entity_ids_response.PartialErrors) + ''' + # If you attempt to delete a negative keyword without an identifier the operation will + # succeed but will return partial errors corresponding to the index of the negative keyword + # that was not deleted. + nested_partial_errors=campaign_service.DeleteNegativeKeywordsFromEntities( + EntityNegativeKeywords=entity_negative_keywords + ) + + if nested_partial_errors is None or len(nested_partial_errors) == 0: + output_status_message("Deleted an exclusive set of negative keywords from the Campaign.\n") + else: + output_status_message("Attempt to DeleteNegativeKeywordsFromEntities without NegativeKeyword identifier partially fails by design.") + output_nested_partial_errors(nested_partial_errors) + + # Delete the negative keywords with identifiers that were returned above. + nested_partial_errors=campaign_service.DeleteNegativeKeywordsFromEntities(get_negative_keywords_by_entity_ids_response.EntityNegativeKeywords) + if nested_partial_errors is None or len(nested_partial_errors) == 0: + output_status_message("Deleted an exclusive set of negative keywords from the Campaign.\n") + else: + output_nested_partial_errors(nested_partial_errors) + + # Negative keywords can also be added and deleted from a shared negative keyword list. + # The negative keyword list can be shared or associated with multiple campaigns. + # NegativeKeywordList inherits from SharedList which inherits from SharedEntity. + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Name="My Negative Keyword List " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + negative_keyword_list.Type="NegativeKeywordList" + + negative_keywords=campaign_service.factory.create('ArrayOfSharedListItem') + negative_keyword_1=set_elements_to_none(campaign_service.factory.create('NegativeKeyword')) + negative_keyword_1.Text="car" + negative_keyword_1.Type="NegativeKeyword" + negative_keyword_1.MatchType='Exact' + negative_keywords.SharedListItem.append(negative_keyword_1) + negative_keyword_2=set_elements_to_none(campaign_service.factory.create('NegativeKeyword')) + negative_keyword_2.Text="car" + negative_keyword_2.Type="NegativeKeyword" + negative_keyword_2.MatchType='Phrase' + negative_keywords.SharedListItem.append(negative_keyword_2) + + # You can create a new list for negative keywords with or without negative keywords. + + add_shared_entity_response=campaign_service.AddSharedEntity( + SharedEntity=negative_keyword_list, + ListItems=negative_keywords + ) + shared_entity_id=add_shared_entity_response.SharedEntityId + list_item_ids=add_shared_entity_response.ListItemIds + + output_status_message("NegativeKeywordList successfully added to account library and assigned identifer {0}\n".format(shared_entity_id)) + + output_negative_keyword_results( + shared_entity_id, + negative_keywords, + list_item_ids, + add_shared_entity_response.PartialErrors + ) + + output_status_message("Negative keywords currently in NegativeKeywordList:") + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + negative_keywords=campaign_service.GetListItemsBySharedList(SharedList=negative_keyword_list) + if negative_keywords is None or len(negative_keywords) == 0: + output_status_message("None\n") + else: + output_negative_keywords(negative_keywords) + + # To update the list of negative keywords, you must either add or remove from the list + # using the respective AddListItemsToSharedList or DeleteListItemsFromSharedList operations. + # To remove the negative keywords from the list pass the negative keyword identifers + # and negative keyword list (SharedEntity) identifer. + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + partial_errors=campaign_service.DeleteListItemsFromSharedList( + ListItemIds=list_item_ids, + SharedList=negative_keyword_list + ) + if partial_errors is None: + output_status_message("Deleted most recently added negative keywords from negative keyword list.\n") + else: + output_partial_errors(partial_errors) + + output_status_message("Negative keywords currently in NegativeKeywordList:") + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + negative_keywords=campaign_service.GetListItemsBySharedList(SharedList=negative_keyword_list) + if negative_keywords is None or len(negative_keywords) == 0: + output_status_message("None\n") + else: + output_negative_keywords(negative_keywords) + + # Whether you created the list with or without negative keywords, more can be added + # using the AddListItemsToSharedList operation. + + negative_keywords=campaign_service.factory.create('ArrayOfSharedListItem') + negative_keyword_1=set_elements_to_none(campaign_service.factory.create('NegativeKeyword')) + negative_keyword_1.Text="auto" + negative_keyword_1.Type="NegativeKeyword" + negative_keyword_1.MatchType='Exact' + negative_keywords.SharedListItem.append(negative_keyword_1) + negative_keyword_2=set_elements_to_none(campaign_service.factory.create('NegativeKeyword')) + negative_keyword_2.Text="auto" + negative_keyword_2.Type="NegativeKeyword" + negative_keyword_2.MatchType='Phrase' + negative_keywords.SharedListItem.append(negative_keyword_2) + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + + add_list_items_to_shared_list_response=campaign_service.AddListItemsToSharedList( + ListItems=negative_keywords, + SharedList=negative_keyword_list + ) + list_item_ids=add_list_items_to_shared_list_response.ListItemIds + + output_negative_keyword_results( + shared_entity_id, + negative_keywords, + list_item_ids, + add_list_items_to_shared_list_response.PartialErrors + ) + + output_status_message("Negative keywords currently in NegativeKeywordList:") + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + negative_keywords=campaign_service.GetListItemsBySharedList(SharedList=negative_keyword_list) + if negative_keywords is None or len(negative_keywords) == 0: + output_status_message("None\n") + else: + output_negative_keywords(negative_keywords) + + # You can update the name of the negative keyword list. + + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + negative_keyword_list.Name="My Updated Negative Keyword List" + negative_keyword_list.Type="NegativeKeywordList" + + negative_keyword_lists=campaign_service.factory.create('ArrayOfSharedEntity') + negative_keyword_list=set_elements_to_none(campaign_service.factory.create('NegativeKeywordList')) + negative_keyword_list.Id=shared_entity_id + negative_keyword_lists.SharedEntity.append(negative_keyword_list) + partial_errors=campaign_service.UpdateSharedEntities(SharedEntities=negative_keyword_lists) + if partial_errors is None: + output_status_message("Updated Negative Keyword List Name to {0}.\n".format(negative_keyword_list.Name)) + else: + output_partial_errors(partial_errors) + + # Get and print the negative keyword lists and return the list of identifiers. + + SHARED_ENTITY_TYPE="NegativeKeywordList" + shared_entity_ids=get_and_output_shared_entity_identifiers(SHARED_ENTITY_TYPE) + + # Negative keywords were added to the negative keyword list above. You can associate the + # shared list of negative keywords with a campaign with or without negative keywords. + # Shared negative keyword lists cannot be associated with an ad group. An ad group can only + # be assigned an exclusive set of negative keywords. + + shared_entity_associations=campaign_service.factory.create('ArrayOfSharedEntityAssociation') + shared_entity_association=set_elements_to_none(campaign_service.factory.create('SharedEntityAssociation')) + shared_entity_association.EntityId=campaign_ids['long'][0] + shared_entity_association.EntityType="Campaign" + shared_entity_association.SharedEntityId=shared_entity_id + shared_entity_association.SharedEntityType="NegativeKeywordList" + shared_entity_associations.SharedEntityAssociation.append(shared_entity_association) + + partial_errors=campaign_service.SetSharedEntityAssociations(Associations=shared_entity_associations) + if partial_errors is None: + output_status_message( + "Associated CampaignId {0} with Negative Keyword List Id {1}.\n".format(campaign_ids['long'][0], shared_entity_id) + ) + else: + output_partial_errors(partial_errors) + + # Get and print the associations either by Campaign or NegativeKeywordList identifier. + get_shared_entity_associations_by_entity_ids_response=campaign_service.GetSharedEntityAssociationsByEntityIds( + EntityIds=campaign_ids, + EntityType="Campaign", + SharedEntityType="NegativeKeywordList" + ) + output_shared_entity_associations(get_shared_entity_associations_by_entity_ids_response.Associations) + output_partial_errors(get_shared_entity_associations_by_entity_ids_response.PartialErrors) + + # Currently the GetSharedEntityAssociationsBySharedEntityIds operation accepts only one shared entity identifier in the list. + get_shared_entity_associations_by_shared_entity_ids_response=campaign_service.GetSharedEntityAssociationsBySharedEntityIds( + EntityType="Campaign", + SharedEntityIds={'long': [shared_entity_ids[len(shared_entity_ids) - 1]]}, + SharedEntityType="NegativeKeywordList" + ) + output_shared_entity_associations(get_shared_entity_associations_by_shared_entity_ids_response.Associations) + output_partial_errors(get_shared_entity_associations_by_shared_entity_ids_response.PartialErrors) + + # Explicitly delete the association between the campaign and the negative keyword list. + + partial_errors=campaign_service.DeleteSharedEntityAssociations(Associations=shared_entity_associations) + if partial_errors is None: + output_status_message("Deleted NegativeKeywordList associations\n") + else: + output_partial_errors(partial_errors) + + # Delete the campaign and any remaining assocations. + + campaign_service.DeleteCampaigns( + AccountId=authorization_data.account_id, + CampaignIds=campaign_ids + ) + + for campaign_id in campaign_ids['long']: + output_status_message("Deleted CampaignId {0}\n".format(campaign_id)) + + # DeleteCampaigns does not delete the negative keyword list from the account's library. + # Call the DeleteSharedEntities operation to delete the shared entities. + + negative_keyword_lists=campaign_service.factory.create('ArrayOfSharedEntity') + negative_keyword_list=campaign_service.factory.create('NegativeKeywordList') + negative_keyword_list.Id=shared_entity_id + negative_keyword_lists.SharedEntity.append(negative_keyword_list) + partial_errors=campaign_service.DeleteSharedEntities(SharedEntities=negative_keyword_lists) + if partial_errors is None: + output_status_message("Deleted Negative Keyword List (SharedEntity) Id {0}\n".format(shared_entity_id)) + else: + output_partial_errors(partial_errors) + + 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/RemarketingLists.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/RemarketingLists.py index 8cb0caef..27a9f84b 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/RemarketingLists.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/RemarketingLists.py @@ -99,13 +99,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/RemarketingLists.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/RemarketingLists.py.bak new file mode 100644 index 00000000..8cb0caef --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/RemarketingLists.py.bak @@ -0,0 +1,487 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_campaign_ids(campaign_ids): + for id in campaign_ids['long']: + output_status_message("Campaign successfully added and assigned CampaignId {0}\n".format(id)) + +def output_ad_group_ids(ad_group_ids): + for id in ad_group_ids['long']: + output_status_message("AdGroup successfully added and assigned AdGroupId {0}\n".format(id)) + +def output_remarketing_list(remarketing_list): + if remarketing_list is not None: + output_status_message("Description: {0}".format(remarketing_list.Description)) + output_status_message("ForwardCompatibilityMap: ") + if remarketing_list.ForwardCompatibilityMap is not None and len(remarketing_list.ForwardCompatibilityMap.KeyValuePairOfstringstring) > 0: + for pair in remarketing_list.ForwardCompatibilityMap: + output_status_message("Key: {0}".format(pair.key)) + output_status_message("Value: {0}".format(pair.value)) + output_status_message("Id: {0}".format(remarketing_list.Id)) + output_status_message("MembershipDuration: {0}".format(remarketing_list.MembershipDuration)) + output_status_message("Name: {0}".format(remarketing_list.Name)) + output_status_message("ParentId: {0}".format(remarketing_list.ParentId)) + output_status_message("Scope: {0}".format(remarketing_list.Scope)) + output_status_message("TagId: {0}".format(remarketing_list.TagId)) + +def output_ad_group_remarketing_list_association(ad_group_remarketing_list_association): + if ad_group_remarketing_list_association is not None: + output_status_message("AdGroupId: {0}".format(ad_group_remarketing_list_association.AdGroupId)) + output_status_message("BidAdjustment: {0}".format(ad_group_remarketing_list_association.BidAdjustment)) + output_status_message("Id: {0}".format(ad_group_remarketing_list_association.Id)) + output_status_message("RemarketingListId: {0}".format(ad_group_remarketing_list_association.RemarketingListId)) + output_status_message("Status: {0}".format(ad_group_remarketing_list_association.Status)) + +# Main execution +if __name__ == '__main__': + + 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 + + # To discover all remarketing lists that the user can associate with ad groups in the current account (per CustomerAccountId header), + # set RemarketingListIds to null when calling the GetRemarketingLists operation. + remarketing_lists=campaign_service.GetRemarketingLists( + RemarketingListIds = None + ).RemarketingLists + + # You must already have at least one remarketing list for the remainder of this example. + # The Bing Ads API does not support remarketing list add, update, or delete operations. + + if (len(remarketing_lists) < 1): + output_status_message("You do not have any remarketing lists that the current user can associate with ad groups.\n") + sys.exit(0) + + # Add an ad group in a campaign. The ad group will later be associated with remarketing lists. + + campaigns=campaign_service.factory.create('ArrayOfCampaign') + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving='true' # Accepts 'true', 'false', True, or False + campaign.Status='Paused' + + campaigns.Campaign.append(campaign) + + ad_groups=campaign_service.factory.create('ArrayOfAdGroup') + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + ad_group.Name="Women's Red Shoes" + ad_group.AdDistribution='Search' + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + ad_group.Network='OwnedAndOperatedAndSyndicatedSearch' + ad_group.Status='Paused' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + search_bid=campaign_service.factory.create('Bid') + search_bid.Amount=0.09 + ad_group.SearchBid=search_bid + ad_group.Language='English' + + # Applicable for all remarketing lists that are associated with this ad group. TargetAndBid indicates + # that you want to show ads only to people included in the remarketing list, with the option to change + # the bid amount. Ads in this ad group will only show to people included in the remarketing list. + ad_group.RemarketingTargetingSetting='TargetAndBid' + + ad_groups.AdGroup.append(ad_group) + + # Add the campaign and ad group + + add_campaigns_response=campaign_service.AddCampaigns( + AccountId=authorization_data.account_id, + Campaigns=campaigns + ) + campaign_ids={ + 'long': add_campaigns_response.CampaignIds['long'] if add_campaigns_response.CampaignIds['long'] else None + } + output_campaign_ids(campaign_ids) + + add_ad_groups_response=campaign_service.AddAdGroups( + CampaignId=campaign_ids['long'][0], + AdGroups=ad_groups + ) + ad_group_ids={ + 'long': add_ad_groups_response.AdGroupIds['long'] if add_ad_groups_response.AdGroupIds['long'] else None + } + output_ad_group_ids(ad_group_ids) + + # If the campaign or ad group add operations failed then we cannot continue this example. + + if ad_group_ids is None or len(ad_group_ids) < 1: + sys.exit(0) + + ad_group_remarketing_list_associations=campaign_service.factory.create('ArrayOfAdGroupRemarketingListAssociation') + + # This example associates all of the remarketing lists with the new ad group. + + for remarketing_list in remarketing_lists['RemarketingList']: + ad_group_remarketing_list_association=set_elements_to_none(campaign_service.factory.create('AdGroupRemarketingListAssociation')) + ad_group_remarketing_list_association.AdGroupId=ad_group_ids['long'][0] + ad_group_remarketing_list_association.BidAdjustment=20.00 + ad_group_remarketing_list_association.RemarketingListId=remarketing_list.Id + ad_group_remarketing_list_association.Status='Paused' + + ad_group_remarketing_list_associations.AdGroupRemarketingListAssociation.append(ad_group_remarketing_list_association) + + output_status_message("\nAssociating the following remarketing list with the ad group\n") + output_remarketing_list(remarketing_list) + + + add_associations_response = campaign_service.AddAdGroupRemarketingListAssociations( + AdGroupRemarketingListAssociations=ad_group_remarketing_list_associations + ) + + get_associations_response = campaign_service.GetAdGroupRemarketingListAssociations( + AdGroupIds=ad_group_ids + ) + + for ad_group_remarketing_list_association in get_associations_response.AdGroupRemarketingListAssociations['AdGroupRemarketingListAssociation']: + output_status_message("\nThe following ad group remarketing list association was added.\n") + output_ad_group_remarketing_list_association(ad_group_remarketing_list_association); + + # You can store the association IDs which can be used to update or delete associations later. + + association_ids = add_associations_response.AssociationIds + + # If the associations were added and retrieved successfully let's practice updating and deleting one of them. + + ad_group_remarketing_list_associations=campaign_service.factory.create('ArrayOfAdGroupRemarketingListAssociation') + + if association_ids is not None and len(association_ids) > 0: + update_ad_group_remarketing_list_association = set_elements_to_none(campaign_service.factory.create('AdGroupRemarketingListAssociation')) + update_ad_group_remarketing_list_association.AdGroupId=ad_group_ids['long'][0] + update_ad_group_remarketing_list_association.BidAdjustment=10.00 + update_ad_group_remarketing_list_association.Id=association_ids['long'][0] + update_ad_group_remarketing_list_association.Status='Active' + ad_group_remarketing_list_associations.AdGroupRemarketingListAssociation.append(update_ad_group_remarketing_list_association) + + update_associations_response = campaign_service.UpdateAdGroupRemarketingListAssociations( + AdGroupRemarketingListAssociations=ad_group_remarketing_list_associations + ) + + delete_associations_response = campaign_service.DeleteAdGroupRemarketingListAssociations( + AdGroupRemarketingListAssociations=ad_group_remarketing_list_associations + ) + + # Delete the campaign, ad group, and ad group remarketing list associations that were previously added. + # You should remove this line if you want to view the added entities in the + # Bing Ads web application or another tool. + campaign_service.DeleteCampaigns( + AccountId=authorization_data.account_id, + CampaignIds=campaign_ids + ) + + for campaign_id in campaign_ids['long']: + output_status_message("Deleted CampaignId {0}\n".format(campaign_id)) + + 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/ShoppingCampaigns.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ShoppingCampaigns.py index 11381edb..cda4b019 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ShoppingCampaigns.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ShoppingCampaigns.py @@ -100,13 +100,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ShoppingCampaigns.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ShoppingCampaigns.py.bak new file mode 100644 index 00000000..11381edb --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/ShoppingCampaigns.py.bak @@ -0,0 +1,1108 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_campaign_ids(campaign_ids): + for id in campaign_ids['long']: + output_status_message("Campaign successfully added and assigned CampaignId {0}\n".format(id)) + +def output_ad_group_ids(ad_group_ids): + for id in ad_group_ids['long']: + output_status_message("AdGroup successfully added and assigned AdGroupId {0}\n".format(id)) + +def output_ad_results(ads, ad_ids, ad_errors): + + # Since len(ads['Ad']) and len(ad_errors['long']) usually differ, we will need to adjust the ad_errors index + # as successful indices are counted. + success_count=0 + + # There is an issue in the SUDS library, where if Index 0 of ArrayOfNullableOflong is empty it is not returned. + # In this case we will need to adjust the index used to access ad_ids. + suds_index_0_gap=len(ads['Ad']) - len(ad_ids['long']) + + error_index=0 + + for ad_index in range(len(ads['Ad'])): + attribute_value=None + if ads['Ad'][ad_index].Type == 'Product': + attribute_value="PromotionalText: {0}".format(ads['Ad'][ad_index].PromotionalText) + else: + attribute_value="Invalid Ad Type for Shopping Campaigns" + + if ad_errors is not None \ + and ad_errors['BatchError'] is not None \ + and ad_index < len(ad_errors['BatchError']) + success_count \ + and ad_index == ad_errors['BatchError'][error_index].Index: + # One ad may have multiple errors, for example editorial errors in multiple publisher countries + while(error_index < len(ad_errors['BatchError']) and ad_errors['BatchError'][error_index].Index == ad_index): + output_status_message("Ad[{0}] ({1}) not added due to the following error:".format(ad_index, attribute_value)) + output_status_message("Index: {0}".format(ad_errors['BatchError'][error_index].Index)) + output_status_message("Code: {0}".format(ad_errors['BatchError'][error_index].Code)) + output_status_message("ErrorCode: {0}".format(ad_errors['BatchError'][error_index].ErrorCode)) + output_status_message("Message: {0}".format(ad_errors['BatchError'][error_index].Message)) + if ad_errors['BatchError'][error_index].Type == 'EditorialError': + output_status_message("DisapprovedText: {0}".format(ad_errors['BatchError'][error_index].DisapprovedText)) + output_status_message("Location: {0}".format(ad_errors['BatchError'][error_index].Location)) + output_status_message("PublisherCountry: {0}".format(ad_errors['BatchError'][error_index].PublisherCountry)) + output_status_message("ReasonCode: {0}".format(ad_errors['BatchError'][error_index].ReasonCode)) + error_index=error_index + 1 + elif ad_ids['long'][ad_index - suds_index_0_gap] is not None: + output_status_message("Ad[{0}] ({1}) successfully added and assigned AdId {2}".format( + ad_index, + attribute_value, + ad_ids['long'][ad_index - suds_index_0_gap])) + success_count=success_count + 1 + output_status_message('') + +def output_product_partitions(ad_group_criterions): + """ + Outputs the list of AdGroupCriterion, formatted as a tree. + Each AdGroupCriterion must be either a BiddableAdGroupCriterion or NegativeAdGroupCriterion. + To ensure the complete tree is represented, you should first call GetAdGroupCriterionsByIds + where CriterionTypeFilter is ProductPartition, and pass the returned list of AdGroupCriterion to this method. + + :param ad_group_criterions: The list of ad group criterions to output formatted as a tree. + :type ad_group_criterions: ArrayOfAdGroupCriterion + + """ + + # Set up the tree for output + + child_branches={} + tree_root=None + + for ad_group_criterion in ad_group_criterions['AdGroupCriterion']: + partition=ad_group_criterion.Criterion + child_branches[ad_group_criterion.Id]=campaign_service.factory.create('ArrayOfAdGroupCriterion') + + # The product partition with ParentCriterionId set to null is the root node. + if partition.ParentCriterionId is not None: + child_branches[partition.ParentCriterionId].AdGroupCriterion.append(ad_group_criterion) + else: + tree_root=ad_group_criterion + + # Outputs the tree root node and any children recursively + output_product_partition_tree(tree_root, child_branches, 0) + +def output_product_partition_tree(node, child_branches, tree_level): + """ + Outputs the details of the specified product partition node, + and passes any children to itself recursively. + + :param node: The node to output, whether a Subdivision or Unit. + :type node: AdGroupCriterion + :param child_branches: The child branches or nodes if any exist. + :type child_branches: dict{long, ArrayOfAdGroupCriterion} + :param tree_level: The number of descendents from the tree root node. + Used by this operation to format the tree structure output. + :type tree_level: int + + """ + + pad='' + for i in range(0, tree_level): + pad=pad + '\t' + output_status_message("{0}{1}".format( + pad, + node.Criterion.PartitionType) + ) + + output_status_message("{0}ParentCriterionId: {1}".format( + pad, + node.Criterion.ParentCriterionId) + ) + + output_status_message("{0}Id: {1}".format( + pad, + node.Id) + ) + + if node.Criterion.PartitionType == 'Unit': + if node.Type == 'BiddableAdGroupCriterion': + output_status_message("{0}Bid Amount: {1}".format( + pad, + node.CriterionBid.Bid.Amount) + ) + elif node.Type == 'NegativeAdGroupCriterion': + output_status_message("{0}Not Bidding on this Condition".format( + pad) + ) + + + null_attribute="(All other)" if node.Criterion.ParentCriterionId is not None else "(Tree Root)" + output_status_message("{0}Attribute: {1}".format( + pad, + null_attribute if node.Criterion.Condition.Attribute is None else node.Criterion.Condition.Attribute) + ) + + output_status_message("{0}Operand: {1}\n".format( + pad, + node.Criterion.Condition.Operand) + ) + + for child_node in child_branches[node.Id]['AdGroupCriterion']: + output_product_partition_tree(child_node, child_branches, tree_level + 1) + +def get_root_node(ad_group_criterions): + """ + Returns the root node of a tree. This operation assumes that a complete + product partition tree is provided for one ad group. The node that has + null ParentCriterionId is the root node. + + :param ad_group_criterions: The ad group criterions that contain the product partition tree. + :type ad_group_criterions: ArrayOfAdGroupCriterion + :return: The ad group criterion that was added to the list of PartitionActions. + :rtype: AdGroupCriterion + + """ + + root_node=None + for ad_group_criterion in ad_group_criterions['AdGroupCriterion']: + if ad_group_criterion.Criterion.ParentCriterionId is None: + root_node=ad_group_criterion + break + + return root_node + + +class PartitionActionHelper: + """ + Helper class used to maintain a list of product partition actions for an ad group. + The list of partition actions can be passed to the Bing Ads ApplyProductPartitionActions service operation. + """ + + def __init__(self, + ad_group_id): + """ + Initialize an instance of this class. + + :param ad_group_id: Each criterion is associated with the same ad group. + :type ad_group_id: long + + """ + + self._ad_group_id=ad_group_id + self._reference_id=-1 + self._partition_actions=campaign_service.factory.create('ArrayOfAdGroupCriterionAction') + + @property + def partition_actions(self): + """ + The list of partition actions that can be passed to the Bing Ads ApplyProductPartitionActions service operation. + + :rtype: ArrayOfAdGroupCriterionAction + """ + + return self._partition_actions + + def add_subdivision(self, parent, condition): + """ + Sets the Add action for a new BiddableAdGroupCriterion corresponding to the specified ProductCondition, + and adds it to the helper's list of AdGroupCriterionAction. + + :param parent: The parent of the product partition subdivision that you want to add. + :type parent: AdGroupCriterion + :param condition: The condition or product filter for the new product partition. + :type condition: ProductCondition + :return: The ad group criterion that was added to the list of PartitionActions. + :rtype: AdGroupCriterion + """ + + biddable_ad_group_criterion=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + product_partition=set_elements_to_none(campaign_service.factory.create('ProductPartition')) + # If the root node is a unit, it would not have a parent + product_partition.ParentCriterionId=parent.Id if parent is not None else None + product_partition.Condition=condition + product_partition.PartitionType='Subdivision' + biddable_ad_group_criterion.Criterion=product_partition + biddable_ad_group_criterion.CriterionBid=None + biddable_ad_group_criterion.AdGroupId=self._ad_group_id + biddable_ad_group_criterion.Status=None + if hasattr(biddable_ad_group_criterion, 'EditorialStatus'): + biddable_ad_group_criterion.EditorialStatus=None + biddable_ad_group_criterion.Id=self._reference_id + self._reference_id=self._reference_id + self._reference_id-=1 + + partition_action=set_elements_to_none(campaign_service.factory.create('AdGroupCriterionAction')) + partition_action.Action='Add' + partition_action.AdGroupCriterion=biddable_ad_group_criterion + self._partition_actions.AdGroupCriterionAction.append(partition_action) + + return biddable_ad_group_criterion + + def add_unit(self, parent, condition, bid_amount, is_negative=False): + """ + Sets the Add action for a new AdGroupCriterion corresponding to the specified ProductCondition, + and adds it to the helper's list of AdGroupCriterionAction. + + :param parent: The parent of the product partition unit that you want to add. + :type parent: AdGroupCriterion + :param condition: The condition or product filter for the new product partition. + :type condition: ProductCondition + :param bid_amount: The bid amount for the new product partition. + :type bid_amount: double + :param is_negative: (Optional) Indicates whether or not to add a NegativeAdGroupCriterion. + The default value is False, in which case a BiddableAdGroupCriterion will be added. + :type is_negative: bool + :return: The ad group criterion that was added to the list of PartitionActions. + :rtype: AdGroupCriterion + """ + + ad_group_criterion=None + if is_negative: + ad_group_criterion=set_elements_to_none(campaign_service.factory.create('NegativeAdGroupCriterion')) + else: + ad_group_criterion=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + fixed_bid=campaign_service.factory.create('FixedBid') + bid=set_elements_to_none(campaign_service.factory.create('Bid')) + bid.Amount=bid_amount + fixed_bid.Bid=bid + ad_group_criterion.CriterionBid=fixed_bid + + ad_group_criterion.AdGroupId=self._ad_group_id + if hasattr(ad_group_criterion, 'EditorialStatus'): + ad_group_criterion.EditorialStatus=None + ad_group_criterion.Status=None + + product_partition=set_elements_to_none(campaign_service.factory.create('ProductPartition')) + # If the root node is a unit, it would not have a parent + product_partition.ParentCriterionId=parent.Id if parent is not None else None + product_partition.Condition=condition + product_partition.PartitionType='Unit' + ad_group_criterion.Criterion=product_partition=product_partition + + partition_action=set_elements_to_none(campaign_service.factory.create('AdGroupCriterionAction')) + partition_action.Action='Add' + partition_action.AdGroupCriterion=ad_group_criterion + self._partition_actions.AdGroupCriterionAction.append(partition_action) + + return ad_group_criterion + + def delete_partition(self, ad_group_criterion): + """ + Sets the Delete action for the specified AdGroupCriterion, + and adds it to the helper's list of AdGroupCriterionAction. + + :param ad_group_criterion: The ad group criterion whose product partition you want to delete. + :type ad_group_criterion: AdGroupCriterion + """ + + ad_group_criterion.AdGroupId=self._ad_group_id + #ad_group_criterion.Status=None + #if hasattr(ad_group_criterion, 'EditorialStatus'): + # ad_group_criterion.EditorialStatus=None + + partition_action=set_elements_to_none(campaign_service.factory.create('AdGroupCriterionAction')) + partition_action.Action='Delete' + partition_action.AdGroupCriterion=ad_group_criterion + self._partition_actions.AdGroupCriterionAction.append(partition_action) + + def update_partition(self, biddable_ad_group_criterion): + """ + Sets the Update action for the specified BiddableAdGroupCriterion, + and adds it to the helper's list of AdGroupCriterionAction. + You can only update the CriterionBid, DestinationUrl, Param1, Param2, and Param3 elements + of the BiddableAdGroupCriterion. + When working with product partitions, youu cannot update the Criterion (ProductPartition). + To update a ProductPartition, you must delete the existing node (delete_partition) and + add a new one (add_unit or add_subdivision) during the same call to ApplyProductPartitionActions. + + :param ad_group_criterion: The biddable ad group criterion to update. + :type ad_group_criterion: BiddableAdGroupCriterion + """ + + biddable_ad_group_criterion.AdGroupId=self._ad_group_id + #biddable_ad_group_criterion.Status=None + #if hasattr(biddable_ad_group_criterion, 'EditorialStatus'): + # biddable_ad_group_criterion.EditorialStatus=None + + partition_action=set_elements_to_none(campaign_service.factory.create('AdGroupCriterionAction')) + partition_action.Action='Update' + partition_action.AdGroupCriterion=biddable_ad_group_criterion + self._partition_actions.AdGroupCriterionAction.append(partition_action) + +# Main execution +if __name__ == '__main__': + + 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 + + # Get a list of all Bing Merchant Center stores associated with your CustomerId + + stores=campaign_service.GetBMCStoresByCustomerId()['BMCStore'] + if stores is None: + output_status_message( + "You do not have any BMC stores registered for CustomerId {0}.\n".format(authorization_data.customer_id) + ) + sys.exit(0) + + #Add a new Bing Shopping campaign that will be associated with a ProductScope criterion. + # - Set the CampaignType element of the Campaign to Shopping. + # - Create a ShoppingSetting instance and set its Priority (0, 1, or 2), SalesCountryCode, and StoreId elements. + # Add this shopping setting to the Settings list of the Campaign. + + campaigns=campaign_service.factory.create('ArrayOfCampaign') + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + settings=campaign_service.factory.create('ArrayOfSetting') + setting=set_elements_to_none(campaign_service.factory.create('ShoppingSetting')) + setting.Priority=0 + setting.SalesCountryCode ='US' + setting.StoreId=stores[0].Id + settings.Setting.append(setting) + campaign.Settings=settings + campaign.CampaignType=['Shopping'] + campaign.Name='Bing Shopping Campaign ' + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description='Bing Shopping Campaign Example.' + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving='true' + campaign.Status='Paused' + campaigns.Campaign.append(campaign) + + add_campaigns_response=campaign_service.AddCampaigns( + AccountId=authorization_data.account_id, + Campaigns=campaigns + ) + campaign_ids={ + 'long': add_campaigns_response.CampaignIds['long'] if add_campaigns_response.CampaignIds['long'] else None + } + + output_campaign_ids(campaign_ids) + + + #Optionally, you can create a ProductScope criterion that will be associated with your Bing Shopping campaign. + #Use the product scope criterion to include a subset of your product catalog, for example a specific brand, + #category, or product type. A campaign can only be associated with one ProductScope, which contains a list + #of up to 7 ProductCondition. You'll also be able to specify more specific product conditions for each ad group. + + campaign_criterions=campaign_service.factory.create('ArrayOfCampaignCriterion') + campaign_criterion=set_elements_to_none(campaign_service.factory.create('CampaignCriterion')) + product_scope=set_elements_to_none(campaign_service.factory.create('ProductScope')) + conditions=campaign_service.factory.create('ArrayOfProductCondition') + condition_new=campaign_service.factory.create('ProductCondition') + condition_new.Operand='Condition' + condition_new.Attribute='New' + conditions.ProductCondition.append(condition_new) + condition_custom_label_0=campaign_service.factory.create('ProductCondition') + condition_custom_label_0.Operand='CustomLabel0' + condition_custom_label_0.Attribute='MerchantDefinedCustomLabel' + conditions.ProductCondition.append(condition_custom_label_0) + product_scope.Conditions=conditions + campaign_criterion.CampaignId=campaign_ids['long'][0] + campaign_criterion.BidAdjustment=None # Reserved for future use + campaign_criterion.Criterion=product_scope + campaign_criterions.CampaignCriterion.append(campaign_criterion) + + add_campaign_criterions_response=campaign_service.AddCampaignCriterions( + CampaignCriterions=campaign_criterions, + CriterionType='ProductScope' + ) + + # Specify one or more ad groups. + + ad_groups=campaign_service.factory.create('ArrayOfAdGroup') + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + ad_group.Name="Product Categories" + ad_group.AdDistribution=['Search'] + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + ad_group.Network=None + ad_group.Status='Paused' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + ad_group.Language='English' + ad_group.RemarketingTargetingSetting=None + ad_groups.AdGroup.append(ad_group) + + add_ad_groups_response=campaign_service.AddAdGroups( + CampaignId=campaign_ids['long'][0], + AdGroups=ad_groups + ) + ad_group_ids={ + 'long': add_ad_groups_response.AdGroupIds['long'] if add_ad_groups_response.AdGroupIds['long'] else None + } + + output_ad_group_ids(ad_group_ids) + + # Bid on all products + + helper=PartitionActionHelper(ad_group_ids['long'][0]) + + root_condition=campaign_service.factory.create('ProductCondition') + root_condition.Operand='All' + root_condition.Attribute=None + + root=helper.add_unit( + None, + root_condition, + 0.35, + False + ) + + output_status_message("Applying only the root as a Unit with a bid . . . \n") + apply_product_partition_actions_response=campaign_service.ApplyProductPartitionActions( + CriterionActions=helper.partition_actions + ) + + ad_group_criterions=campaign_service.GetAdGroupCriterionsByIds( + AccountId=authorization_data.account_id, + AdGroupId=ad_group_ids['long'][0], + AdGroupCriterionIds=None, + CriterionType='ProductPartition' + ) + + output_status_message("The ad group's product partition only has a tree root node: \n") + output_product_partitions(ad_group_criterions) + + # Let's update the bid of the root Unit we just added. + + updated_root=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + fixed_bid=campaign_service.factory.create('FixedBid') + bid=campaign_service.factory.create('Bid') + bid.Amount=0.45 + fixed_bid.Bid=bid + updated_root.Id=apply_product_partition_actions_response.AdGroupCriterionIds['long'][0] + updated_root.CriterionBid=fixed_bid + + helper=PartitionActionHelper(ad_group_ids['long'][0]) + helper.update_partition(updated_root) + + output_status_message("Updating the bid for the tree root node . . . \n") + campaign_service.ApplyProductPartitionActions( + CriterionActions=helper.partition_actions + ) + + ad_group_criterions=campaign_service.GetAdGroupCriterionsByIds( + AccountId=authorization_data.account_id, + AdGroupId=ad_group_ids['long'][0], + AdGroupCriterionIds=None, + CriterionType='ProductPartition' + ) + + output_status_message("Updated the bid for the tree root node: \n") + output_product_partitions(ad_group_criterions) + + + #Now we will overwrite any existing tree root, and build a product partition group tree structure in multiple steps. + #You could build the entire tree in a single call since there are less than 5,000 nodes; however, + #we will build it in steps to demonstrate how to use the results from ApplyProductPartitionActions to update the tree. + + #For a list of validation rules, see the Bing Shopping Campaigns technical guide: + #https://msdn.microsoft.com/en-US/library/bing-ads-campaign-management-bing-shopping-campaigns.aspx + + + helper=PartitionActionHelper(ad_group_ids['long'][0]) + + #Check whether a root node exists already. + + ad_group_criterions=campaign_service.GetAdGroupCriterionsByIds( + AccountId=authorization_data.account_id, + AdGroupId=ad_group_ids['long'][0], + AdGroupCriterionIds=None, + CriterionType='ProductPartition' + ) + + existing_root=get_root_node(ad_group_criterions) + if existing_root is not None: + helper.delete_partition(existing_root) + + root_condition=campaign_service.factory.create('ProductCondition') + root_condition.Operand='All' + root_condition.Attribute=None + root=helper.add_subdivision( + None, + root_condition + ) + + + #The direct children of any node must have the same Operand. + #For this example we will use CategoryL1 nodes as children of the root. + #For a list of valid CategoryL1 through CategoryL5 values, see the Bing Category Taxonomy: + #http://advertise.bingads.microsoft.com/en-us/WWDocs/user/search/en-us/Bing_Category_Taxonomy.txt + + animals_condition=campaign_service.factory.create('ProductCondition') + animals_condition.Operand='CategoryL1' + animals_condition.Attribute='Animals & Pet Supplies' + animals_subdivision=helper.add_subdivision( + root, + animals_condition + ) + + + #If you use a CategoryL2 node, it must be a descendant (child or later) of a CategoryL1 node. + #In other words you cannot have a CategoryL2 node as parent of a CategoryL1 node. + #For this example we will a CategoryL2 node as child of the CategoryL1 Animals & Pet Supplies node. + + pet_supplies_condition=campaign_service.factory.create('ProductCondition') + pet_supplies_condition.Operand='CategoryL2' + pet_supplies_condition.Attribute='Pet Supplies' + pet_supplies_subdivision=helper.add_subdivision( + animals_subdivision, + pet_supplies_condition + ) + + brand_a_condition=campaign_service.factory.create('ProductCondition') + brand_a_condition.Operand='Brand' + brand_a_condition.Attribute='Brand A' + brand_a=helper.add_unit( + pet_supplies_subdivision, + brand_a_condition, + 0.35, + False + ) + + + #If you won't bid on Brand B, set the helper method's bidAmount to '0' and isNegative to True. + #The helper method will create a NegativeAdGroupCriterion and apply the condition. + + brand_b_condition=campaign_service.factory.create('ProductCondition') + brand_b_condition.Operand='Brand' + brand_b_condition.Attribute='Brand B' + brand_b=helper.add_unit( + pet_supplies_subdivision, + brand_b_condition, + 0, + True + ) + + other_brands_condition=campaign_service.factory.create('ProductCondition') + other_brands_condition.Operand='Brand' + other_brands_condition.Attribute=None + other_brands=helper.add_unit( + pet_supplies_subdivision, + other_brands_condition, + 0.35, + False + ) + + other_pet_supplies_condition=campaign_service.factory.create('ProductCondition') + other_pet_supplies_condition.Operand='CategoryL2' + other_pet_supplies_condition.Attribute=None + other_pet_supplies=helper.add_unit( + animals_subdivision, + other_pet_supplies_condition, + 0.35, + False + ) + + electronics_condition=campaign_service.factory.create('ProductCondition') + electronics_condition.Operand='CategoryL1' + electronics_condition.Attribute='Electronics' + electronics=helper.add_unit( + root, + electronics_condition, + 0.35, + False + ) + + other_categoryL1_condition=campaign_service.factory.create('ProductCondition') + other_categoryL1_condition.Operand='CategoryL1' + other_categoryL1_condition.Attribute=None + other_categoryL1=helper.add_unit( + root, + other_categoryL1_condition, + 0.35, + False + ) + + output_status_message("Applying product partitions to the ad group . . . \n") + apply_product_partition_actions_response=campaign_service.ApplyProductPartitionActions( + CriterionActions=helper.partition_actions + ) + + # To retrieve product partitions after they have been applied, call GetAdGroupCriterionsByIds. + # The product partition with ParentCriterionId set to null is the root node. + + ad_group_criterions=campaign_service.GetAdGroupCriterionsByIds( + AccountId=authorization_data.account_id, + AdGroupId=ad_group_ids['long'][0], + AdGroupCriterionIds=None, + CriterionType='ProductPartition' + ) + + + #The product partition group tree now has 9 nodes. + + #All other (Root Node) + #| + #+-- Animals & Pet Supplies (CategoryL1) + #| | + #| +-- Pet Supplies (CategoryL2) + #| | | + #| | +-- Brand A + #| | | + #| | +-- Brand B + #| | | + #| | +-- All other (Brand) + #| | + #| +-- All other (CategoryL2) + #| + #+-- Electronics (CategoryL1) + #| + #+-- All other (CategoryL1) + + output_status_message("The product partition group tree now has 9 nodes: \n") + output_product_partitions(ad_group_criterions) + + + #Let's replace the Electronics (CategoryL1) node created above with an Electronics (CategoryL1) node that + #has children i.e. Brand C (Brand), Brand D (Brand), and All other (Brand) as follows: + + #Electronics (CategoryL1) + #| + #+-- Brand C (Brand) + #| + #+-- Brand D (Brand) + #| + #+-- All other (Brand) + + helper=PartitionActionHelper(ad_group_ids['long'][0]) + + + #To replace a node we must know its Id and its ParentCriterionId. In this case the parent of the node + #we are replacing is All other (Root Node), and was created at Index 1 of the previous ApplyProductPartitionActions call. + #The node that we are replacing is Electronics (CategoryL1), and was created at Index 8. + + root_id=apply_product_partition_actions_response.AdGroupCriterionIds['long'][1] + electronics.Id=apply_product_partition_actions_response.AdGroupCriterionIds['long'][8] + helper.delete_partition(electronics) + + parent=set_elements_to_none(campaign_service.factory.create('BiddableAdGroupCriterion')) + parent.Id=root_id + + electronics_subdivision_condition=campaign_service.factory.create('ProductCondition') + electronics_subdivision_condition.Operand='CategoryL1' + electronics_subdivision_condition.Attribute='Electronics' + electronics_subdivision=helper.add_subdivision( + parent, + electronics_subdivision_condition + ) + + brand_c_condition=campaign_service.factory.create('ProductCondition') + brand_c_condition.Operand='Brand' + brand_c_condition.Attribute='Brand C' + brand_c=helper.add_unit( + electronics_subdivision, + brand_c_condition, + 0.35, + False + ) + + brand_d_condition=campaign_service.factory.create('ProductCondition') + brand_d_condition.Operand='Brand' + brand_d_condition.Attribute='Brand D' + brand_d=helper.add_unit( + electronics_subdivision, + brand_d_condition, + 0.35, + False + ) + + other_electronics_brands_condition=campaign_service.factory.create('ProductCondition') + other_electronics_brands_condition.Operand='Brand' + other_electronics_brands_condition.Attribute=None + other_electronics_brands=helper.add_unit( + electronics_subdivision, + other_electronics_brands_condition, + 0.35, + False + ) + + output_status_message( + "Updating the product partition group to refine Electronics (CategoryL1) with 3 child nodes . . . \n" + ) + apply_product_partition_actions_response=campaign_service.ApplyProductPartitionActions( + CriterionActions=helper.partition_actions + ) + + ad_group_criterions=campaign_service.GetAdGroupCriterionsByIds( + AccountId=authorization_data.account_id, + AdGroupId=ad_group_ids['long'][0], + AdGroupCriterionIds=None, + CriterionType='ProductPartition' + ) + + + #The product partition group tree now has 12 nodes, including the children of Electronics (CategoryL1): + + #All other (Root Node) + #| + #+-- Animals & Pet Supplies (CategoryL1) + #| | + #| +-- Pet Supplies (CategoryL2) + #| | | + #| | +-- Brand A + #| | | + #| | +-- Brand B + #| | | + #| | +-- All other (Brand) + #| | + #| +-- All other (CategoryL2) + #| + #+-- Electronics (CategoryL1) + #| | + #| +-- Brand C (Brand) + #| | + #| +-- Brand D (Brand) + #| | + #| +-- All other (Brand) + #| + #+-- All other (CategoryL1) + + output_status_message( + "The product partition group tree now has 12 nodes, including the children of Electronics (CategoryL1): \n" + ) + output_product_partitions(ad_group_criterions) + + + #Create a product ad. You must add at least one ProductAd to the corresponding ad group. + #A ProductAd is not used directly for delivered ad copy. Instead, the delivery engine generates + #product ads from the product details that it finds in your Bing Merchant Center store's product catalog. + #The primary purpose of the ProductAd object is to provide promotional text that the delivery engine + #adds to the product ads that it generates. For example, if the promotional text is set to + #'Free shipping on $99 purchases', the delivery engine will set the product ad's description to + #'Free shipping on $99 purchases.' + + ads=campaign_service.factory.create('ArrayOfAd') + product_ad=set_elements_to_none(campaign_service.factory.create('ProductAd')) + product_ad.PromotionalText='Free shipping on $99 purchases.' + product_ad.Type='Product' + product_ad.Status=None + product_ad.EditorialStatus=None + ads.Ad.append(product_ad) + + add_ads_response=campaign_service.AddAds( + AdGroupId=ad_group_ids['long'][0], + Ads=ads + ) + ad_ids={ + 'long': add_ads_response.AdIds['long'] if add_ads_response.AdIds['long'] else None + } + ad_errors={ + 'BatchError': add_ads_response.PartialErrors['BatchError'] if add_ads_response.PartialErrors else None + } + + output_ad_results(ads, ad_ids, ad_errors) + + #Delete the campaign, ad group, criterion, and ad that were previously added. + #You should remove this region if you want to view the added entities in the + #Bing Ads web application or another tool. + + campaign_service.DeleteCampaigns( + AccountId=authorization_data.account_id, + CampaignIds=campaign_ids + ) + + for campaign_id in campaign_ids['long']: + output_status_message("Deleted CampaignId {0}\n".format(campaign_id)) + + output_status_message("Program execution completed") + + except WebFault as ex: + output_webfault_errors(ex) + except Exception as ex: + output_status_message(ex) \ No newline at end of file diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py index c3616f96..fffadd4e 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py @@ -99,13 +99,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py.bak new file mode 100644 index 00000000..c3616f96 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v10/Targets.py.bak @@ -0,0 +1,717 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v10 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 output_status_message(message): + print(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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +def output_campaign_ids(campaign_ids): + if not hasattr(campaign_ids, 'long'): + return None + for id in campaign_ids: + output_status_message("Campaign successfully added and assigned CampaignId {0}\n".format(id)) + +def output_ad_group_ids(ad_group_ids): + if not hasattr(ad_group_ids, 'long'): + return None + for id in ad_group_ids: + output_status_message("AdGroup successfully added and assigned AdGroupId {0}\n".format(id)) + +def output_targets(targets): + if not hasattr(targets, 'Target'): + return None + for target in targets['Target']: + output_status_message("Target Id: {0}".format(target.Id)) + output_status_message("Target Name: {0}\n".format(target.Name)) + + if hasattr(target.Age, 'Bids'): + output_status_message("AgeTarget:") + for bid in target.Age.Bids['AgeTargetBid']: + output_status_message("\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\tAge: {0}\n".format(bid.Age)) + + if hasattr(target.DayTime, 'Bids'): + output_status_message("DayTimeTarget:") + for bid in target.DayTime.Bids['DayTimeTargetBid']: + output_status_message("\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\tDay: {0}".format(bid.Day)) + output_status_message("\tFrom Hour: {0}".format(bid.FromHour)) + output_status_message("\tFrom Minute: {0}".format(bid.FromMinute)) + output_status_message("\tTo Hour: {0}".format(bid.ToHour)) + output_status_message("\tTo Minute: {0}\n".format(bid.ToMinute)) + + if hasattr(target.DeviceOS, 'Bids'): + output_status_message("DeviceOSTarget:") + for bid in target.DeviceOS.Bids['DeviceOSTargetBid']: + output_status_message("\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\tDeviceName: {0}".format(bid.DeviceName)) + + if hasattr(target.Gender, 'Bids'): + output_status_message("GenderTarget:") + for bid in target.Gender.Bids['GenderTargetBid']: + output_status_message("\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\tGender: {0}\n".format(bid.Gender)) + + if target.Location is not None: + output_status_message("LocationTarget:") + output_status_message("\tIntentOption: {0}\n".format(target.Location.IntentOption)) + + if hasattr(target.Location.CityTarget, 'Bids'): + output_status_message("\tCityTarget:") + for bid in target.Location.CityTarget.Bids['CityTargetBid']: + output_status_message("\t\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\t\tCity: {0}\n".format(bid.City)) + output_status_message("\t\tIsExcluded: {0}\n".format(bid.IsExcluded)) + + if hasattr(target.Location.CountryTarget, 'Bids'): + output_status_message("\tCountryTarget:") + for bid in target.Location.CountryTarget.Bids['CountryTargetBid']: + output_status_message("\t\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\t\tCountryAndRegion: {0}".format(bid.CountryAndRegion)) + output_status_message("\t\tIsExcluded: {0}\n".format(bid.IsExcluded)) + + if hasattr(target.Location.MetroAreaTarget, 'Bids'): + output_status_message("\tMetroAreaTarget:") + for bid in target.Location.MetroAreaTarget.Bids['MetroAreaTargetBid']: + output_status_message("\t\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\t\tMetroArea: {0}".format(bid.MetroArea)) + output_status_message("\t\tIsExcluded: {0}\n".format(bid.IsExcluded)) + + if hasattr(target.Location.PostalCodeTarget, 'Bids'): + output_status_message("\tPostalCodeTarget:") + for bid in target.Location.PostalCodeTarget.Bids['PostalCodeTargetBid']: + output_status_message("\t\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\t\tPostalCode: {0}".format(bid.PostalCode)) + output_status_message("\t\tIsExcluded: {0}\n".format(bid.IsExcluded)) + + if hasattr(target.Location.RadiusTarget, 'Bids'): + output_status_message("\tRadiusTarget:") + for bid in target.Location.RadiusTarget.Bids['RadiusTargetBid']: + output_status_message("\t\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\t\tLatitudeDegrees: {0}".format(bid.LatitudeDegrees)) + output_status_message("\t\tLongitudeDegrees: {0}".format(bid.LongitudeDegrees)) + output_status_message("\t\tRadius: {0} {1}\n".format(bid.Radius, bid.RadiusUnit)) + + if hasattr(target.Location.StateTarget, 'Bids'): + output_status_message("\tStateTarget:") + for bid in target.Location.StateTarget.Bids['StateTargetBid']: + output_status_message("\t\tBidAdjustment: {0}".format(bid.BidAdjustment)) + output_status_message("\t\tState: {0}".format(bid.State)) + output_status_message("\t\tIsExcluded: {0}\n".format(bid.IsExcluded)) + +def output_targets_info(targets_info): + if not hasattr(targets_info, 'TargetInfo'): + return None + for info in targets_info['TargetInfo']: + output_status_message("Target Id: {0}".format(info.Id)) + output_status_message("Target Name: {0}".format(info.Name)) + +# Main execution +if __name__ == '__main__': + + 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 + + # Add a campaign that will later be associated with targets. + + campaigns=campaign_service.factory.create('ArrayOfCampaign') + campaign=set_elements_to_none(campaign_service.factory.create('Campaign')) + campaign.Name="Summer Shoes " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + campaign.Description="Summer shoes line." + campaign.BudgetType='DailyBudgetStandard' + campaign.DailyBudget=10 + campaign.TimeZone='PacificTimeUSCanadaTijuana' + campaign.DaylightSaving='true' # Accepts 'true', 'false', True, or False + campaign.Status='Paused' + campaigns.Campaign.append(campaign) + + add_campaigns_response=campaign_service.AddCampaigns( + AccountId=authorization_data.account_id, + Campaigns=campaigns + ) + campaign_ids={ + 'long': add_campaigns_response.CampaignIds['long'] if add_campaigns_response.CampaignIds['long'] else None + } + + output_campaign_ids(campaign_ids) + + # Add an ad group that will later be associated with a target. + + ad_groups=campaign_service.factory.create('ArrayOfAdGroup') + ad_group=set_elements_to_none(campaign_service.factory.create('AdGroup')) + ad_group.Name="Women's Red Shoes" + ad_group.AdDistribution='Search' + ad_group.BiddingModel='Keyword' + ad_group.PricingModel='Cpc' + ad_group.Network='OwnedAndOperatedAndSyndicatedSearch' + ad_group.Status='Paused' + end_date=campaign_service.factory.create('Date') + end_date.Day=31 + end_date.Month=12 + end_date.Year=strftime("%Y", gmtime()) + ad_group.EndDate=end_date + search_bid=campaign_service.factory.create('Bid') + search_bid.Amount=0.09 + ad_group.SearchBid=search_bid + ad_group.Language='English' + ad_groups.AdGroup.append(ad_group) + + add_ad_groups_response=campaign_service.AddAdGroups( + CampaignId=campaign_ids['long'][0], + AdGroups=ad_groups + ) + ad_group_ids={ + 'long': add_ad_groups_response.AdGroupIds['long'] if add_ad_groups_response.AdGroupIds['long'] else None + } + + output_ad_group_ids(ad_group_ids) + + + # Create targets to associate with the campaign and ad group. + + campaign_targets=campaign_service.factory.create('ArrayOfTarget') + campaign_target=set_elements_to_none(campaign_service.factory.create('Target')) + campaign_target.Name = "My Campaign Target" + + campaign_day_time_target=campaign_service.factory.create('DayTimeTarget') + campaign_day_time_target_bids=campaign_service.factory.create('ArrayOfDayTimeTargetBid') + campaign_day_time_target_bid=campaign_service.factory.create('DayTimeTargetBid') + campaign_day_time_target_bid.BidAdjustment=10 + campaign_day_time_target_bid.Day='Monday' + campaign_day_time_target_bid.FromHour=1 + campaign_day_time_target_bid.FromMinute='Zero' + campaign_day_time_target_bid.ToHour=12 + campaign_day_time_target_bid.ToMinute='FortyFive' + campaign_day_time_target_bids.DayTimeTargetBid.append(campaign_day_time_target_bid) + campaign_day_time_target.Bids=campaign_day_time_target_bids + + campaign_device_os_target=campaign_service.factory.create('DeviceOSTarget') + campaign_device_os_target_bids=campaign_service.factory.create('ArrayOfDeviceOSTargetBid') + campaign_device_os_target_bid=campaign_service.factory.create('DeviceOSTargetBid') + campaign_device_os_target_bid.BidAdjustment = 10 + campaign_device_os_target_bid.DeviceName='Tablets' + campaign_device_os_target_bids.DeviceOSTargetBid.append(campaign_device_os_target_bid) + campaign_device_os_target.Bids=campaign_device_os_target_bids + + campaign_location_target=campaign_service.factory.create('LocationTarget') + campaign_location_target.IntentOption='PeopleIn' + + campaign_metro_area_target=campaign_service.factory.create('MetroAreaTarget') + campaign_metro_area_target_bids=campaign_service.factory.create('ArrayOfMetroAreaTargetBid') + campaign_metro_area_target_bid=campaign_service.factory.create('MetroAreaTargetBid') + campaign_metro_area_target_bid.BidAdjustment = 15 + campaign_metro_area_target_bid.MetroArea="Seattle-Tacoma, WA, WA US" + campaign_metro_area_target_bid.IsExcluded=False + campaign_metro_area_target_bids.MetroAreaTargetBid.append(campaign_metro_area_target_bid) + campaign_metro_area_target.Bids=campaign_metro_area_target_bids + campaign_location_target.MetroAreaTarget=campaign_metro_area_target + + campaign_radius_target=campaign_service.factory.create('RadiusTarget') + campaign_radius_target_bids=campaign_service.factory.create('ArrayOfRadiusTargetBid') + campaign_radius_target_bid=campaign_service.factory.create('RadiusTargetBid') + campaign_radius_target_bid.BidAdjustment = 50 + campaign_radius_target_bid.LatitudeDegrees = 47.755367 + campaign_radius_target_bid.LongitudeDegrees = -122.091827 + campaign_radius_target_bid.Radius = 5 + campaign_radius_target_bid.RadiusUnit='Kilometers' + campaign_radius_target_bid.IsExcluded=False + campaign_radius_target_bids.RadiusTargetBid.append(campaign_radius_target_bid) + campaign_radius_target.Bids=campaign_radius_target_bids + campaign_location_target.RadiusTarget=campaign_radius_target + + campaign_target.DayTime=campaign_day_time_target + campaign_target.DeviceOS=campaign_device_os_target + campaign_target.Location=campaign_location_target + campaign_targets.Target.append(campaign_target) + + ad_group_targets=campaign_service.factory.create('ArrayOfTarget') + ad_group_target=set_elements_to_none(campaign_service.factory.create('Target')) + ad_group_target.Name = "My Ad Group Target" + + ad_group_day_time_target=campaign_service.factory.create('DayTimeTarget') + ad_group_day_time_target_bids=campaign_service.factory.create('ArrayOfDayTimeTargetBid') + ad_group_day_time_target_bid=campaign_service.factory.create('DayTimeTargetBid') + ad_group_day_time_target_bid.BidAdjustment=10 + ad_group_day_time_target_bid.Day='Friday' + ad_group_day_time_target_bid.FromHour=1 + ad_group_day_time_target_bid.FromMinute='Zero' + ad_group_day_time_target_bid.ToHour=12 + ad_group_day_time_target_bid.ToMinute='FortyFive' + ad_group_day_time_target_bids.DayTimeTargetBid.append(ad_group_day_time_target_bid) + ad_group_day_time_target.Bids=ad_group_day_time_target_bids + + ad_group_target.DayTime=ad_group_day_time_target + ad_group_targets.Target.append(ad_group_target) + + # Each customer has a target library that can be used to set up targeting for any campaign + # or ad group within the specified customer. + + # Add a target to the library and associate it with the campaign. + campaign_target_id=campaign_service.AddTargetsToLibrary(Targets=campaign_targets)['long'][0] + output_status_message("Added Target Id: {0}\n".format(campaign_target_id)) + campaign_service.SetTargetToCampaign( + CampaignId=campaign_ids['long'][0], + TargetId=campaign_target_id + ) + output_status_message("Associated CampaignId {0} with TargetId {1}.\n".format(campaign_ids['long'][0], campaign_target_id)) + + # Add a target to the library and associate it with the ad group. + ad_group_target_id=campaign_service.AddTargetsToLibrary(Targets=ad_group_targets)['long'][0] + output_status_message("Added Target Id: {0}\n".format(ad_group_target_id)) + campaign_service.SetTargetToAdGroup(ad_group_ids['long'][0], ad_group_target_id) + output_status_message("Associated AdGroupId {0} with TargetId {1}.\n".format(ad_group_ids['long'][0], ad_group_target_id)) + + # Get and print the targets with the GetTargetsByIds operation + output_status_message("Get Campaign and AdGroup targets: \n") + targets=campaign_service.GetTargetsByIds( + TargetIds={'long': [campaign_target_id, ad_group_target_id] } + ) + output_targets(targets) + + # Update the ad group's target as a Target object with additional target types. + # Existing target types such as DayTime must be specified + # or they will not be included in the updated target. + + targets=campaign_service.factory.create('ArrayOfTarget') + target=set_elements_to_none(campaign_service.factory.create('Target')) + + age_target=campaign_service.factory.create('AgeTarget') + age_target_bids=campaign_service.factory.create('ArrayOfAgeTargetBid') + age_target_bid=campaign_service.factory.create('AgeTargetBid') + age_target_bid.BidAdjustment = 10 + age_target_bid.Age='EighteenToTwentyFive' + age_target_bids.AgeTargetBid.append(age_target_bid) + age_target.Bids=age_target_bids + + day_time_target=campaign_service.factory.create('DayTimeTarget') + day_time_target_bids=campaign_service.factory.create('ArrayOfDayTimeTargetBid') + day_time_target_bid=campaign_service.factory.create('DayTimeTargetBid') + day_time_target_bid.BidAdjustment=10 + day_time_target_bid.Day='Friday' + day_time_target_bid.FromHour=1 + day_time_target_bid.FromMinute='Zero' + day_time_target_bid.ToHour=12 + day_time_target_bid.ToMinute='FortyFive' + day_time_target_bids.DayTimeTargetBid.append(day_time_target_bid) + day_time_target.Bids=day_time_target_bids + + device_os_target=campaign_service.factory.create('DeviceOSTarget') + device_os_target_bids=campaign_service.factory.create('ArrayOfDeviceOSTargetBid') + device_os_target_bid=campaign_service.factory.create('DeviceOSTargetBid') + device_os_target_bid.BidAdjustment = 20 + device_os_target_bid.DeviceName='Tablets' + device_os_target_bids.DeviceOSTargetBid.append(device_os_target_bid) + device_os_target.Bids=device_os_target_bids + + gender_target=campaign_service.factory.create('GenderTarget') + gender_target_bids=campaign_service.factory.create('ArrayOfGenderTargetBid') + gender_target_bid=campaign_service.factory.create('GenderTargetBid') + gender_target_bid.BidAdjustment = 10 + gender_target_bid.Gender='Female' + gender_target_bids.GenderTargetBid.append(gender_target_bid) + gender_target.Bids=gender_target_bids + + country_target=campaign_service.factory.create('CountryTarget') + country_target_bids=campaign_service.factory.create('ArrayOfCountryTargetBid') + country_target_bid=campaign_service.factory.create('CountryTargetBid') + country_target_bid.BidAdjustment=10 + country_target_bid.CountryAndRegion="US" + country_target_bid.IsExcluded=False + country_target_bids.CountryTargetBid.append(country_target_bid) + country_target.Bids=country_target_bids + + metro_area_target=campaign_service.factory.create('MetroAreaTarget') + metro_area_target_bids=campaign_service.factory.create('ArrayOfMetroAreaTargetBid') + metro_area_target_bid=campaign_service.factory.create('MetroAreaTargetBid') + metro_area_target_bid.BidAdjustment=15 + metro_area_target_bid.MetroArea="Seattle-Tacoma, WA, WA US" + metro_area_target_bid.IsExcluded=False + metro_area_target_bids.MetroAreaTargetBid.append(metro_area_target_bid) + metro_area_target.Bids=metro_area_target_bids + + postal_code_target=campaign_service.factory.create('PostalCodeTarget') + postal_code_target_bids=campaign_service.factory.create('ArrayOfPostalCodeTargetBid') + postal_code_target_bid=campaign_service.factory.create('PostalCodeTargetBid') + # Bid adjustments are not allowed for location exclusions. + # If IsExcluded is true, this element will be ignored. + postal_code_target_bid.BidAdjustment=10 + postal_code_target_bid.PostalCode="98052, WA US" + postal_code_target_bid.IsExcluded=True + postal_code_target_bids.PostalCodeTargetBid.append(postal_code_target_bid) + postal_code_target.Bids=postal_code_target_bids + + radius_target=campaign_service.factory.create('RadiusTarget') + radius_target_bids=campaign_service.factory.create('ArrayOfRadiusTargetBid') + radius_target_bid=campaign_service.factory.create('RadiusTargetBid') + radius_target_bid.BidAdjustment=10 + radius_target_bid.LatitudeDegrees=47.755367 + radius_target_bid.LongitudeDegrees=-122.091827 + radius_target_bid.Radius=11 + radius_target_bid.RadiusUnit='Miles' + radius_target_bid.IsExcluded=False + radius_target_bids.RadiusTargetBid.append(radius_target_bid) + radius_target.Bids=radius_target_bids + + location_target=campaign_service.factory.create('LocationTarget') + location_target.IntentOption='PeopleSearchingForOrViewingPages' + location_target.CountryTarget=country_target + location_target.MetroAreaTarget=metro_area_target + location_target.PostalCodeTarget=postal_code_target + location_target.RadiusTarget=radius_target + + target.Age=age_target + target.DayTime=day_time_target + target.DeviceOS=device_os_target + target.Gender=gender_target + target.Id = ad_group_target_id + target.Location=location_target + target.Name = "My Target" + targets.Target.append(target) + + # Update the Target object associated with the ad group. + campaign_service.UpdateTargetsInLibrary(Targets=targets) + output_status_message("Updated the ad group level target as a Target object.\n") + + # Get and print the targets with the GetTargetsByIds operation + output_status_message("Get Campaign and AdGroup targets: \n") + targets=campaign_service.GetTargetsByIds( + TargetIds={'long': [campaign_target_id, ad_group_target_id] } + ) + output_targets(targets) + + # Get all new and existing targets in the customer library, whether or not they are + # associated with campaigns or ad groups. + + all_targets_info=campaign_service.GetTargetsInfoFromLibrary() + output_status_message("All target identifiers and names from the customer library: \n") + output_targets_info(all_targets_info) + + # Delete the campaign, ad group, and targets that were previously added. + # DeleteCampaigns would remove the campaign and ad group, as well as the association + # between ad groups and campaigns. To explicitly delete the association between an entity + # and the target, use DeleteTargetFromCampaign and DeleteTargetFromAdGroup respectively. + + campaign_service.DeleteTargetFromCampaign(CampaignId=campaign_ids['long'][0]) + campaign_service.DeleteTargetFromAdGroup(AdGroupId=ad_group_ids['long'][0]) + + campaign_service.DeleteCampaigns( + AccountId=authorization_data.account_id, + CampaignIds=campaign_ids + ) + + for campaign_id in campaign_ids['long']: + output_status_message("Deleted CampaignId {0}\n".format(campaign_id)) + + # DeleteCampaigns deletes the association between the campaign and target, but does not + # delete the target from the customer library. + # Call the DeleteTargetsFromLibrary operation for each target that you want to delete. + # You must specify an array with exactly one item. + + campaign_service.DeleteTargetsFromLibrary(TargetIds={'long': [campaign_target_id] }) + output_status_message("Deleted TargetId {0}\n".format(campaign_target_id)) + + campaign_service.DeleteTargetsFromLibrary(TargetIds={'long': [ad_group_target_id] }) + output_status_message("Deleted TargetId {0}\n".format(ad_group_target_id)) + + 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/v11/auth_helper.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/auth_helper.py index 9448c2af..778ee54c 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/auth_helper.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/auth_helper.py @@ -106,13 +106,13 @@ def request_user_consent(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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/auth_helper.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/auth_helper.py.bak new file mode 100644 index 00000000..9448c2af --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/auth_helper.py.bak @@ -0,0 +1,219 @@ +import sys +import webbrowser +from time import gmtime, strftime +from suds import WebFault + +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads.v11 import * +from bingads.v11.bulk import * +from bingads.v11.reporting import * + +# Required +DEVELOPER_TOKEN='DeveloperTokenGoesHere' +ENVIRONMENT='production' + +# If you are using OAuth in production, CLIENT_ID is required and CLIENT_STATE is recommended. +# The REFRESH_TOKEN_PATH is required for the provided examples, although in production you should +# always store the refresh token securely. +CLIENT_ID='ClientIdGoesHere' +CLIENT_STATE='ClientStateGoesHere' +REFRESH_TOKEN_PATH="refresh.txt" + +# If you are not using OAuth in production, USER_NAME and PASSWORD are required. +USER_NAME='UserNameGoesHere' +PASSWORD='PasswordGoesHere' + +ALL_CAMPAIGN_TYPES=['DynamicSearchAds SearchAndContent Shopping'] + +ALL_AD_TYPES={ + 'AdType': ['AppInstall', 'DynamicSearch', 'ExpandedText', 'Product', 'Text'] +} + +def authenticate(authorization_data): + + #import logging + #logging.basicConfig(level=logging.INFO) + #logging.getLogger('suds.client').setLevel(logging.DEBUG) + #logging.getLogger('suds.transport.http').setLevel(logging.DEBUG) + + customer_service=ServiceClient( + 'CustomerManagementService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=11, + ) + + # 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(authorization_data) + + # 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(authorization_data) + + # 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(customer_service, 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 + +def authenticate_with_username(authorization_data): + + authentication=PasswordAuthentication( + user_name=USER_NAME, + password=PASSWORD + ) + + # Assign this authentication instance to the authorization_data. + authorization_data.authentication=authentication + +def authenticate_with_oauth(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 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(authorization_data) + 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(authorization_data) + +def request_user_consent(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_TOKEN_PATH) + 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_TOKEN_PATH,"w+") as file: + file.write(oauth_tokens.refresh_token) + file.close() + return None + +def search_accounts_by_user_id(customer_service, 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 + ''' + + 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 set_elements_to_none(suds_object): + # Bing Ads Campaign Management service operations require that if you specify a non-primitives, + # it must be one of the values defined by the service i.e. it cannot be a nil element. + # Since Suds requires non-primitives and Bing Ads won't accept nil elements in place of an enum value, + # you must either set the non-primitives or they must be set to None. Also in case new properties are added + # in a future service release, it is a good practice to set each element of the SUDS object to None as a baseline. + + for (element) in suds_object: + suds_object.__setitem__(element[0], None) + return suds_object + +# Set the read-only properties of a campaign to null. This operation can be useful between calls to +# GetCampaignsByIds and UpdateCampaigns. The update operation would fail if you send certain read-only +# fields. +def set_read_only_campaign_elements_to_none(campaign): + if campaign is not None: + campaign.CampaignType=None + campaign.Settings=None + campaign.Status=None + +# Set the read-only properties of an ad extension to null. This operation can be useful between calls to +# GetAdExtensionsByIds and UpdateAdExtensions. The update operation would fail if you send certain read-only +# fields. +def set_read_only_ad_extension_elements_to_none(extension): + if extension is None or extension.Id is None: + return extension + else: + # Set to None for all extension types. + extension.Version = None + + if extension.Type == 'LocationAdExtension': + extension.GeoCodeStatus = None + + return extension diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py index 67b4e31b..45d8bf6a 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py @@ -22,20 +22,20 @@ def main(authorization_data): try: get_geo_locations_file_url_response = campaign_service.GetGeoLocationsFileUrl(VERSION, LANGUAGE_LOCALE) - request=urllib2.Request(get_geo_locations_file_url_response.FileUrl) - response=urllib2.urlopen(request) + request=urllib.request.Request(get_geo_locations_file_url_response.FileUrl) + response=urllib.request.urlopen(request) if response.getcode() == 200: download_locations_file(response) - print("Downloaded the geographical locations to {0}.\n".format(LOCAL_FILE)) + print(("Downloaded the geographical locations to {0}.\n".format(LOCAL_FILE))) output_status_message("Program execution completed") - except urllib2.URLError as ex: + except urllib.error.URLError as ex: if hasattr(ex, 'code'): - print("Error code: {0}".format(ex.code)) + print(("Error code: {0}".format(ex.code))) elif hasattr(ex, 'reason'): - print("Reason: {0}".format(ex.reason)) + print(("Reason: {0}".format(ex.reason))) except WebFault as ex: output_webfault_errors(ex) diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py.bak new file mode 100644 index 00000000..67b4e31b --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v11/geographical_locations.py.bak @@ -0,0 +1,80 @@ +import urllib.request as urllib2 + +from auth_helper import * +from output_helper import * + +# You must provide credentials in auth_helper.py. + +# The full path to the local machine's copy of the geographical locations file. +LOCAL_FILE="c:\geolocations\geolocations.csv" + +# The language and locale of the geographical locations file available for download. +# This example uses 'en' (English). Supported locales are 'zh-Hant' (Traditional Chinese), 'en' (English), 'fr' (French), +# 'de' (German), 'it' (Italian), 'pt-BR' (Portuguese - Brazil), and 'es' (Spanish). + +LANGUAGE_LOCALE = "en" + +# The only supported file format version is 1.0. + +VERSION = "1.0" + +def main(authorization_data): + try: + get_geo_locations_file_url_response = campaign_service.GetGeoLocationsFileUrl(VERSION, LANGUAGE_LOCALE) + + request=urllib2.Request(get_geo_locations_file_url_response.FileUrl) + response=urllib2.urlopen(request) + + if response.getcode() == 200: + download_locations_file(response) + print("Downloaded the geographical locations to {0}.\n".format(LOCAL_FILE)) + + output_status_message("Program execution completed") + + except urllib2.URLError as ex: + if hasattr(ex, 'code'): + print("Error code: {0}".format(ex.code)) + elif hasattr(ex, 'reason'): + print("Reason: {0}".format(ex.reason)) + + except WebFault as ex: + output_webfault_errors(ex) + except Exception as ex: + output_status_message(ex) + +def download_locations_file(response): + CHUNK=16 * 1024 + with open(LOCAL_FILE, 'wb') as f: + while True: + chunk=response.read(CHUNK) + if not chunk: break + f.write(chunk) + f.flush() + +# Main execution +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") + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + campaign_service=ServiceClient( + service='CampaignManagementService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=11, + ) + + # 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(authorization_data) + + main(authorization_data) \ No newline at end of file diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py index 84af6bc1..adace05e 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py @@ -89,13 +89,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py.bak new file mode 100644 index 00000000..84af6bc1 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/CustomerSignup.py.bak @@ -0,0 +1,409 @@ +from bingads 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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. + ''' + #return None # To switch users for testing. + 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 output_status_message(message): + print(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/v9: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') + +# Main execution +if __name__ == '__main__': + + 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. + get_user_response=customer_service.GetUser(UserId=None) + user = get_user_response.User + + # Only a user with the aggregator role (33) can sign up new customers. + # If the user does not have the aggregator role, then do not continue. + if(not 33 in get_user_response.Roles['int']): + output_status_message("Only a user with the aggregator role (33) can sign up new customers."); + exit(0) + + # For Customer.CustomerAddress and Account.BusinessAddress, you can use the same address + # as your aggregator user, although you must set Id and TimeStamp to null. + user_address = user.ContactInfo.Address + user_address.Id = None + user_address.TimeStamp = None + + customer = customer_service.factory.create('ns5:Customer') + + # The customer's business address. + customer.CustomerAddress = user_address + + # The list of key and value strings for forward compatibility. This element can be used + # to avoid otherwise breaking changes when new elements are added in future releases. + # There are currently no forward compatibility changes for the Customer object. + customer.ForwardCompatibilityMap = None + + # The primary business segment of the customer, for example, automotive, food, or entertainment. + customer.Industry = 'Other' + + # The primary country where the customer operates. This country will be the + # default country for ad groups in the customer�s campaigns. + customer.MarketCountry = 'US' + + # The primary language that the customer uses. This language will be the + # default language for ad groups in the customer�s campaigns. + customer.MarketLanguage = 'English' + + # The name of the customer. This element can contain a maximum of 100 characters. + customer.Name = "Child Customer " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + + # SUDS requires that you set unused value sets to None + customer.CustomerFinancialStatus=None + customer.ServiceLevel=None + customer.CustomerLifeCycleStatus=None + + account=customer_service.factory.create('ns5:AdvertiserAccount') + + # The type of account. Bing Ads API only supports the Advertiser account. + account.AccountType = 'Advertiser' + + # The location where your business is legally registered. + # The business address is used to determine your tax requirements. + # BusinessAddress will be required in a future version of the Bing Ads API. + # Please start using it. + account.BusinessAddress = user_address + + # The type of currency that is used to settle the account. The service uses the currency information for billing purposes. + account.CurrencyType = 'USDollar' + + # Optionally you can set up each account with auto tagging. + # The AutoTag key and value pair is an account level setting that determines whether to append or replace + # the supported UTM tracking codes within the final URL of ads delivered. The default value is '0', and + # Bing Ads will not append any UTM tracking codes to your ad or keyword final URL. + account_FCM = customer_service.factory.create('ns0:ArrayOfKeyValuePairOfstringstring') + auto_tag=customer_service.factory.create('ns0:KeyValuePairOfstringstring') + auto_tag.key="AutoTag" + auto_tag.value="0" + account_FCM.KeyValuePairOfstringstring.append(auto_tag) + + # The list of key and value strings for forward compatibility. This element can be used + # to avoid otherwise breaking changes when new elements are added in future releases. + account.ForwardCompatibilityMap = account_FCM + + # The name of the account. The name can contain a maximum of 100 characters and must be unique within the customer. + account.Name = "Child Account " + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + + # The identifier of the customer that owns the account. In the Bing Ads API operations + # that require a customer identifier, this is the identifier that you set the CustomerId SOAP header to. + account.ParentCustomerId = user.CustomerId + + # The TaxId (VAT identifier) is optional. If specified, The VAT identifier must be valid + #in the country that you specified in the BusinessAddress element. Without a VAT registration + # number or exemption certificate, taxes might apply based on your business location. + account.TaxId = None + + # The default time-zone value to use for campaigns in this account. + # If not specified, the time zone will be set to PacificTimeUSCanadaTijuana by default. + # TimeZone will be required in a future version of the Bing Ads API. + # Please start using it. + account.TimeZone = 'PacificTimeUSCanadaTijuana' + + # SUDS requires that you set unused value sets to None + account.AccountFinancialStatus=None + account.Language=None + account.PaymentMethodType=None + account.AccountLifeCycleStatus=None + account.TaxType=None + account.TaxIdStatus=None + + # Signup a new customer and account for the reseller. + signup_customer_response = customer_service.SignupCustomer( + customer, + account, + user.CustomerId); + + output_status_message("New Customer and Account:\n") + + # This is the identifier that you will use to set the CustomerId + # element in most of the Bing Ads API service operations. + output_status_message("\tCustomerId: {0}".format(signup_customer_response.CustomerId)) + + # The read-only system-generated customer number that is used in the Bing Ads web application. + # The customer number is of the form, Cnnnnnnn, where nnnnnnn is a series of digits. + output_status_message("\tCustomerNumber: {0}".format(signup_customer_response.CustomerNumber)) + + # This is the identifier that you will use to set the AccountId and CustomerAccountId + # elements in most of the Bing Ads API service operations. + output_status_message("\tAccountId: {0}".format(signup_customer_response.AccountId)) + + # The read-only system generated account number that is used to identify the account in the Bing Ads web application. + # The account number has the form xxxxxxxx, where xxxxxxxx is a series of any eight alphanumeric characters. + output_status_message("\tAccountNumber: {0}\n".format(signup_customer_response.AccountNumber)) + + 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/v9/InviteUser.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/InviteUser.py index c1899139..6f0eac12 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/InviteUser.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/InviteUser.py @@ -95,13 +95,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/InviteUser.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/InviteUser.py.bak new file mode 100644 index 00000000..c1899139 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/InviteUser.py.bak @@ -0,0 +1,437 @@ +from bingads import * + +import sys +import webbrowser +from time import gmtime, strftime +from datetime import datetime, timedelta +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") + + # Specify the email address where the invitation should be sent. + # It is important to note that the recipient can accept the invitation + # and sign into Bing Ads with a Microsoft account different than the invitation email address. + user_invite_recipient_email = "" + + 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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. + ''' + #return None # To switch users for testing. + 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, + }, + ] + } + + return customer_service.SearchAccounts( + PageInfo = paging, + Predicates = predicates + ) + +def output_status_message(message): + print(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/v9: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_user_invitations(user_invitations): + if user_invitations is None: + return + + for user_invitation in user_invitations: + output_status_message("FirstName: {0}".format(user_invitation.FirstName)) + output_status_message("LastName: {0}".format(user_invitation.LastName)) + output_status_message("Email: {0}".format(user_invitation.Email)) + output_status_message("Role: {0}".format(user_invitation.Role)) + output_status_message("Invitation Id: {0}\n".format(user_invitation.Id)) + +def output_user(user): + if user is None: + return + + for user_invitation in user_invitations: + output_status_message("Id: {0}".format(user.Id)) + output_status_message("UserName: {0}".format(user.UserName)) + output_status_message("Contact Email: {0}".format(user.ContactInfo.Email)) + output_status_message("First Name: {0}".format(user.Name.FirstName)) + output_status_message("Last Name: {0}\n".format(user.Name.LastName)) + +# Main execution +if __name__ == '__main__': + + 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 + + output_status_message("You must edit this example to provide the email address (user_invite_recipient_email) for " \ + "the user invitation.") + output_status_message("Login as a Super Admin user to send a user invitation.\n") + + # Prepare to invite a new user + user_invitation = customer_service.factory.create('ns5:UserInvitation') + + # The identifier of the customer this user is invited to manage. + # The AccountIds element determines which customer accounts the user can manage. + user_invitation.CustomerId = authorization_data.customer_id + + # Users with account level roles such as Advertiser Campaign Manager can be restricted to specific accounts. + # Users with customer level roles such as Super Admin can access all accounts within the user’s customer, + # and their access cannot be restricted to specific accounts. + user_invitation.AccountIds=None + + #The user role, which determines the level of access that the user has to the accounts specified in the AccountIds element. + user_invitation.Role = 'AdvertiserCampaignManager' + + # The email address where the invitation should be sent. This element can contain a maximum of 100 characters. + user_invitation.Email = user_invite_recipient_email + + # The first name of the user. This element can contain a maximum of 40 characters. + user_invitation.FirstName = "FirstNameGoesHere" + + # The last name of the user. This element can contain a maximum of 40 characters. + user_invitation.LastName = "LastNameGoesHere" + + # The locale to use when sending correspondence to the user by email or postal mail. The default is EnglishUS. + user_invitation.Lcid = 'EnglishUS' + + + # Once you send a user invitation, there is no option to rescind the invitation using the API. + # You can delete a pending invitation in the Accounts & Billing -> Users tab of the Bing Ads web application. + user_invitation_id=customer_service.SendUserInvitation(user_invitation) + output_status_message("Sent new user invitation to {0}.\n".format(user_invite_recipient_email)) + + # It is possible to have multiple pending invitations sent to the same email address, + # which have not yet expired. It is also possible for those invitations to have specified + # different user roles, for example if you sent an invitation with an incorrect user role + # and then sent a second invitation with the correct user role. The recipient can accept + # any of the invitations. The Bing Ads API does not support any operations to delete + # pending user invitations. After you invite a user, the only way to cancel the invitation + # is through the Bing Ads web application. You can find both pending and accepted invitations + # in the Users section of Accounts & Billing. + + # Since a recipient can accept the invitation and sign into Bing Ads with a Microsoft account different + # than the invitation email address, you cannot determine with certainty the mapping from UserInvitation + # to accepted User. You can search by the invitation ID (returned by SendUserInvitations), + # only to the extent of finding out whether or not the invitation has been accepted or has expired. + # The SearchUserInvitations operation returns all pending invitations, whether or not they have expired. + # Accepted invitations are not included in the SearchUserInvitations response. + + # This example searches for all user invitations of the customer that you manage, + # and then filters the search results to find the invitation sent above. + # Note: In this example the invitation (sent above) should be active and not expired. You can set a breakpoint + # and then either accept or delete the invitation in the Bing Ads web application to change the invitation status. + + predicates = { + 'Predicate': [ + { + 'Field': 'CustomerId', + 'Operator': 'In', + 'Value': authorization_data.customer_id, + }, + ] + } + + user_invitations = customer_service.SearchUserInvitations( + Predicates = predicates + )['UserInvitation'] + output_status_message("Existing UserInvitation(s):\n") + output_user_invitations(user_invitations) + + # Determine whether the invitation has been accepted or has expired. + # If you specified a valid InvitationId, and if the invitation is not found, + # then the recipient has accepted the invitation. + # If the invitation is found, and if the expiration date is later than the current date and time, + # then the invitation is still pending and has not yet expired. + pending_invitation=next((invitation for invitation in user_invitations if + (invitation.Id == user_invitation_id and invitation.ExpirationDate - datetime.utcnow() > timedelta(seconds=0)) + ), "None") + + # You can send a new invitation if the invitation was either not found, has expired, + # or the user has accepted the invitation. This example does not send a new invitation if the + # invitationId was found and has not yet expired, i.e. the invitation is pending. + if pending_invitation is None or pending_invitation == 'None': + # Once you send a user invitation, there is no option to rescind the invitation using the API. + # You can delete a pending invitation in the Accounts & Billing -> Users tab of the Bing Ads web application. + user_invitation_id=customer_service.SendUserInvitation(user_invitation) + output_status_message("Sent new user invitation to {0}.\n".format(user_invite_recipient_email)) + + else: + output_status_message("UserInvitationId {0} is pending.\n".format(user_invitation_id)) + + # After the invitation has been accepted, you can call GetUsersInfo and GetUser to access the Bing Ads user details. + # Once again though, since a recipient can accept the invitation and sign into Bing Ads with a Microsoft account + # different than the invitation email address, you cannot determine with certainty the mapping from UserInvitation + # to accepted User. With the user ID returned by GetUsersInfo or GetUser, you can call DeleteUser to remove the user. + + users_info = customer_service.GetUsersInfo(CustomerId=authorization_data.customer_id)['UserInfo'] + confirmed_user_info=next((info for info in users_info if info.UserName == user_invite_recipient_email), "None") + + # If the user already accepted, you can call GetUser to view all user details. + if confirmed_user_info is not None and confirmed_user_info != 'None': + get_user_response=customer_service.GetUser(confirmed_user_info.Id) + output_status_message("Found Requested User Details (Not necessarily related to above Invitation ID(s):") + output_user(get_user_response.User) + output_status_message("Role Ids:") + output_status_message("; ".join(str(role) for role in get_user_response.Roles['int']) + "\n") + + # You have the option of calling DeleteUser to revoke their access to your customer accounts. + # Note: Only a super admin or aggregator user can delete users. + #time_stamp = customer_service.GetUser(confirmed_user_info.Id)['User'].TimeStamp + #customer_service.DeleteUser( + # UserId=confirmed_user_info.Id, + # TimeStamp=time_stamp + #) + #output_status_message("Deleted UserName {0}.\n".format(user_invite_recipient_email)) + + 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/v9/ManageClient.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ManageClient.py index b8684619..4561947f 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ManageClient.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ManageClient.py @@ -92,13 +92,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ManageClient.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ManageClient.py.bak new file mode 100644 index 00000000..b8684619 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ManageClient.py.bak @@ -0,0 +1,496 @@ +from bingads import * + +import sys +import webbrowser +from time import gmtime, strftime +import datetime +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' + + CLIENT_ACCOUNT_ID='YourClientAccountIdGoesHere' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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 add_client_links(client_links): + global customer_service + + return customer_service.AddClientLinks( + ClientLinks=client_links + ) + +def search_client_links(ordering, page_info, predicates): + global customer_service + + return customer_service.SearchClientLinks( + Ordering=ordering, + PageInfo=page_info, + Predicates=predicates + ).ClientLinks + +def update_client_links(client_links): + global customer_service + + return customer_service.UpdateClientLinks( + ClientLinks=client_links + ) + +def output_status_message(message): + print(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/v9: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_client_links(client_links): + if hasattr(client_links, 'ClientLink'): + for client_link in client_links['ClientLink']: + output_status_message("Status: {0}".format(client_link.Status)) + output_status_message("ClientAccountId: {0}".format(client_link.ClientAccountId)) + output_status_message("ClientAccountNumber: {0}".format(client_link.ClientAccountNumber)) + output_status_message("ManagingAgencyCustomerId: {0}".format(client_link.ManagingCustomerId)) + output_status_message("ManagingCustomerNumber: {0}".format(client_link.ManagingCustomerNumber)) + output_status_message("IsBillToClient: True" if client_link.IsBillToClient else "IsBillToClient: False") + output_status_message("InviterEmail: {0}".format(client_link.InviterEmail)) + output_status_message("InviterName: {0}".format(client_link.InviterName)) + output_status_message("InviterPhone: {0}".format(client_link.InviterPhone)) + output_status_message("LastModifiedByUserId: {0}".format(client_link.LastModifiedByUserId)) + output_status_message("LastModifiedDateTime: {0}".format(client_link.LastModifiedDateTime)) + output_status_message("Name: {0}".format(client_link.Name)) + output_status_message("Note: {0}".format(client_link.Note)) + output_status_message('') + +def output_partial_errors(operation_errors, partial_errors): + if hasattr(operation_errors, 'OperationError'): + for error in operation_errors['OperationError']: + output_status_message("OperationError"); + output_status_message("Code: {0}\nMessage: {1}\n".format(error.Code, error.Message)) + + if hasattr(partial_errors, 'ArrayOfOperationError'): + for errors in partial_errors['ArrayOfOperationError']: + if errors is not None: + for error in errors['OperationError']: + if error is not None: + output_status_message("OperationError"); + output_status_message("Code: {0}\nMessage: {1}\n".format(error.Code, error.Message)) + +# Main execution +if __name__ == '__main__': + + try: + authenticate_with_oauth() # To expedite testing + + # 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) + + # Required in this example for adding new client links + authorization_data.customer_id=accounts['Account'][0].ParentCustomerId + + output_status_message( + "You must edit this example to provide the ClientAccountId for the client link." \ + "When adding a client link, the client link's ManagingCustomerId is set to the CustomerId " \ + "of the current authenticated user, who must be a Super Admin of the agency." \ + "Login as an agency Super Admin user to send a client link invitation, or unlink an existing client link." \ + "Login as a client Super Admin user to accept a client link invitation.\n" + ) + + update_client_links_response=None + + # Specify the client link search criteria + + page_info=customer_service.factory.create('ns5:Paging') + page_info.Index=0 # The first page + page_info.Size=100 # The first 100 client links for this page of results + + ordering=customer_service.factory.create('ns5:ArrayOfOrderBy') + order_by=customer_service.factory.create('ns5:OrderBy') + order_by.Field='Number' + order_by.Order='Ascending' + ordering.OrderBy.append(order_by) + + predicates=customer_service.factory.create('ns5:ArrayOfPredicate') + predicate=customer_service.factory.create('ns5:Predicate') + predicate.Field='ClientAccountId' + predicate.Operator='In' + predicate.Value=CLIENT_ACCOUNT_ID + predicates.Predicate.append(predicate) + + # Search for client links that match the specified criteria. + + client_links=customer_service.SearchClientLinks( + Ordering=ordering, + PageInfo=page_info, + Predicates=predicates + ) + + ''' + Determine whether the agency is already managing the specified client account. + If a link exists with status either Active, LinkInProgress, LinkPending, + UnlinkInProgress, or UnlinkPending, the agency may not initiate a duplicate client link. + ''' + + client_link=None + new_link_required=True + + if len(client_links['ClientLink']) > 0: + client_link=client_links['ClientLink'][0] + + # Reformat the start date to trim tzinfo. This workaround is temporarily required because + # Customer Management service does not accept signed utc offset e.g. +00:00 + start_date=client_links['ClientLink'][0].StartDate + reformatted_start_date=(datetime.datetime( + year=start_date.year, + month=start_date.month, + day=start_date.day, + hour=start_date.hour, + minute=start_date.minute, + second=start_date.second, + microsecond=start_date.microsecond, + tzinfo=None).isoformat('T')) + client_link.StartDate=reformatted_start_date + + client_links=customer_service.factory.create('ns5:ArrayOfClientLink') + client_links.ClientLink.append(client_link) + + output_status_message("Current ClientLink Status: {0}.\n".format(client_link.Status)) + + # The agency may choose to initiate the unlink process, + # which would terminate the existing relationship with the client. + if client_link.Status == 'Active': + client_link.Status='UnlinkRequested' + update_client_links_response=customer_service.UpdateClientLinks(client_links) + output_status_message("UpdateClientLinks : UnlinkRequested.\n") + new_link_required=False + # Waiting on a system status transition or waiting for the StartDate. + elif client_link.Status == 'LinkAccepted': + output_status_message("The status is transitioning towards LinkInProgress.\n") + new_link_required=False + # Waiting on a system status transition. + elif client_link.Status == 'LinkInProgress': + output_status_message("The status is transitioning towards Active.\n") + new_link_required=False + # When the status is LinkPending, either the agency or client may update the status. + # The agency may choose to cancel the client link invitation; however, in this example + # the client will accept the invitation. + # If the client does not accept or decline the invitation within 30 days, and if the agency + # does not update the status to LinkCanceled, the system updates the status to LinkExpired. + elif client_link.Status == 'LinkPending': + ''' + client_link.Status='LinkCanceled' + update_client_links_response=customer_service.UpdateClientLinks(client_links) + output_status_message("UpdateClientLinks: LinkCanceled.\n") + ''' + client_link.Status='LinkAccepted' + update_client_links_response=customer_service.UpdateClientLinks(client_links) + output_status_message("UpdateClientLinks: LinkAccepted.\n") + new_link_required=False + # Waiting on a system status transition. + elif client_link.Status == 'UnlinkInProgress': + output_status_message("The status is transitioning towards Inactive.\n") + new_link_required=False + # Waiting on a system status transition. + elif client_link.Status == 'UnlinkPending': + output_status_message("The status is transitioning towards Inactive.\n") + new_link_required=False + # The link lifecycle has ended. + else: + output_status_message("A new client link invitation is required.\n") + + + # Print errors if any occurred when updating the client link. + if update_client_links_response is not None: + output_partial_errors( + update_client_links_response.OperationErrors, + update_client_links_response.PartialErrors + ) + + # If no links exist between the agency and specified client account, or a link exists with status + # either Inactive, LinkCanceled, LinkDeclined, LinkExpired, or LinkFailed, then the agency must + # initiate a new client link. + + if new_link_required: + client_links=customer_service.factory.create('ns5:ArrayOfClientLink') + client_link=customer_service.factory.create('ns5:ClientLink') + client_link.ClientAccountId=CLIENT_ACCOUNT_ID + client_link.ManagingCustomerId=authorization_data.customer_id + client_link.IsBillToClient=True + client_link.Name="My Client Link" + client_link.StartDate=None + client_link.SuppressNotification=True + client_link.Status=None + client_links.ClientLink.append(client_link) + + add_client_links_response=customer_service.AddClientLinks(client_links) + + # Print errors if any occurred when adding the client link. + + output_partial_errors(add_client_links_response.OperationErrors, add_client_links_response.PartialErrors) + output_status_message("The user attempted to add a new ClientLink.\n") + output_status_message("Login as the client Super Admin to accept the agency's request to manage AccountId {0}.\n".format(CLIENT_ACCOUNT_ID)) + + # Get and print the current client link + + client_links=customer_service.SearchClientLinks( + Ordering=ordering, + PageInfo=page_info, + Predicates=predicates + ) + + output_client_links(client_links) + + 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/v9/ReportRequests.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ReportRequests.py index 1692d28a..1cf46246 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ReportRequests.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ReportRequests.py @@ -124,13 +124,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ReportRequests.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ReportRequests.py.bak new file mode 100644 index 00000000..1692d28a --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/ReportRequests.py.bak @@ -0,0 +1,638 @@ +from bingads.service_client import ServiceClient +from bingads.authorization import * +from bingads import * +from bingads.reporting 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) +#logging.getLogger('suds.transport.http').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 report files. + FILE_DIRECTORY='c:/reports/' + + # The name of the report download file. + DOWNLOAD_FILE_NAME='download.csv' + + # The report file extension type. + REPORT_FILE_FORMAT='Csv' + + # The maximum amount of time (in milliseconds) that you want to wait for the report download. + TIMEOUT_IN_MILLISECONDS=3600000 + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + customer_service=ServiceClient( + 'CustomerManagementService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=9, + ) + + reporting_service_manager=ReportingServiceManager( + authorization_data=authorization_data, + poll_interval_in_milliseconds=5000, + environment=ENVIRONMENT, + ) + + # In addition to ReportingServiceManager, you will need a reporting ServiceClient + # to build the ReportRequest. + + reporting_service=ServiceClient( + 'ReportingService', + authorization_data=authorization_data, + environment=ENVIRONMENT, + version=9, + ) + + # You can print the service SOAP client to view the namespaces as needed. + #print(reporting_service.soap_client) + +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 output_status_message(message): + print(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/v9: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 get_budget_summary_report_request(): + ''' + Build a budget summary report request, including Format, ReportName, + Time, and Columns. + ''' + report_request=reporting_service.factory.create('BudgetSummaryReportRequest') + report_request.Format=REPORT_FILE_FORMAT + report_request.ReportName='My Budget Summary Report' + report_request.ReturnOnlyCompleteData=False + report_request.Language='English' + + scope=reporting_service.factory.create('AccountThroughCampaignReportScope') + scope.AccountIds={'long': [authorization_data.account_id] } + scope.Campaigns=None + report_request.Scope=scope + + report_time=reporting_service.factory.create('ReportTime') + # You may either use a custom date range or predefined time. + + #custom_date_range_start=reporting_service.factory.create('Date') + #custom_date_range_start.Day=1 + #custom_date_range_start.Month=1 + #custom_date_range_start.Year=int(strftime("%Y", gmtime()))-1 + #report_time.CustomDateRangeStart=custom_date_range_start + #custom_date_range_end=reporting_service.factory.create('Date') + #custom_date_range_end.Day=31 + #custom_date_range_end.Month=12 + #custom_date_range_end.Year=int(strftime("%Y", gmtime()))-1 + #report_time.CustomDateRangeEnd=custom_date_range_end + #report_time.PredefinedTime=None + + report_time.PredefinedTime='Yesterday' + report_request.Time=report_time + + # Specify the attribute and data report columns. + + report_columns=reporting_service.factory.create('ArrayOfBudgetSummaryReportColumn') + report_columns.BudgetSummaryReportColumn.append([ + 'AccountName', + 'AccountNumber', + 'CampaignName', + 'CurrencyCode', + 'Date', + 'DailySpend', + ]) + report_request.Columns=report_columns + + return report_request + +def get_keyword_performance_report_request(): + ''' + Build a keyword performance report request, including Format, ReportName, Aggregation, + Scope, Time, Filter, and Columns. + ''' + report_request=reporting_service.factory.create('KeywordPerformanceReportRequest') + report_request.Format=REPORT_FILE_FORMAT + report_request.ReportName='My Keyword Performance Report' + report_request.ReturnOnlyCompleteData=False + report_request.Aggregation='Daily' + report_request.Language='English' + + scope=reporting_service.factory.create('AccountThroughAdGroupReportScope') + scope.AccountIds={'long': [authorization_data.account_id] } + scope.Campaigns=None + scope.AdGroups=None + report_request.Scope=scope + + report_time=reporting_service.factory.create('ReportTime') + # You may either use a custom date range or predefined time. + + #custom_date_range_start=reporting_service.factory.create('Date') + #custom_date_range_start.Day=1 + #custom_date_range_start.Month=1 + #custom_date_range_start.Year=int(strftime("%Y", gmtime()))-1 + #report_time.CustomDateRangeStart=custom_date_range_start + #custom_date_range_end=reporting_service.factory.create('Date') + #custom_date_range_end.Day=31 + #custom_date_range_end.Month=12 + #custom_date_range_end.Year=int(strftime("%Y", gmtime()))-1 + #report_time.CustomDateRangeEnd=custom_date_range_end + #report_time.PredefinedTime=None + + report_time.PredefinedTime='Yesterday' + report_request.Time=report_time + + # If you specify a filter, results may differ from data you see in the Bing Ads web application + + #report_filter=reporting_service.factory.create('KeywordPerformanceReportFilter') + #report_filter.DeviceType=[ + # 'Computer', + # 'SmartPhone' + #] + #report_request.Filter=report_filter + + # Specify the attribute and data report columns. + + report_columns=reporting_service.factory.create('ArrayOfKeywordPerformanceReportColumn') + report_columns.KeywordPerformanceReportColumn.append([ + 'TimePeriod', + 'AccountId', + 'CampaignId', + 'Keyword', + 'KeywordId', + 'DeviceType', + 'BidMatchType', + 'Clicks', + 'Impressions', + 'Ctr', + 'AverageCpc', + 'Spend', + 'QualityScore', + ]) + report_request.Columns=report_columns + + # You may optionally sort by any KeywordPerformanceReportColumn, and optionally + # specify the maximum number of rows to return in the sorted report. + + report_sorts=reporting_service.factory.create('ArrayOfKeywordPerformanceReportSort') + report_sort=reporting_service.factory.create('KeywordPerformanceReportSort') + report_sort.SortColumn='Clicks' + report_sort.SortOrder='Ascending' + report_sorts.KeywordPerformanceReportSort.append(report_sort) + report_request.Sort=report_sorts + + report_request.MaxRows=10 + + return report_request + +def get_geo_location_performance_report_request(): + ''' + Build a geo location performance report request, including Format, ReportName, Aggregation, + Scope, Time, Filter, and Columns. + ''' + report_request=reporting_service.factory.create('GeoLocationPerformanceReportRequest') + report_request.Format=REPORT_FILE_FORMAT + report_request.ReportName='My Geo Location Performance Report' + report_request.ReturnOnlyCompleteData=False + report_request.Aggregation='Daily' + report_request.Language='English' + + scope=reporting_service.factory.create('AccountThroughAdGroupReportScope') + scope.AccountIds={'long': [authorization_data.account_id] } + scope.Campaigns=None + scope.AdGroups=None + report_request.Scope=scope + + report_time=reporting_service.factory.create('ReportTime') + # You may either use a custom date range or predefined time. + + #custom_date_range_start=reporting_service.factory.create('Date') + #custom_date_range_start.Day=1 + #custom_date_range_start.Month=1 + #custom_date_range_start.Year=int(strftime("%Y", gmtime()))-1 + #report_time.CustomDateRangeStart=custom_date_range_start + #custom_date_range_end=reporting_service.factory.create('Date') + #custom_date_range_end.Day=31 + #custom_date_range_end.Month=12 + #custom_date_range_end.Year=int(strftime("%Y", gmtime()))-1 + #report_time.CustomDateRangeEnd=custom_date_range_end + #report_time.PredefinedTime=None + + report_time.PredefinedTime='Yesterday' + report_request.Time=report_time + + # If you specify a filter, results may differ from data you see in the Bing Ads web application + + report_filter=reporting_service.factory.create('GeoLocationPerformanceReportFilter') + country_codes=reporting_service.factory.create('ns1:ArrayOfstring') + country_codes.string.append('US') + report_filter.CountryCode=country_codes + report_request.Filter=report_filter + + # Specify the attribute and data report columns. + + report_columns=reporting_service.factory.create('ArrayOfGeoLocationPerformanceReportColumn') + report_columns.GeoLocationPerformanceReportColumn.append([ + 'TimePeriod', + 'AccountId', + 'AccountName', + 'CampaignId', + 'AdGroupId', + 'LocationType', + 'Country', + 'Clicks', + 'Impressions', + 'Ctr', + 'AverageCpc', + 'Spend', + ]) + report_request.Columns=report_columns + + return report_request + +def background_completion(reporting_download_parameters): + ''' + You can submit a download request and the ReportingServiceManager will automatically + return results. The ReportingServiceManager abstracts the details of checking for result file + completion, and you don't have to write any code for results polling. + ''' + global reporting_service_manager + result_file_path = reporting_service_manager.download_file(reporting_download_parameters) + output_status_message("Download result file: {0}\n".format(result_file_path)) + +def submit_and_download(report_request): + ''' + Submit the download request and then use the ReportingDownloadOperation result to + track status until the report is complete e.g. either using + ReportingDownloadOperation.track() or ReportingDownloadOperation.get_status(). + ''' + global reporting_service_manager + reporting_download_operation = reporting_service_manager.submit_download(report_request) + + # You may optionally cancel the track() operation after a specified time interval. + reporting_operation_status = reporting_download_operation.track(timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS) + + # You can use ReportingDownloadOperation.track() to poll until complete as shown above, + # or use custom polling logic with get_status() as shown below. + #for i in range(10): + # time.sleep(reporting_service_manager.poll_interval_in_milliseconds / 1000.0) + + # download_status = reporting_download_operation.get_status() + + # if download_status.status == 'Success': + # break + + result_file_path = reporting_download_operation.download_result_file( + result_file_directory = FILE_DIRECTORY, + result_file_name = DOWNLOAD_FILE_NAME, + decompress = True, + overwrite = True, # Set this value true if you want to overwrite the same file. + timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS # You may optionally cancel the download after a specified time interval. + ) + + output_status_message("Download result file: {0}\n".format(result_file_path)) + +def download_results(request_id, authorization_data): + ''' + If for any reason you have to resume from a previous application state, + you can use an existing download request identifier and use it + to download the result file. Use ReportingDownloadOperation.track() to indicate that the application + should wait to ensure that the download status is completed. + ''' + reporting_download_operation = ReportingDownloadOperation( + request_id = request_id, + authorization_data=authorization_data, + poll_interval_in_milliseconds=1000, + environment=ENVIRONMENT, + ) + + # Use track() to indicate that the application should wait to ensure that + # the download status is completed. + # You may optionally cancel the track() operation after a specified time interval. + reporting_operation_status = reporting_download_operation.track(timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS) + + result_file_path = reporting_download_operation.download_result_file( + result_file_directory = FILE_DIRECTORY, + result_file_name = DOWNLOAD_FILE_NAME, + decompress = True, + overwrite = True, # Set this value true if you want to overwrite the same file. + timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS # You may optionally cancel the download after a specified time interval. + ) + + output_status_message("Download result file: {0}".format(result_file_path)) + output_status_message("Status: {0}\n".format(reporting_operation_status.status)) + +# Main execution +if __name__ == '__main__': + + 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 + + # You can submit one of the example reports, or build your own. + + #report_request=get_budget_summary_report_request() + #report_request=get_geo_location_performance_report_request() + report_request=get_keyword_performance_report_request() + + reporting_download_parameters = ReportingDownloadParameters( + report_request=report_request, + result_file_directory = FILE_DIRECTORY, + result_file_name = DOWNLOAD_FILE_NAME, + overwrite_result_file = True, # Set this value true if you want to overwrite the same file. + timeout_in_milliseconds=TIMEOUT_IN_MILLISECONDS # You may optionally cancel the download after a specified time interval. + ) + + #Option A - Background Completion with ReportingServiceManager + #You can submit a download request and the ReportingServiceManager will automatically + #return results. The ReportingServiceManager abstracts the details of checking for result file + #completion, and you don't have to write any code for results polling. + + output_status_message("Awaiting Background Completion . . ."); + background_completion(reporting_download_parameters) + + #Option B - Submit and Download with ReportingServiceManager + #Submit the download request and then use the ReportingDownloadOperation result to + #track status yourself using ReportingServiceManager.get_status(). + + output_status_message("Awaiting Submit and Download . . ."); + submit_and_download(report_request) + + #Option C - Download Results with ReportingServiceManager + #If for any reason you have to resume from a previous application state, + #you can use an existing download request identifier and use it + #to download the result file. + + #For example you might have previously retrieved a request ID using submit_download. + reporting_operation=reporting_service_manager.submit_download(report_request); + request_id=reporting_operation.request_id; + + #Given the request ID above, you can resume the workflow and download the report. + #The report request identifier is valid for two days. + #If you do not download the report within two days, you must request the report again. + output_status_message("Awaiting Download Results . . ."); + download_results(request_id, authorization_data) + + 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/v9/SearchUserAccounts.py b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/SearchUserAccounts.py index 0a887277..3be7ea89 100644 --- a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/SearchUserAccounts.py +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/SearchUserAccounts.py @@ -89,13 +89,13 @@ def request_user_consent(): 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( + response_uri=eval(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( + 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" diff --git a/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/SearchUserAccounts.py.bak b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/SearchUserAccounts.py.bak new file mode 100644 index 00000000..0a887277 --- /dev/null +++ b/examples/BingAdsPythonConsoleExamples/BingAdsPythonConsoleExamples/v9/SearchUserAccounts.py.bak @@ -0,0 +1,342 @@ +from bingads 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' + + authorization_data=AuthorizationData( + account_id=None, + customer_id=None, + developer_token=DEVELOPER_TOKEN, + authentication=None, + ) + + 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. + ''' + #return None # To switch users for testing. + 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 output_status_message(message): + print(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/v9: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_user(user): + output_status_message("User Id {0}".format(user.Id)) + output_status_message("UserName {0}".format(user.UserName)) + output_status_message("First Name {0}".format(user.Name.FirstName)) + output_status_message("Last Name {0}\n".format(user.Name.LastName)) + +def output_user_roles(roles): + for role in roles: + if role == 16: + output_status_message("16 - The user has the Advertiser Campaign Manager role.") + elif role == 33: + output_status_message("33 - The user has the Aggregator role.") + elif role == 41: + output_status_message("41 - The user has the Super Admin role.") + elif role == 100: + output_status_message("100 - The user has the Viewer role.") + else: + output_status_message("{0} - The user has a deprecated, internal, or unknown user role.".format(role)) + output_status_message('') + +def output_account(account): + if account is None: + return None + + output_status_message("Account Id {0}".format(account.Id)) + output_status_message("Account Number {0}".format(account.Number)) + output_status_message("Account Name {0}".format(account.Name)) + output_status_message("Account Parent Customer Id: {0}\n".format(account.ParentCustomerId)) + +# Main execution +if __name__ == '__main__': + + 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. + get_user_response=customer_service.GetUser(UserId=None) + user = get_user_response.User + accounts = search_accounts_by_user_id(user.Id) + + output_status_message("The user can access the following Bing Ads accounts: \n") + for account in accounts['Account']: + customer_service.GetAccount(AccountId=account.Id) + output_account(account) + + # Optionally you can find out which pilot features the customer is able to use. + # Each account could belong to a different customer, so use the customer ID in each account. + feature_pilot_flags = customer_service.GetCustomerPilotFeatures(CustomerId = account.ParentCustomerId) + output_status_message("Customer Pilot flags:") + output_status_message("; ".join(str(flag) for flag in feature_pilot_flags['int']) + "\n") + + # Optionally you can update each account with a tracking template. + #account_FCM = customer_service.factory.create('ns0:ArrayOfKeyValuePairOfstringstring') + #tracking_url_template=customer_service.factory.create('ns0:KeyValuePairOfstringstring') + #tracking_url_template.key="TrackingUrlTemplate" + #tracking_url_template.value="http://tracker.example.com/?season={_season}&promocode={_promocode}&u={lpurl}" + #account_FCM.KeyValuePairOfstringstring.append(tracking_url_template) + + #account.ForwardCompatibilityMap = account_FCM + #customer_service.UpdateAccount(account) + #output_status_message("Updated the account with a TrackingUrlTemplate: {0}\n".format(tracking_url_template.value)) + + 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/djangowebapp/ptvs_virtualenv_proxy.py b/examples/djangowebapp/ptvs_virtualenv_proxy.py index ce301299..cea64fb4 100644 --- a/examples/djangowebapp/ptvs_virtualenv_proxy.py +++ b/examples/djangowebapp/ptvs_virtualenv_proxy.py @@ -92,7 +92,7 @@ def get_wsgi_handler(handler_name): def get_virtualenv_handler(): log('Activating virtualenv with %s\n' % activate_this) - execfile(activate_this, dict(__file__=activate_this)) + exec(compile(open(activate_this).read(), activate_this, 'exec'), dict(__file__=activate_this)) log('Getting handler %s\n' % os.getenv('WSGI_ALT_VIRTUALENV_HANDLER')) handler = get_wsgi_handler(os.getenv('WSGI_ALT_VIRTUALENV_HANDLER')) diff --git a/examples/djangowebapp/ptvs_virtualenv_proxy.py.bak b/examples/djangowebapp/ptvs_virtualenv_proxy.py.bak new file mode 100644 index 00000000..ce301299 --- /dev/null +++ b/examples/djangowebapp/ptvs_virtualenv_proxy.py.bak @@ -0,0 +1,118 @@ + # ############################################################################ + # + # Copyright (c) Microsoft Corporation. + # + # This source code is subject to terms and conditions of the Apache License, Version 2.0. A + # copy of the license can be found in the License.html file at the root of this distribution. If + # you cannot locate the Apache License, Version 2.0, please send an email to + # vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound + # by the terms of the Apache License, Version 2.0. + # + # You must not remove this notice, or any other, from this software. + # + # ########################################################################### + +import datetime +import os +import sys + +if sys.version_info[0] == 3: + def to_str(value): + return value.decode(sys.getfilesystemencoding()) + + def execfile(path, global_dict): + """Execute a file""" + with open(path, 'r') as f: + code = f.read() + code = code.replace('\r\n', '\n') + '\n' + exec(code, global_dict) +else: + def to_str(value): + return value.encode(sys.getfilesystemencoding()) + +def log(txt): + """Logs fatal errors to a log file if WSGI_LOG env var is defined""" + log_file = os.environ.get('WSGI_LOG') + if log_file: + f = open(log_file, 'a+') + try: + f.write('%s: %s' % (datetime.datetime.now(), txt)) + finally: + f.close() + +ptvsd_secret = os.getenv('WSGI_PTVSD_SECRET') +if ptvsd_secret: + log('Enabling ptvsd ...\n') + try: + import ptvsd + try: + ptvsd.enable_attach(ptvsd_secret) + log('ptvsd enabled.\n') + except: + log('ptvsd.enable_attach failed\n') + except ImportError: + log('error importing ptvsd.\n'); + +def get_wsgi_handler(handler_name): + if not handler_name: + raise Exception('WSGI_HANDLER env var must be set') + + if not isinstance(handler_name, str): + handler_name = to_str(handler_name) + + module_name, _, callable_name = handler_name.rpartition('.') + should_call = callable_name.endswith('()') + callable_name = callable_name[:-2] if should_call else callable_name + name_list = [(callable_name, should_call)] + handler = None + + while module_name: + try: + handler = __import__(module_name, fromlist=[name_list[0][0]]) + for name, should_call in name_list: + handler = getattr(handler, name) + if should_call: + handler = handler() + break + except ImportError: + module_name, _, callable_name = module_name.rpartition('.') + should_call = callable_name.endswith('()') + callable_name = callable_name[:-2] if should_call else callable_name + name_list.insert(0, (callable_name, should_call)) + handler = None + + if handler is None: + raise ValueError('"%s" could not be imported' % handler_name) + + return handler + +activate_this = os.getenv('WSGI_ALT_VIRTUALENV_ACTIVATE_THIS') +if not activate_this: + raise Exception('WSGI_ALT_VIRTUALENV_ACTIVATE_THIS is not set') + +def get_virtualenv_handler(): + log('Activating virtualenv with %s\n' % activate_this) + execfile(activate_this, dict(__file__=activate_this)) + + log('Getting handler %s\n' % os.getenv('WSGI_ALT_VIRTUALENV_HANDLER')) + handler = get_wsgi_handler(os.getenv('WSGI_ALT_VIRTUALENV_HANDLER')) + log('Got handler: %r\n' % handler) + return handler + +def get_venv_handler(): + log('Activating venv with executable at %s\n' % activate_this) + import site + sys.executable = activate_this + old_sys_path, sys.path = sys.path, [] + + site.main() + + sys.path.insert(0, '') + for item in old_sys_path: + if item not in sys.path: + sys.path.append(item) + + log('Getting handler %s\n' % os.getenv('WSGI_ALT_VIRTUALENV_HANDLER')) + handler = get_wsgi_handler(os.getenv('WSGI_ALT_VIRTUALENV_HANDLER')) + log('Got handler: %r\n' % handler) + return handler diff --git a/make.py b/make.py index 4dea3b2c..af5e0dca 100644 --- a/make.py +++ b/make.py @@ -110,7 +110,7 @@ def make_execution_plan(func_name): next_tasks[dependent_func] = [] next_tasks[dependent_func].append(current) while len(in_degree) != 0: - for t in in_degree.keys(): + for t in list(in_degree.keys()): # can be optimized here, use linear search for easy to implemented and no performance concern currently if in_degree[t] == 0: t_selected = t @@ -205,19 +205,19 @@ def dist(): print('No task defined yet.') exit(0) max_length = 0 - for key in tasks.keys(): + for key in list(tasks.keys()): if len(key) > max_length: max_length = len(key) format_str = '{0:<' + str(max_length) + '} -- {1}' - for key in tasks.keys(): - print(format_str.format(key, tasks[key])) + for key in list(tasks.keys()): + print((format_str.format(key, tasks[key]))) elif len(argv) > 2: print('Only support one task at a time') exit(-1) else: task_name = argv[1] if task_name not in tasks: - print(str.format("Task: '{0}' not defined", task_name)) + print((str.format("Task: '{0}' not defined", task_name))) exit(-1) task_names = make_execution_plan(task_name) for task_name in task_names: diff --git a/make.py.bak b/make.py.bak new file mode 100644 index 00000000..4dea3b2c --- /dev/null +++ b/make.py.bak @@ -0,0 +1,225 @@ +import os +from subprocess import call + +dependent_graph = {} +tasks = {} + + +def dependent_on(*dependent_funcs): + """ Describe the dependencies of a specific functions. + + :param dependent_funcs: dependent functions + :return: decorated function + """ + + def dependencies_wrapper(func): + dependent_graph[func.__name__] = [dependent_func.__name__ for dependent_func in dependent_funcs] + return func + + return dependencies_wrapper + + +def task(description=None): + """ Describe the function be decorated by this task function. + + :param description: the description of decorated function + :return: decorated function + """ + + if description is None: + description = '' + + def task_wrapper(func): + tasks[func.__name__] = description + return func + + return task_wrapper + + +def delete_file(file_path): + """ Delete file specified by path. + + :param file_path: the file path + :type file_path: str + :return: None + """ + + try: + os.remove(file_path) + except OSError: + pass + + +def delete_folder(folder_path): + """ Delete folder specified by path and all the files under it recursively. + + :param folder_path: the folder path + :type folder_path: the folder path + :return: None + """ + + import shutil + + shutil.rmtree(folder_path, ignore_errors=True) + + +def run_cmd(command): + """ Run os command. + + :param command: the concrete content of command + :type command: str + :return: None + """ + + print(command) + call(command, shell=True) + + +def make_execution_plan(func_name): + """ Make the execution plan for a specific function, consider the dependencies. + + After analyze the dependency graph, use topological sort to give a proper execution order + + :param func_name: the function name to be executed + :type func_name: str + :return: the iterable function names, by execution order + :rtype: iter(str) + """ + + in_degree = {} + next_tasks = {} + + remaining = [func_name] + while len(remaining) != 0: + current = remaining.pop() + if current not in dependent_graph: + if current not in in_degree: + in_degree[current] = 0 + if current not in next_tasks: + next_tasks[current] = [] + continue + if current not in in_degree: + in_degree[current] = 0 + in_degree[current] += len(dependent_graph[current]) + if current not in next_tasks: + next_tasks[current] = [] + for dependent_func in dependent_graph[current]: + if dependent_func not in in_degree: + in_degree[dependent_func] = 0 + if dependent_func not in next_tasks: + next_tasks[dependent_func] = [] + next_tasks[dependent_func].append(current) + while len(in_degree) != 0: + for t in in_degree.keys(): + # can be optimized here, use linear search for easy to implemented and no performance concern currently + if in_degree[t] == 0: + t_selected = t + break + for t_next in next_tasks[t_selected]: + in_degree[t_next] -= 1 + in_degree.pop(t_selected) + next_tasks.pop(t_selected) + yield t_selected + + +########################## +# Define Tasks From Here # +########################## + + +@task('clean the temporary output file') +def clean(): + delete_folder('.tox') + delete_folder('bingads.egg-info') + delete_folder('docs/_build') + delete_folder('dist') + delete_file('.coverage') + + +@task('code style check by flake8') +def lint(): + run_cmd('flake8 bingads tests') + + +@task('run all tests under current interpreter, and print coverage report') +def test(): + run_cmd('coverage run --source bingads -m py.test -v --strict') + run_cmd('coverage report') + + +@task('run all unit tests under current interpreter, and print coverage report') +def ut(): + run_cmd('coverage run --source bingads -m py.test -k "not functional" -v --strict') + run_cmd('coverage report') + +@task('run all v10 unit tests under current interpreter, and print coverage report') +def v10_ut(): + run_cmd('coverage run --source bingads -m py.test v10tests/ -k "not functional" -v --strict') + run_cmd('coverage report') + +@task('run all v11 unit tests under current interpreter, and print coverage report') +def v11_ut(): + run_cmd('coverage run --source bingads -m py.test v11tests/ -k "not functional" -v --strict') + run_cmd('coverage report') + + +@task('run all functional tests under current interpreter.') +def ft(): + run_cmd('py.test -k "functional" -v --strict') + + +@task('run all v10 functional tests under current interpreter.') +def v10_ft(): + run_cmd('py.test v10tests/ -k "functional" -v --strict') + + +@task('run all v11 functional tests under current interpreter.') +def v11_ft(): + run_cmd('py.test v11tests/ -k "functional" -v --strict') + + +@task('run tests on all supported interpreters (check tox.ini)') +@dependent_on(clean) +def test_all(): + run_cmd('tox') + + +@task('generate static html documents, by sphinx, under docs/_build/html') +@dependent_on(clean) +def docs(): + run_cmd('sphinx-apidoc -F -o docs bingads') # generate API rst file, may need to check-in + run_cmd('sphinx-build -b html docs docs/_build/html') # generate html file + + +@task('make compressed installation package') +@dependent_on(clean) +def dist(): + run_cmd('python setup.py sdist --formats=gztar,zip') + + +if __name__ == '__main__': + from sys import argv, exit, modules + + if len(argv) == 1: + if len(tasks) == 0: + print('No task defined yet.') + exit(0) + max_length = 0 + for key in tasks.keys(): + if len(key) > max_length: + max_length = len(key) + format_str = '{0:<' + str(max_length) + '} -- {1}' + for key in tasks.keys(): + print(format_str.format(key, tasks[key])) + elif len(argv) > 2: + print('Only support one task at a time') + exit(-1) + else: + task_name = argv[1] + if task_name not in tasks: + print(str.format("Task: '{0}' not defined", task_name)) + exit(-1) + task_names = make_execution_plan(task_name) + for task_name in task_names: + task = getattr(modules[__name__], task_name) + task()