Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

Release History

13.0.26(2026-12-09)
+++++++++++++++++++++++++
API Updates:
* Update Bing Ads API Version 13 service proxies to reflect recent interface changes. For details please see the [Bing Ads API Release Notes](https://learn.microsoft.com/en-us/advertising/guides/release-notes?view=bingads-13).
* New report: BidStrategyReport.
* New column ConversionDelay in AccountPerformanceReport, CampaignPerformanceReport and GoalsAndFunnelsReport.
* New column CostPerConversion in AssetGroupPerformanceReport.
* New API in AdInsight Service: GetAudienceBreakdown.
* New criterion type: TopicCriterion.
* Support multiple campaignIds/adGroupIds in ImpressionBasedRemarketingList.
* New field Industry in UetTag.
* New fields TrackingUrlTemplate, FinalUrlSuffix and UrlCustomParameters in AssetGroup.
* New field IsPolitical in Campaign.
* New field CurrencyCode, ReportingTimeZone and Scope in BidStrategy.
* New field BidStrategyScope in Campaign.
* Add enum value DataDriven in AttributionModelType.
* New option NewTopicTargets and UpdateTopicTargets in GoogleImportOption.

Bulk Mapping Updates:
* Add bulk mappings: BulkAdGroupTopicCriterion, BulkContentPlacement and BulkTopic.
* Add TrackingTemplate, FinalUrlSuffix and CustomParameter in BulkAssetGroup mapping.
* Add BidStrategyScope, CurrencyCode and TimeZone in BulkBidStrategy mapping.
* Add BidStrategyScope in BulkCampaign mapping.

Other:
* Support for Google login is now available. You can use GoogleOAuthDesktopMobileAuthCodeGrant or GoogleOAuthWebAuthCodeGrant to sign in with a Google account and access the Bing Ads API.
* Remove pkg_resources and switch to importlib_resources.

13.0.25.3(2025-09-12)
+++++++++++++++++++++++++
* Update Bing Ads API Version 13 service proxies to reflect recent interface changes. For details please see the [Bing Ads API Release Notes](https://learn.microsoft.com/en-us/advertising/guides/release-notes?view=bingads-13).
Expand Down
95 changes: 80 additions & 15 deletions bingads/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@

from .exceptions import OAuthTokenRequestException

PRODUCTION='production'
SANDBOX='sandbox'
MSADS_MANAGE='msads.manage'
ADS_MANAGE='ads.manage'
BINGADS_MANAGE='bingads.manage'
MSA_PROD='msa.prod'
PRODUCTION = 'production'
SANDBOX = 'sandbox'
MSADS_MANAGE = 'msads.manage'
ADS_MANAGE = 'ads.manage'
BINGADS_MANAGE = 'bingads.manage'
MSA_PROD = 'msa.prod'
GOOGLE_PROD = 'google.prod'

class AuthorizationData:
""" Represents a user who intends to access the corresponding customer and account.
Expand Down Expand Up @@ -568,6 +569,62 @@ def __init__(self, client_id, oauth_tokens=None, env=PRODUCTION, oauth_scope=MSA
use_msa_prod=use_msa_prod
)

class GoogleOAuthDesktopMobileAuthCodeGrant(OAuthWithAuthorizationCode):
""" Represents a Google 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 Google OAuth 2.0, which follows the standard OAuth 2.0 flow
as defined in detail in the <see href="https://tools.ietf.org/html/rfc6749#section-4.1">Authorization Code Grant section of the OAuth 2.0 spec</see>.
The Google access token obtained through this flow can be passed to the Bing Ads API backend in the same manner as Microsoft tokens.
"""

def __init__(self, client_id, client_secret=None, oauth_tokens=None, env=PRODUCTION, oauth_scope=GOOGLE_PROD, tenant='common'):
""" 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(GoogleOAuthDesktopMobileAuthCodeGrant, self).__init__(
client_id,
client_secret,
_UriOAuthService.REDIRECTION_URI[(env, oauth_scope)],
oauth_tokens=oauth_tokens,
env=env,
oauth_scope=oauth_scope,
tenant=tenant
)

class GoogleOAuthWebAuthCodeGrant(OAuthWithAuthorizationCode):
""" Represents a Google 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
to authenticate with Bing Ads REST API services.
"""

def __init__(self, client_id, client_secret, redirect_url, oauth_tokens=None, env=PRODUCTION, oauth_scope=GOOGLE_PROD, tenant='common'):
""" Initializes a new instance of 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: OAuth token information
:type oauth_tokens: OAuthTokens
"""
super(GoogleOAuthWebAuthCodeGrant, self).__init__(
client_id,
client_secret,
redirect_url,
oauth_tokens=oauth_tokens,
env=env,
oauth_scope=oauth_scope,
tenant=tenant
)


class OAuthWebAuthCodeGrant(OAuthWithAuthorizationCode):
""" Represents an OAuth authorization object implementing the authorization code grant flow for use in a web application.
Expand Down Expand Up @@ -666,33 +723,41 @@ class _UriOAuthService:
def __init__(self):
pass

REDIRECTION_URI={
REDIRECTION_URI = {
(PRODUCTION, MSADS_MANAGE): 'https://login.microsoftonline.com/common/oauth2/nativeclient',
(PRODUCTION, ADS_MANAGE): 'https://login.microsoftonline.com/common/oauth2/nativeclient',
(PRODUCTION, BINGADS_MANAGE): 'https://login.live.com/oauth20_desktop.srf',
(SANDBOX, MSADS_MANAGE): 'https://login.windows-ppe.net/common/oauth2/nativeclient',
(SANDBOX, MSA_PROD): 'https://login.microsoftonline.com/common/oauth2/nativeclient'
(SANDBOX, MSA_PROD): 'https://login.microsoftonline.com/common/oauth2/nativeclient',
(PRODUCTION, GOOGLE_PROD): 'http://localhost',
(SANDBOX, GOOGLE_PROD): 'http://localhost'
}
AUTH_TOKEN_URI={
AUTH_TOKEN_URI = {
(PRODUCTION, MSADS_MANAGE): 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
(PRODUCTION, ADS_MANAGE): 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
(PRODUCTION, BINGADS_MANAGE): 'https://login.live.com/oauth20_token.srf',
(SANDBOX, MSADS_MANAGE): 'https://login.windows-ppe.net/consumers/oauth2/v2.0/token',
(SANDBOX, MSA_PROD): 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
(SANDBOX, MSA_PROD): 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
(PRODUCTION, GOOGLE_PROD): 'https://oauth2.googleapis.com/token',
(SANDBOX, GOOGLE_PROD): 'https://oauth2.googleapis.com/token'
}
AUTHORIZE_URI={
AUTHORIZE_URI = {
(PRODUCTION, MSADS_MANAGE): 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={0}&scope=https%3A%2F%2Fads.microsoft.com%2Fmsads.manage%20offline_access&response_type={1}&redirect_uri={2}',
(PRODUCTION, ADS_MANAGE): 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={0}&scope=https%3A%2F%2Fads.microsoft.com%2Fads.manage%20offline_access&response_type={1}&redirect_uri={2}',
(PRODUCTION, BINGADS_MANAGE): 'https://login.live.com/oauth20_authorize.srf?client_id={0}&scope=bingads.manage&response_type={1}&redirect_uri={2}',
(SANDBOX, MSADS_MANAGE): 'https://login.windows-ppe.net/consumers/oauth2/v2.0/authorize?client_id={0}&scope=https://api.ads.microsoft.com/msads.manage%20offline_access&response_type={1}&redirect_uri={2}&prompt=login',
(SANDBOX, MSA_PROD): 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={0}&scope=https%3A%2F%2Fsi.ads.microsoft.com%2Fmsads.manage%20offline_access&response_type={1}&redirect_uri={2}'
(SANDBOX, MSA_PROD): 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={0}&scope=https%3A%2F%2Fsi.ads.microsoft.com%2Fmsads.manage%20offline_access&response_type={1}&redirect_uri={2}',
(PRODUCTION, GOOGLE_PROD): 'https://accounts.google.com/o/oauth2/v2/auth?client_id={0}&scope=openid%20email%20profile&response_type={1}&redirect_uri={2}&access_type=offline&prompt=consent',
(SANDBOX, GOOGLE_PROD): 'https://accounts.google.com/o/oauth2/v2/auth?client_id={0}&scope=openid%20email%20profile&response_type={1}&redirect_uri={2}&access_type=offline&prompt=consent'
}
SCOPE={
SCOPE = {
(PRODUCTION, MSADS_MANAGE): 'https://ads.microsoft.com/msads.manage offline_access',
(PRODUCTION, ADS_MANAGE): 'https://ads.microsoft.com/ads.manage offline_access',
(PRODUCTION, BINGADS_MANAGE): 'bingads.manage',
(SANDBOX, MSADS_MANAGE): 'https://api.ads.microsoft.com/msads.manage offline_access',
(SANDBOX, MSA_PROD): 'https://si.ads.microsoft.com/msads.manage offline_access'
(SANDBOX, MSA_PROD): 'https://si.ads.microsoft.com/msads.manage offline_access',
(PRODUCTION, GOOGLE_PROD): 'openid email profile',
(SANDBOX, GOOGLE_PROD): 'openid email profile'
}

@staticmethod
Expand Down Expand Up @@ -725,4 +790,4 @@ def get_access_token(**kwargs):
raise OAuthTokenRequestException(error_json.get('error'), error_json.get('error_description'))

r_json = json.loads(r.text)
return OAuthTokens(r_json['access_token'], int(r_json['expires_in']), r_json['refresh_token'], r_json)
return OAuthTokens(r_json['access_token'], int(r_json['expires_in']), r_json.get('refresh_token', None), r_json)
2 changes: 1 addition & 1 deletion bingads/manifest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
VERSION = '13.0.25.3'
VERSION = '13.0.26'
BULK_FORMAT_VERSION_6 = '6.0'
WORKING_NAME = 'BingAdsSDKPython'
USER_AGENT = '{0} {1} {2}'.format(WORKING_NAME, VERSION, sys.version_info[0:3])
16 changes: 10 additions & 6 deletions bingads/service_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, resolver):
@type resolver: L{resolver.Resolver}
"""
self.resolver = resolver

def skip_value(self, type):
""" whether or not to skip setting the value """
return False
Expand All @@ -45,7 +45,7 @@ def __init__(self, service, version, authorization_data=None, environment='produ
self._authorization_data = authorization_data
self._refresh_oauth_tokens_automatically = True
self._version = ServiceClient._format_version(version)


# Use in-memory cache by default.
if 'cache' not in suds_options:
Expand All @@ -68,7 +68,7 @@ def __getattr__(self, name):

self.set_options(**self._options)
return _ServiceCall(self, name)

def get_response_header(self):
return self.hp.get_response_header()

Expand Down Expand Up @@ -109,7 +109,7 @@ def factory(self):
"""

return self.soap_client.factory

@property
def service_url(self):
""" The wsdl url of service based on the specific service and environment.
Expand Down Expand Up @@ -304,14 +304,18 @@ def name(self):
return self._name


import pkg_resources
import sys
if sys.version_info >= (3, 9):
import importlib.resources as importlib_resources
else:
import importlib_resources
import types
from suds.sudsobject import Property
from suds.sax.text import Text

# this is used to create entity only. Given the sandbox should have the same contract, we are good to use sandbox wsdl.
_CAMPAIGN_MANAGEMENT_SERVICE_V13 = Client(
'file:///' + pkg_resources.resource_filename('bingads', 'v13/proxies/sandbox/campaignmanagement_service.xml'), cache=DictCache())
'file:///' + str(importlib_resources.files('bingads').joinpath('v13/proxies/sandbox/campaignmanagement_service.xml')), cache=DictCache())
_CAMPAIGN_OBJECT_FACTORY_V13 = _CAMPAIGN_MANAGEMENT_SERVICE_V13.factory
_CAMPAIGN_OBJECT_FACTORY_V13.builder = BingAdsBuilder(_CAMPAIGN_OBJECT_FACTORY_V13.builder.resolver)

Expand Down
10 changes: 7 additions & 3 deletions bingads/service_info.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import pkg_resources
import sys
if sys.version_info >= (3, 9):
import importlib.resources as importlib_resources
else:
import importlib_resources

_SERVICE_LIST = ['adinsight', 'bulk', 'campaignmanagement', 'customerbilling', 'customermanagement', 'reporting']

SERVICE_INFO_DICT_V13 = {}

for service in _SERVICE_LIST:
SERVICE_INFO_DICT_V13[(service, 'production')] = 'file:///' + pkg_resources.resource_filename('bingads', 'v13/proxies/production/%s_service.xml' % (service))
SERVICE_INFO_DICT_V13[(service, 'sandbox')] = 'file:///' + pkg_resources.resource_filename('bingads', 'v13/proxies/sandbox/%s_service.xml' % (service))
SERVICE_INFO_DICT_V13[(service, 'production')] = 'file:///' + str(importlib_resources.files('bingads').joinpath('v13/proxies/production/%s_service.xml' % (service)))
SERVICE_INFO_DICT_V13[(service, 'sandbox')] = 'file:///' + str(importlib_resources.files('bingads').joinpath('v13/proxies/sandbox/%s_service.xml' % (service)))

SERVICE_INFO_DICT = {13: SERVICE_INFO_DICT_V13}
2 changes: 2 additions & 0 deletions bingads/v13/bulk/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@
from .bulk_new_customer_acquisition_goal import *
from .goals import *
from .account_placement_exclusion_list import *
from .bulk_topic import *
from .bulk_content_placement import *
15 changes: 15 additions & 0 deletions bingads/v13/bulk/entities/bulk_asset_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ def asset_group(self, asset_group):
field_to_csv=lambda c: bulk_optional_str(c.asset_group.Path2, c.asset_group.Id),
csv_to_field=lambda c, v: setattr(c.asset_group, 'Path2', v)
),
_SimpleBulkMapping(
header=_StringTable.TrackingTemplate,
field_to_csv=lambda c: bulk_optional_str(c.asset_group.TrackingUrlTemplate, c.asset_group.Id),
csv_to_field=lambda c, v: setattr(c.asset_group, 'TrackingUrlTemplate', v if v else None)
),
_SimpleBulkMapping(
header=_StringTable.FinalUrlSuffix,
field_to_csv=lambda c: bulk_optional_str(c.asset_group.FinalUrlSuffix, c.asset_group.Id),
csv_to_field=lambda c, v: setattr(c.asset_group, 'FinalUrlSuffix', v if v else None)
),
_SimpleBulkMapping(
header=_StringTable.CustomParameter,
field_to_csv=lambda c: field_to_csv_UrlCustomParameters(c.asset_group),
csv_to_field=lambda c, v: csv_to_field_UrlCustomParameters(c.asset_group, v)
),
]


