From e9c3f4ac1ae82af3d0525dc9582cdacfee9b7ed7 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 5 Apr 2018 16:01:32 -0400 Subject: [PATCH 01/16] Added basic support to retrieve ad units. --- dfp/get_ad_units.py | 51 +++++++++++++++++++++++++++++++++ tasks/add_new_prebid_partner.py | 5 ++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 dfp/get_ad_units.py diff --git a/dfp/get_ad_units.py b/dfp/get_ad_units.py new file mode 100644 index 0000000..1c87d6a --- /dev/null +++ b/dfp/get_ad_units.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging + +from googleads import dfp + +from dfp.client import get_client + + +logger = logging.getLogger(__name__) + +def get_all_ad_units(print_ad_units=False): + """ + Gets ad units from DFP. + + Returns: + array of ad units + """ + + # Initialize units array + ad_units = [] + + dfp_client = get_client() + + # Initialize appropriate service. + ad_unit_service = dfp_client.GetService('InventoryService', version='v201802') + + # Create a statement to select ad units. + statement = dfp.StatementBuilder() + + # Retrieve a small amount of ad units at a time, paging + # through until all ad units have been retrieved. + while True: + response = ad_unit_service.getAdUnitsByStatement(statement.ToStatement()) + if 'results' in response: + for ad_unit in response['results']: + ad_units.append(ad_unit) + if print_ad_units: + print('Ad unit with ID "%s" and name "%s" was found.' % (ad_unit['id'], ad_unit['name'])) + statement.offset += dfp.SUGGESTED_PAGE_LIMIT + else: + break + + return ad_units + +def main(): + get_all_ad_units(print_ad_units=True) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tasks/add_new_prebid_partner.py b/tasks/add_new_prebid_partner.py index 00698d9..9ed039d 100755 --- a/tasks/add_new_prebid_partner.py +++ b/tasks/add_new_prebid_partner.py @@ -278,14 +278,15 @@ def main(): raise MissingSettingException('DFP_ORDER_NAME') placements = getattr(settings, 'DFP_TARGETED_PLACEMENT_NAMES', None) + no_inventory = getattr(settings, 'DFP_ALLOW_NO_INVENTORY_TARGETING', None) if placements is None: raise MissingSettingException('DFP_TARGETED_PLACEMENT_NAMES') - elif len(placements) < 1: + elif len(placements) < 1 and no_inventory is not True: raise BadSettingException('The setting "DFP_TARGETED_PLACEMENT_NAMES" ' 'must contain at least one DFP placement ID.') sizes = getattr(settings, 'DFP_PLACEMENT_SIZES', None) - if sizes is None: + if sizes is None : raise MissingSettingException('DFP_PLACEMENT_SIZES') elif len(sizes) < 1: raise BadSettingException('The setting "DFP_PLACEMENT_SIZES" ' From d89af9db2c33a5f709381df613860b06cf7f4ebb Mon Sep 17 00:00:00 2001 From: root Date: Thu, 5 Apr 2018 18:19:14 -0400 Subject: [PATCH 02/16] Completed targeting of root ad unit when no placement is provided. --- dfp/create_line_items.py | 18 ++++++++++++++---- dfp/get_ad_units.py | 13 +++++++++++++ tasks/add_new_prebid_partner.py | 14 +++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/dfp/create_line_items.py b/dfp/create_line_items.py index 8763926..92513a3 100755 --- a/dfp/create_line_items.py +++ b/dfp/create_line_items.py @@ -25,7 +25,7 @@ def create_line_items(line_items): def create_line_item_config(name, order_id, placement_ids, cpm_micro_amount, sizes, hb_bidder_key_id, hb_pb_key_id, hb_bidder_value_id, hb_pb_value_id, - currency_code='USD'): + currency_code='USD', root_ad_unit=None): """ Creates a line item config object. @@ -79,15 +79,25 @@ def create_line_item_config(name, order_id, placement_ids, cpm_micro_amount, 'children': [hb_bidder_criteria, hb_pb_criteria] } + targeting = { + 'targetedPlacementIds': placement_ids + } + + if not root_ad_unit is None: + targeting = { + 'targetedAdUnits': [{ + 'adUnitId': root_ad_unit['id'], + 'includeDescendants': True + }] + } + # https://developers.google.com/doubleclick-publishers/docs/reference/v201802/LineItemService.LineItem line_item_config = { 'name': name, 'orderId': order_id, # https://developers.google.com/doubleclick-publishers/docs/reference/v201802/LineItemService.Targeting 'targeting': { - 'inventoryTargeting': { - 'targetedPlacementIds': placement_ids - }, + 'inventoryTargeting': targeting, 'customTargeting': top_set, }, 'startDateTimeType': 'IMMEDIATELY', diff --git a/dfp/get_ad_units.py b/dfp/get_ad_units.py index 1c87d6a..a6456fc 100644 --- a/dfp/get_ad_units.py +++ b/dfp/get_ad_units.py @@ -44,6 +44,19 @@ def get_all_ad_units(print_ad_units=False): return ad_units +def get_root_ad_unit(): + """ + Gets root ad unit from DFP. + + Returns: + an ad unit, or None + """ + ad_units = get_all_ad_units() + for ad_unit in ad_units: + if not hasattr(ad_unit, 'parentId'): + return ad_unit + return None + def main(): get_all_ad_units(print_ad_units=True) diff --git a/tasks/add_new_prebid_partner.py b/tasks/add_new_prebid_partner.py index 9ed039d..e70f463 100755 --- a/tasks/add_new_prebid_partner.py +++ b/tasks/add_new_prebid_partner.py @@ -19,9 +19,11 @@ import dfp.get_custom_targeting import dfp.get_placements import dfp.get_users +import dfp.get_ad_units from dfp.exceptions import ( BadSettingException, - MissingSettingException + MissingSettingException, + DFPObjectNotFound ) from tasks.price_utils import ( get_prices_array, @@ -181,6 +183,15 @@ def create_line_item_configs(prices, order_id, placement_ids, bidder_code, hb_bidder_value_id = HBBidderValueGetter.get_value_id(bidder_code) line_items_config = [] + root_ad_unit = None + + if not placement_ids: + # Since the placement ids array is empty, it means we should target a run of network for the line item + root_ad_unit = dfp.get_ad_units.get_root_ad_unit() + + if root_ad_unit is None: + raise DFPObjectNotFound('Could not find the root ad unit to target a run of network.') + for price in prices: price_str = num_to_str(micro_amount_to_num(price)) @@ -205,6 +216,7 @@ def create_line_item_configs(prices, order_id, placement_ids, bidder_code, hb_bidder_value_id=hb_bidder_value_id, hb_pb_value_id=hb_pb_value_id, currency_code=currency_code, + root_ad_unit=root_ad_unit ) line_items_config.append(config) From dabc75053d86eda547aee7cc2bc2627da9eaec58 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 6 Apr 2018 10:58:36 -0400 Subject: [PATCH 03/16] Changes to the settings file. --- settings.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index 47c4377..031a70c 100644 --- a/settings.py +++ b/settings.py @@ -18,9 +18,12 @@ # The exact name of the DFP advertiser for the created order DFP_ADVERTISER_NAME = None -# Names of placements the line items should target. +# Names of placements the line items should target. Has priority over ad units. DFP_TARGETED_PLACEMENT_NAMES = [] +# Names of ad units the line items should target. +DFP_TARGETED_ADUNIT_NAMES = [] + # Sizes of placements. These are used to set line item and creative sizes. DFP_PLACEMENT_SIZES = [ { @@ -33,6 +36,11 @@ }, ] +# If no placement or ad unit should be used, for example if the user wants a run +# of network. If True, DFP_PLACEMENT_SIZE and DFP_TARGETED_PLACEMENT_NAMES need +# to be set to an empty array. +DFP_ALLOW_NO_INVENTORY_TARGETING = False + # Whether we should create the advertiser in DFP if it does not exist. # If False, the program will exit rather than create an advertiser. DFP_CREATE_ADVERTISER_IF_DOES_NOT_EXIST = False @@ -81,4 +89,4 @@ try: from local_settings import * except ImportError: - pass + pass \ No newline at end of file From 346b83495d6d4e9be696308b938af87d10a490db Mon Sep 17 00:00:00 2001 From: root Date: Fri, 6 Apr 2018 14:09:37 -0400 Subject: [PATCH 04/16] Completed implementation of Prebid params. --- dfp/create_creatives.py | 15 ++++++++ dfp/creative_snippet.html | 2 +- settings.py | 4 +- tasks/add_new_prebid_partner.py | 66 +++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/dfp/create_creatives.py b/dfp/create_creatives.py index e973c54..40deb8d 100644 --- a/dfp/create_creatives.py +++ b/dfp/create_creatives.py @@ -7,6 +7,8 @@ from dfp.client import get_client +import settings + logger = logging.getLogger(__name__) @@ -48,6 +50,19 @@ def create_creative_config(name, advertiser_id): with open(snippet_file_path, 'r') as snippet_file: snippet = snippet_file.read() + # Determine what bidder params should be + bidder_code = getattr(settings, 'PREBID_BIDDER_CODE', None) + bidder_params = getattr(settings, 'PREBID_BIDDER_PARAMS', None) + hb_adid_key = 'hb_adid' + + if bidder_params is True: + hb_adid_key += '_' + bidder_code + + if len(hb_adid_key) > 20: + hb_adid_key = hb_adid_key[:20] + + snippet = snippet.replace('{hb_adid_key}', hb_adid_key) + # https://developers.google.com/doubleclick-publishers/docs/reference/v201802/CreativeService.Creative config = { 'xsi_type': 'ThirdPartyCreative', diff --git a/dfp/creative_snippet.html b/dfp/creative_snippet.html index 4a68081..5437527 100644 --- a/dfp/creative_snippet.html +++ b/dfp/creative_snippet.html @@ -4,7 +4,7 @@ w = w.parent; if (w.pbjs) { try { - w.pbjs.renderAd(document, '%%PATTERN:hb_adid%%'); + w.pbjs.renderAd(document, '%%PATTERN:{hb_adid_key}%%'); break; } catch (e) { continue; diff --git a/settings.py b/settings.py index 031a70c..eb68119 100644 --- a/settings.py +++ b/settings.py @@ -37,8 +37,8 @@ ] # If no placement or ad unit should be used, for example if the user wants a run -# of network. If True, DFP_PLACEMENT_SIZE and DFP_TARGETED_PLACEMENT_NAMES need -# to be set to an empty array. +# of network. If True, DFP_TARGETED_PLACEMENT_NAMES and DFP_TARGETED_ADUNIT_NAMES +# still need to be set to empty arrays. DFP_ALLOW_NO_INVENTORY_TARGETING = False # Whether we should create the advertiser in DFP if it does not exist. diff --git a/tasks/add_new_prebid_partner.py b/tasks/add_new_prebid_partner.py index e70f463..8297a24 100755 --- a/tasks/add_new_prebid_partner.py +++ b/tasks/add_new_prebid_partner.py @@ -6,6 +6,7 @@ import sys from builtins import input from pprint import pprint +import sys from colorama import init @@ -72,15 +73,38 @@ def setup_partner(user_email, advertiser_name, order_name, placements, # Create creatives. creative_configs = dfp.create_creatives.create_duplicate_creative_configs( bidder_code, order_name, advertiser_id, num_creatives) + creative_ids = dfp.create_creatives.create_creatives(creative_configs) + # Determine what bidder params should be + bidder_params = getattr(settings, 'PREBID_BIDDER_PARAMS', None) + hb_pb_key = 'hb_pb' + + if bidder_params is True: + hb_pb_key += '_' + bidder_code + hb_adid_key = 'hb_adid_' + bidder_code + hb_size_key = 'hb_size_' + bidder_code + + if len(hb_pb_key) > 20: + hb_pb_key = hb_pb_key[:20] + + if len(hb_adid_key) > 20: + hb_adid_key = hb_adid_key[:20] + + if len(hb_size_key) > 20: + hb_size_key = hb_size_key[:20] + + # Create adid and size keys + get_or_create_dfp_targeting_key(hb_adid_key) + get_or_create_dfp_targeting_key(hb_size_key) + # Get DFP key IDs for line item targeting. hb_bidder_key_id = get_or_create_dfp_targeting_key('hb_bidder') - hb_pb_key_id = get_or_create_dfp_targeting_key('hb_pb') + hb_pb_key_id = get_or_create_dfp_targeting_key(hb_pb_key) # Instantiate DFP targeting value ID getters for the targeting keys. HBBidderValueGetter = DFPValueIdGetter('hb_bidder') - HBPBValueGetter = DFPValueIdGetter('hb_pb') + HBPBValueGetter = DFPValueIdGetter(hb_pb_key) # Create line items. line_items_config = create_line_item_configs(prices, order_id, @@ -314,10 +338,42 @@ def main(): len(placements) ) + # In the case where no inventory is being used, make sure at least one + # creative is created + if not num_creatives > 0: + num_creatives = 1 + bidder_code = getattr(settings, 'PREBID_BIDDER_CODE', None) if bidder_code is None: raise MissingSettingException('PREBID_BIDDER_CODE') + bidder_params = getattr(settings, 'PREBID_BIDDER_PARAMS', None) + hb_pb_key = 'hb_pb' + additional_keys = '' + + if bidder_params is True: + hb_pb_key += '_' + bidder_code + hb_adid_key = 'hb_adid_' + bidder_code + hb_size_key = 'hb_size_' + bidder_code + + if len(hb_pb_key) > 20: + hb_pb_key = hb_pb_key[:20] + + if len(hb_adid_key) > 20: + hb_adid_key = hb_adid_key[:20] + + if len(hb_size_key) > 20: + hb_size_key = hb_size_key[:20] + + additional_keys = u""" + + Additionally, keys {name_start_format}{hb_adid_key}{format_end} and {name_start_format}{hb_size_key}{format_end} will be created.""".format( + hb_adid_key=hb_adid_key, + hb_size_key=hb_size_key, + name_start_format=color.BOLD, + format_end=color.END, + ) + price_buckets = getattr(settings, 'PREBID_PRICE_BUCKETS', None) if price_buckets is None: raise MissingSettingException('PREBID_PRICE_BUCKETS') @@ -337,15 +393,16 @@ def main(): {name_start_format}Owner{format_end}: {value_start_format}{user_email}{format_end} Line items will have targeting: - {name_start_format}hb_pb{format_end} = {value_start_format}{prices_summary}{format_end} + {name_start_format}{hb_pb_key}{format_end} = {value_start_format}{prices_summary}{format_end} {name_start_format}hb_bidder{format_end} = {value_start_format}{bidder_code}{format_end} - {name_start_format}placements{format_end} = {value_start_format}{placements}{format_end} + {name_start_format}placements{format_end} = {value_start_format}{placements}{format_end}{additional_keys} """.format( num_line_items = len(prices), order_name=order_name, advertiser=advertiser_name, user_email=user_email, + hb_pb_key=hb_pb_key, prices_summary=prices_summary, bidder_code=bidder_code, placements=placements, @@ -353,6 +410,7 @@ def main(): name_start_format=color.BOLD, format_end=color.END, value_start_format=color.BLUE, + additional_keys=additional_keys )) ok = input('Is this correct? (y/n)\n') From 6ebd50dd611ac67ba8a447e85e41d94968ca5b8d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 6 Apr 2018 14:17:06 -0400 Subject: [PATCH 05/16] Fixed tests or DFP create creatives. --- tests/test_dfp_create_creatives.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_dfp_create_creatives.py b/tests/test_dfp_create_creatives.py index 0cbb4d4..b79fe87 100644 --- a/tests/test_dfp_create_creatives.py +++ b/tests/test_dfp_create_creatives.py @@ -43,6 +43,19 @@ def test_create_creative_config(self, mock_dfp_client): with open(snippet_file_path, 'r') as snippet_file: snippet = snippet_file.read() + # Determine what bidder params should be + bidder_code = getattr(settings, 'PREBID_BIDDER_CODE', None) + bidder_params = getattr(settings, 'PREBID_BIDDER_PARAMS', None) + hb_adid_key = 'hb_adid' + + if bidder_params is True: + hb_adid_key += '_' + bidder_code + + if len(hb_adid_key) > 20: + hb_adid_key = hb_adid_key[:20] + + snippet = snippet.replace('{hb_adid_key}', hb_adid_key) + self.assertEqual( dfp.create_creatives.create_creative_config( name='My Creative', From eb8e7632f5796de03b92344ff03093d48e562d8d Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Apr 2018 10:49:02 -0400 Subject: [PATCH 06/16] Use effectiveRootAdUnitd from current network instead of filtering all existing ad units. --- dfp/create_line_items.py | 8 ++++---- dfp/get_ad_units.py | 18 +++++++++++------- settings.py | 6 ++++++ tasks/add_new_prebid_partner.py | 6 +++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/dfp/create_line_items.py b/dfp/create_line_items.py index 92513a3..fc09c3b 100755 --- a/dfp/create_line_items.py +++ b/dfp/create_line_items.py @@ -25,7 +25,7 @@ def create_line_items(line_items): def create_line_item_config(name, order_id, placement_ids, cpm_micro_amount, sizes, hb_bidder_key_id, hb_pb_key_id, hb_bidder_value_id, hb_pb_value_id, - currency_code='USD', root_ad_unit=None): + currency_code='USD', root_ad_unit_id=None): """ Creates a line item config object. @@ -83,11 +83,11 @@ def create_line_item_config(name, order_id, placement_ids, cpm_micro_amount, 'targetedPlacementIds': placement_ids } - if not root_ad_unit is None: + if not root_ad_unit_id is None: targeting = { 'targetedAdUnits': [{ - 'adUnitId': root_ad_unit['id'], - 'includeDescendants': True + 'adUnitId': root_ad_unit_id, + 'includeDescendants': 'true' }] } diff --git a/dfp/get_ad_units.py b/dfp/get_ad_units.py index a6456fc..89d936f 100644 --- a/dfp/get_ad_units.py +++ b/dfp/get_ad_units.py @@ -44,17 +44,21 @@ def get_all_ad_units(print_ad_units=False): return ad_units -def get_root_ad_unit(): +def get_root_ad_unit_id(): """ - Gets root ad unit from DFP. + Gets root ad unit ID from DFP. Returns: - an ad unit, or None + an ad unit ID, or None """ - ad_units = get_all_ad_units() - for ad_unit in ad_units: - if not hasattr(ad_unit, 'parentId'): - return ad_unit + + dfp_client = get_client() + network_service = dfp_client.GetService('NetworkService', version='v201802') + current_network = network_service.getCurrentNetwork() + + if hasattr(current_network, 'effectiveRootAdUnitId'): + return current_network.effectiveRootAdUnitId + return None def main(): diff --git a/settings.py b/settings.py index eb68119..2b768fa 100644 --- a/settings.py +++ b/settings.py @@ -41,6 +41,8 @@ # still need to be set to empty arrays. DFP_ALLOW_NO_INVENTORY_TARGETING = False +# + # Whether we should create the advertiser in DFP if it does not exist. # If False, the program will exit rather than create an advertiser. DFP_CREATE_ADVERTISER_IF_DOES_NOT_EXIST = False @@ -72,6 +74,10 @@ PREBID_BIDDER_CODE = None +# Whether DFP targeting keys should be created following Bidders' Params structure +# See: http://prebid.org/dev-docs/bidders.html +PREBID_BIDDER_PARAMS = True + # Price buckets. This should match your Prebid settings for the partner. See: # http://prebid.org/dev-docs/publisher-api-reference.html#module_pbjs.setPriceGranularity # FIXME: this should be an array of buckets. See: diff --git a/tasks/add_new_prebid_partner.py b/tasks/add_new_prebid_partner.py index 8297a24..28d59d4 100755 --- a/tasks/add_new_prebid_partner.py +++ b/tasks/add_new_prebid_partner.py @@ -211,9 +211,9 @@ def create_line_item_configs(prices, order_id, placement_ids, bidder_code, if not placement_ids: # Since the placement ids array is empty, it means we should target a run of network for the line item - root_ad_unit = dfp.get_ad_units.get_root_ad_unit() + root_ad_unit_id = dfp.get_ad_units.get_root_ad_unit_id() - if root_ad_unit is None: + if root_ad_unit_id is None: raise DFPObjectNotFound('Could not find the root ad unit to target a run of network.') for price in prices: @@ -240,7 +240,7 @@ def create_line_item_configs(prices, order_id, placement_ids, bidder_code, hb_bidder_value_id=hb_bidder_value_id, hb_pb_value_id=hb_pb_value_id, currency_code=currency_code, - root_ad_unit=root_ad_unit + root_ad_unit_id=root_ad_unit_id ) line_items_config.append(config) From be9cc2ee74beafff0857b8ae9c9a5a055fdf80f8 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 14:02:39 -0400 Subject: [PATCH 07/16] Completed support for RON. --- dfp/create_line_items.py | 49 +++++++++++++++++++--------------- settings.py | 7 +++-- tests/test_dfp_get_ad_units.py | 32 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 tests/test_dfp_get_ad_units.py diff --git a/dfp/create_line_items.py b/dfp/create_line_items.py index fc09c3b..ca66368 100755 --- a/dfp/create_line_items.py +++ b/dfp/create_line_items.py @@ -3,6 +3,7 @@ from dfp.client import get_client +import settings def create_line_items(line_items): """ @@ -56,13 +57,18 @@ def create_line_item_config(name, order_id, placement_ids, cpm_micro_amount, # https://github.com/googleads/googleads-python-lib/blob/master/examples/dfp/v201802/line_item_service/target_custom_criteria.py # create custom criterias - hb_bidder_criteria = { - 'xsi_type': 'CustomCriteria', - 'keyId': hb_bidder_key_id, - 'valueIds': [hb_bidder_value_id], - 'operator': 'IS' + inventory = { + 'targetedPlacementIds': placement_ids } + if not root_ad_unit_id is None: + inventory = { + 'targetedAdUnits': [{ + 'adUnitId': root_ad_unit_id, + 'includeDescendants': 'true' + }] + } + hb_pb_criteria = { 'xsi_type': 'CustomCriteria', 'keyId': hb_pb_key_id, @@ -70,34 +76,33 @@ def create_line_item_config(name, order_id, placement_ids, cpm_micro_amount, 'operator': 'IS' } - # The custom criteria will resemble: - # (hb_bidder_criteria.key == hb_bidder_criteria.value AND - # hb_pb_criteria.key == hb_pb_criteria.value) + children = [hb_pb_criteria] + + bidder_params = getattr(settings, 'PREBID_BIDDER_PARAMS', None) + + if not bidder_params is True: + hb_bidder_criteria = { + 'xsi_type': 'CustomCriteria', + 'keyId': hb_bidder_key_id, + 'valueIds': [hb_bidder_value_id], + 'operator': 'IS' + } + + children.append(hb_bidder_criteria) + top_set = { 'xsi_type': 'CustomCriteriaSet', 'logicalOperator': 'AND', - 'children': [hb_bidder_criteria, hb_pb_criteria] - } - - targeting = { - 'targetedPlacementIds': placement_ids + 'children': children } - if not root_ad_unit_id is None: - targeting = { - 'targetedAdUnits': [{ - 'adUnitId': root_ad_unit_id, - 'includeDescendants': 'true' - }] - } - # https://developers.google.com/doubleclick-publishers/docs/reference/v201802/LineItemService.LineItem line_item_config = { 'name': name, 'orderId': order_id, # https://developers.google.com/doubleclick-publishers/docs/reference/v201802/LineItemService.Targeting 'targeting': { - 'inventoryTargeting': targeting, + 'inventoryTargeting': inventory, 'customTargeting': top_set, }, 'startDateTimeType': 'IMMEDIATELY', diff --git a/settings.py b/settings.py index 2b768fa..40bfd14 100644 --- a/settings.py +++ b/settings.py @@ -21,9 +21,6 @@ # Names of placements the line items should target. Has priority over ad units. DFP_TARGETED_PLACEMENT_NAMES = [] -# Names of ad units the line items should target. -DFP_TARGETED_ADUNIT_NAMES = [] - # Sizes of placements. These are used to set line item and creative sizes. DFP_PLACEMENT_SIZES = [ { @@ -74,8 +71,10 @@ PREBID_BIDDER_CODE = None -# Whether DFP targeting keys should be created following Bidders' Params structure +# Whether DFP targeting keys should be created following Bidders' Params structure. +# This is used when it's required to sen all bids to the ad server. # See: http://prebid.org/dev-docs/bidders.html +# And: http://prebid.org/adops/send-all-bids-adops.html PREBID_BIDDER_PARAMS = True # Price buckets. This should match your Prebid settings for the partner. See: diff --git a/tests/test_dfp_get_ad_units.py b/tests/test_dfp_get_ad_units.py new file mode 100644 index 0000000..76c36dd --- /dev/null +++ b/tests/test_dfp_get_ad_units.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +from unittest import TestCase +from mock import MagicMock, Mock, patch + +import dfp.get_orders + + +@patch('googleads.dfp.DfpClient.LoadFromStorage') +class DFPServiceTests(TestCase): + + def test_get_all_ad_units(self, mock_dfp_client): + """ + Ensure `get_all_ad_units` makes one call to DFP. + """ + mock_dfp_client.return_value = MagicMock() + + # Response for fetching ad units. + (mock_dfp_client.return_value + .GetService.return_value + .getAdUnitsByStatement) = MagicMock() + + dfp.get_ad_units.get_all_ad_units() + + # Confirm that it loaded the mock DFP client. + mock_dfp_client.assert_called_once() + + expected_arg = {'query': 'LIMIT 500 OFFSET 0', 'values': None} + (mock_dfp_client.return_value + .GetService.return_value + .getAdUnitsByStatement.assert_called_once_with(expected_arg) + ) \ No newline at end of file From 24a71d0b0a97ccde2b37bedb7d65f4de98a33914 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 14:31:52 -0400 Subject: [PATCH 08/16] Fixed tests. --- tasks/add_new_prebid_partner.py | 2 +- tests/test_dfp_create_line_items.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/tasks/add_new_prebid_partner.py b/tasks/add_new_prebid_partner.py index 28d59d4..a45322a 100755 --- a/tasks/add_new_prebid_partner.py +++ b/tasks/add_new_prebid_partner.py @@ -207,7 +207,7 @@ def create_line_item_configs(prices, order_id, placement_ids, bidder_code, hb_bidder_value_id = HBBidderValueGetter.get_value_id(bidder_code) line_items_config = [] - root_ad_unit = None + root_ad_unit_id = None if not placement_ids: # Since the placement ids array is empty, it means we should target a run of network for the line item diff --git a/tests/test_dfp_create_line_items.py b/tests/test_dfp_create_line_items.py index 119f2c2..459cfa7 100644 --- a/tests/test_dfp_create_line_items.py +++ b/tests/test_dfp_create_line_items.py @@ -28,6 +28,7 @@ def test_create_line_items_call(self, mock_dfp_client): hb_pb_key_id=888888, hb_bidder_value_id=222222, hb_pb_value_id=111111, + root_ad_unit_id=None ) ] @@ -57,6 +58,7 @@ def test_create_line_item_config(self, mock_dfp_client): hb_pb_key_id=888888, hb_bidder_value_id=222222, hb_pb_value_id=111111, + root_ad_unit_id=None ), { 'orderId': 1234567, @@ -68,15 +70,15 @@ def test_create_line_item_config(self, mock_dfp_client): 'customTargeting': { 'children': [ { - 'keyId': 999999, + 'keyId': 888888, 'operator': 'IS', - 'valueIds': [222222], + 'valueIds': [111111], 'xsi_type': 'CustomCriteria' }, { - 'keyId': 888888, + 'keyId': 999999, 'operator': 'IS', - 'valueIds': [111111], + 'valueIds': [222222], 'xsi_type': 'CustomCriteria' } ], @@ -120,6 +122,7 @@ def test_create_line_item_config(self, mock_dfp_client): hb_bidder_value_id=222222, hb_pb_value_id=111111, currency_code='EUR', + root_ad_unit_id=None ), { 'orderId': 22334455, @@ -131,15 +134,15 @@ def test_create_line_item_config(self, mock_dfp_client): 'customTargeting': { 'children': [ { - 'keyId': 999999, + 'keyId': 888888, 'operator': 'IS', - 'valueIds': [222222], + 'valueIds': [111111], 'xsi_type': 'CustomCriteria' }, { - 'keyId': 888888, + 'keyId': 999999, 'operator': 'IS', - 'valueIds': [111111], + 'valueIds': [222222], 'xsi_type': 'CustomCriteria' } ], From 70a277d019477324299faaf903edf099933da571 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 14:37:14 -0400 Subject: [PATCH 09/16] Removed unused line from settings.py. --- settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.py b/settings.py index 40bfd14..6d1adfd 100644 --- a/settings.py +++ b/settings.py @@ -38,8 +38,6 @@ # still need to be set to empty arrays. DFP_ALLOW_NO_INVENTORY_TARGETING = False -# - # Whether we should create the advertiser in DFP if it does not exist. # If False, the program will exit rather than create an advertiser. DFP_CREATE_ADVERTISER_IF_DOES_NOT_EXIST = False From 3dcfa0a090ef67e4d25ba0a12afac6e7d9f4b805 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 14:44:38 -0400 Subject: [PATCH 10/16] Fixed typo. --- settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 6d1adfd..a674c66 100644 --- a/settings.py +++ b/settings.py @@ -70,7 +70,7 @@ PREBID_BIDDER_CODE = None # Whether DFP targeting keys should be created following Bidders' Params structure. -# This is used when it's required to sen all bids to the ad server. +# This is used when it's required to send all bids to the ad server. # See: http://prebid.org/dev-docs/bidders.html # And: http://prebid.org/adops/send-all-bids-adops.html PREBID_BIDDER_PARAMS = True From 7fe03f95b8dcd3d5578a7b21ad3e4ecdc26c7d99 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 14:52:52 -0400 Subject: [PATCH 11/16] Changes to readme. Removed reference to ad units from settings. --- README.md | 2 ++ settings.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7863612..5d5e302 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Setting | Description | Default `DFP_USE_EXISTING_ORDER_IF_EXISTS` | Whether we should modify an existing order if one already exists with name `DFP_ORDER_NAME` | `False` `DFP_NUM_CREATIVES_PER_LINE_ITEM` | The number of duplicate creatives to attach to each line item. Due to [DFP limitations](https://support.google.com/dfp_sb/answer/82245?hl=en), this should be equal to or greater than the number of ad units you serve on a given page. | the length of setting `DFP_TARGETED_PLACEMENT_NAMES` `DFP_CURRENCY_CODE` | The currency to use in line items. | `'USD'` +`DFP_ALLOW_NO_INVENTORY_TARGETING` | If no placement should be used, for example for a run of network. If True, DFP_TARGETED_PLACEMENT_NAMES still need to be set to an empty array. | `False` +`PREBID_BIDDER_PARAMS` | Whether DFP targeting keys should be created following Bidders' Params structure. This is used when it's required to send all bids to the ad server. | `False` ## Limitations diff --git a/settings.py b/settings.py index a674c66..5a5fb7d 100644 --- a/settings.py +++ b/settings.py @@ -33,9 +33,8 @@ }, ] -# If no placement or ad unit should be used, for example if the user wants a run -# of network. If True, DFP_TARGETED_PLACEMENT_NAMES and DFP_TARGETED_ADUNIT_NAMES -# still need to be set to empty arrays. +# If no placement should be used, for example for a run of network. If True, +# DFP_TARGETED_PLACEMENT_NAMES still need to be set to an empty array. DFP_ALLOW_NO_INVENTORY_TARGETING = False # Whether we should create the advertiser in DFP if it does not exist. From 2efc445dc9ab65a9b9e0852ebb8be69d2aaeb4e2 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 14:55:14 -0400 Subject: [PATCH 12/16] More changes to readme since #16 has been fixed. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d5e302..9e5543b 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,8 @@ Setting | Description | Default ## Limitations -* Currently, the names of the bidder code targeting key (`hb_bidder`) and price bucket targeting key (`hb_pb`) are not customizable. The `hb_bidder` targeting key is currently required (see [#18](../../issues/18)) -* This tool does not support additional line item targeting beyond placement, `hb_bidder`, and `hb_pb` values. Placement targeting is currently required (see [#16](../../issues/16)), and targeting by ad unit isn't supported (see [#17](../../issues/17)) +* Currently, the names of the bidder code targeting key (`hb_bidder`) is not customizable. The `hb_bidder` targeting key is currently required (see [#18](../../issues/18)) +* This tool does not support additional line item targeting beyond placement, `hb_bidder`, and `hb_pb` values. Targeting by ad unit isn't supported (see [#17](../../issues/17)) * The price bucketing setting `PREBID_PRICE_BUCKETS` only allows for uniform bucketing. For example, you can create $0.01 buckets from $0 - $20, but you cannot specify $0.01 buckets from $0 - $5 and $0.50 buckets from $5 - $20. Using entirely $0.01 buckets will still work for the custom buckets—you'll just have more line items than you need. * This tool does not modify existing orders or line items, it only creates them. If you need to make a change to an order, it's easiest to archive the existing order and recreate it. From 3aac015bc80c59a0f2a1cab9bffcfbb4500dc36a Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Apr 2018 15:00:57 -0400 Subject: [PATCH 13/16] Restored proper default value in order to fix tests. --- settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 5a5fb7d..fe149e5 100644 --- a/settings.py +++ b/settings.py @@ -72,7 +72,7 @@ # This is used when it's required to send all bids to the ad server. # See: http://prebid.org/dev-docs/bidders.html # And: http://prebid.org/adops/send-all-bids-adops.html -PREBID_BIDDER_PARAMS = True +PREBID_BIDDER_PARAMS = False # Price buckets. This should match your Prebid settings for the partner. See: # http://prebid.org/dev-docs/publisher-api-reference.html#module_pbjs.setPriceGranularity From b5f0be840f78008c19b03a8a2bb259e76fb4eb35 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Apr 2018 14:23:42 -0400 Subject: [PATCH 14/16] Added an option to make line items and creatives associations in batch, in order to prevent timeouts from DFP. --- README.md | 1 + dfp/associate_line_items_and_creatives.py | 31 +++++++++++++++++++---- settings.py | 5 ++++ tasks/add_new_prebid_partner.py | 1 + 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e5543b..9440b38 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Setting | Description | Default `DFP_NUM_CREATIVES_PER_LINE_ITEM` | The number of duplicate creatives to attach to each line item. Due to [DFP limitations](https://support.google.com/dfp_sb/answer/82245?hl=en), this should be equal to or greater than the number of ad units you serve on a given page. | the length of setting `DFP_TARGETED_PLACEMENT_NAMES` `DFP_CURRENCY_CODE` | The currency to use in line items. | `'USD'` `DFP_ALLOW_NO_INVENTORY_TARGETING` | If no placement should be used, for example for a run of network. If True, DFP_TARGETED_PLACEMENT_NAMES still need to be set to an empty array. | `False` +`DFP_ASSOCIATIONS_BATCH` | Determine number of line item/creative associations to be created in one batch. | the number of line items to be created multiplied by `DFP_NUM_CREATIVES_PER_LINE_ITEM` `PREBID_BIDDER_PARAMS` | Whether DFP targeting keys should be created following Bidders' Params structure. This is used when it's required to send all bids to the ad server. | `False` ## Limitations diff --git a/dfp/associate_line_items_and_creatives.py b/dfp/associate_line_items_and_creatives.py index 99092cf..0c6ea4e 100644 --- a/dfp/associate_line_items_and_creatives.py +++ b/dfp/associate_line_items_and_creatives.py @@ -4,6 +4,8 @@ from dfp.client import get_client +import settings + logger = logging.getLogger(__name__) @@ -41,10 +43,29 @@ def make_licas(line_item_ids, creative_ids, size_overrides=[]): # settings, as recommended: http://prebid.org/adops/step-by-step.html 'sizes': sizes }) - licas = lica_service.createLineItemCreativeAssociations(licas) - if licas: - logger.info( - u'Created {0} line item <> creative associations.'.format(len(licas))) + associations_batch = getattr(settings, 'DFP_ASSOCIATIONS_BATCH', None) + + if not associations_batch is None: + while licas: + batch = [] + + for b in range(0, associations_batch): + if licas: + batch.append(licas.pop(0)) + + batch = lica_service.createLineItemCreativeAssociations(batch) + + if batch: + logger.info( + u'Created {0} line item <> creative associations.'.format(len(batch))) + else: + logger.info(u'No line item <> creative associations created.') else: - logger.info(u'No line item <> creative associations created.') + licas = lica_service.createLineItemCreativeAssociations(licas) + + if licas: + logger.info( + u'Created {0} line item <> creative associations.'.format(len(licas))) + else: + logger.info(u'No line item <> creative associations created.') diff --git a/settings.py b/settings.py index fe149e5..d4854da 100644 --- a/settings.py +++ b/settings.py @@ -62,6 +62,11 @@ # The currency to use in DFP when setting line item CPMs. Defaults to 'USD'. # DFP_CURRENCY_CODE = 'USD' +# Optional +# Determine if line items and creative should be associated in batch. +# Useful to avoid timeouts if many of them have to be created. +# DFP_ASSOCIATIONS_BATCH = 50 + ######################################################################### # PREBID SETTINGS ######################################################################### diff --git a/tasks/add_new_prebid_partner.py b/tasks/add_new_prebid_partner.py index a45322a..4144942 100755 --- a/tasks/add_new_prebid_partner.py +++ b/tasks/add_new_prebid_partner.py @@ -113,6 +113,7 @@ def setup_partner(user_email, advertiser_name, order_name, placements, logger.info("Creating line items...") line_item_ids = dfp.create_line_items.create_line_items(line_items_config) + logger.info("Associating creatives with line items...") # Associate creatives with line items. dfp.associate_line_items_and_creatives.make_licas(line_item_ids, creative_ids, size_overrides=sizes) From 132e32eff61c934e87533926223bfe8c2418452e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Apr 2018 17:03:17 -0400 Subject: [PATCH 15/16] Manage CommonError.CONCURRENT_MODIFICATION when it occurs, per Google recommendations. --- dfp/associate_line_items_and_creatives.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dfp/associate_line_items_and_creatives.py b/dfp/associate_line_items_and_creatives.py index 0c6ea4e..6d463cd 100644 --- a/dfp/associate_line_items_and_creatives.py +++ b/dfp/associate_line_items_and_creatives.py @@ -5,7 +5,9 @@ from dfp.client import get_client import settings - +import time +import suds +from pprint import pprint logger = logging.getLogger(__name__) @@ -54,7 +56,15 @@ def make_licas(line_item_ids, creative_ids, size_overrides=[]): if licas: batch.append(licas.pop(0)) - batch = lica_service.createLineItemCreativeAssociations(batch) + try: + batch = lica_service.createLineItemCreativeAssociations(batch) + except suds.WebFault as err: + #if err.fault.faultstring is '[CommonError.CONCURRENT_MODIFICATION @ ]': + logger.info(u'A common error was raised (it can happen). Waiting 30 seconds and retrying...') + time.sleep(30) + batch = lica_service.createLineItemCreativeAssociations(batch) + #else: + # raise if batch: logger.info( From 3840021ff36c85c8abb10c54bec488de522f42e3 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 12 Apr 2018 07:16:37 -0400 Subject: [PATCH 16/16] Added exception handling for common errors. --- dfp/associate_line_items_and_creatives.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dfp/associate_line_items_and_creatives.py b/dfp/associate_line_items_and_creatives.py index 6d463cd..d403aae 100644 --- a/dfp/associate_line_items_and_creatives.py +++ b/dfp/associate_line_items_and_creatives.py @@ -57,14 +57,17 @@ def make_licas(line_item_ids, creative_ids, size_overrides=[]): batch.append(licas.pop(0)) try: + time.sleep(1) batch = lica_service.createLineItemCreativeAssociations(batch) except suds.WebFault as err: - #if err.fault.faultstring is '[CommonError.CONCURRENT_MODIFICATION @ ]': logger.info(u'A common error was raised (it can happen). Waiting 30 seconds and retrying...') time.sleep(30) - batch = lica_service.createLineItemCreativeAssociations(batch) - #else: - # raise + try: + batch = lica_service.createLineItemCreativeAssociations(batch) + except suds.WebFault as err: + logger.info(u'A common error was raised (it can happen). Waiting 30 seconds and retrying...') + time.sleep(30) + batch = lica_service.createLineItemCreativeAssociations(batch) if batch: logger.info(