Expand Down
15 changes: 15 additions & 0 deletions bingads/v13/bulk/entities/bulk_bid_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ def account_id(self, value):
field_to_csv=lambda c: bulk_str(c.bid_strategy.AssociatedCampaignType),
csv_to_field=lambda c, v: setattr(c.bid_strategy, 'AssociatedCampaignType', v if v else None)
),
_SimpleBulkMapping(
header=_StringTable.BidStrategyScope,
field_to_csv=lambda c: bulk_str(c.bid_strategy.Scope),
csv_to_field=lambda c, v: csv_to_field_enum(c.bid_strategy, v, 'Scope', EntityScope)
),
_SimpleBulkMapping(
header=_StringTable.CurrencyCode,
field_to_csv=lambda c: bulk_str(c.bid_strategy.CurrencyCode),
csv_to_field=lambda c, v: setattr(c.bid_strategy, 'CurrencyCode', v)
),
_SimpleBulkMapping(
header=_StringTable.TimeZone,
field_to_csv=lambda c: bulk_str(c.bid_strategy.ReportingTimeZone),
csv_to_field=lambda c, v: setattr(c.bid_strategy, 'ReportingTimeZone', v)
),
_ComplexBulkMapping(bid_strategy_biddingscheme_to_csv, csv_to_bid_strategy_biddingscheme),
]

Expand Down
14 changes: 14 additions & 0 deletions bingads/v13/bulk/entities/bulk_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(self, account_id=None, campaign=None):
self._destination_channel = None
self._is_multi_channel_campaign = None
self._should_serve_on_msan = None
self._scope = None

@property
def account_id(self):
Expand Down Expand Up @@ -148,6 +149,14 @@ def should_serve_on_msan(self):
def should_serve_on_msan(self, value):
self._should_serve_on_msan = value

@property
def scope(self):
return self._scope

@scope.setter
def scope(self, value):
self._scope = value

def _get_dynamic_feed_setting(self):
return self._get_setting(_DynamicFeedSetting, 'DynamicFeedSetting')

Expand Down Expand Up @@ -844,6 +853,11 @@ def _write_website(c):
field_to_csv=lambda c: field_to_csv_bool(c.campaign.IsPolitical),
csv_to_field=lambda c, v: setattr(c.campaign, 'IsPolitical', parse_bool(v))
),
_SimpleBulkMapping(
header=_StringTable.BidStrategyScope,
field_to_csv=lambda c: bulk_str(c.scope),
csv_to_field=lambda c, v: csv_to_field_enum(c, v, 'scope', EntityScope)
),
]

def read_additional_data(self, stream_reader):
Expand Down
Loading