diff --git a/.pylintrc b/.pylintrc index ba02b0f..f7d2ba9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -48,7 +48,7 @@ ignore=CVS # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\' represents the directory delimiter on Windows systems, it # can't be used as an escape character. -ignore-paths=generated,.venv,venv,docs,samples,package_test,integration_tests +ignore-paths=generated,.venv,venv,docs,samples,package_test,integration_tests,pinterest/utils/validations.py # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores @@ -473,7 +473,7 @@ max-returns=6 max-statements=50 # Minimum number of public methods for a class (see R0903). -min-public-methods=2 +min-public-methods=1 [STRING] diff --git a/README.md b/README.md index 5d2b6d9..f702ac1 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,6 @@ campaign.set_lifetime_budget( * Documentation is hosted on [Developer Site](https://developers.pinterest.com/docs/sdk/). - ## Exceptions See `pinterest.utils.sdk_exceptions` for a list of exceptions which may be thrown by the SDK. diff --git a/integration_tests/ads/test_ad_accounts.py b/integration_tests/ads/test_ad_accounts.py index c83f04c..6d1aeff 100644 --- a/integration_tests/ads/test_ad_accounts.py +++ b/integration_tests/ads/test_ad_accounts.py @@ -1,7 +1,9 @@ """ Test Ad Account Model """ - +from datetime import date +from datetime import timedelta +from parameterized import parameterized from unittest.mock import patch from pinterest.ads.ad_accounts import AdAccount @@ -9,7 +11,8 @@ from pinterest.ads.audiences import Audience from integration_tests.base_test import BaseTestCase -from integration_tests.config import OWNER_USER_ID, DEFAULT_AD_ACCOUNT_ID +from integration_tests.config import OWNER_USER_ID +from integration_tests.config import DEFAULT_AD_ACCOUNT_ID class TestAdAccount(BaseTestCase): @@ -136,3 +139,59 @@ def test_list_customer_list_success(self): get_all_customer_list_ids.add(getattr(customer_list, "_id")) assert new_customer_list_ids == get_all_customer_list_ids + + +class TestGetAnalytics(BaseTestCase): + """ + Test getting Ad accounts analytics + """ + DAYS_BACK = 2 + FURTHEST_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + MAX_RANGE_DAYS = 3 # Max time range in days for granularity HOUR + FURTHEST_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_hour", "HOUR"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_ad_analytics_success(self, name, granularity): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","PIN_PROMOTION_ID","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + ad_account = AdAccount( + ad_account_id=self.ad_account_utils.get_default_ad_account_id(), + client=self.test_client + ) + ad_account_analytics = ad_account.get_analytics(**analytics_info_dict) + self.assertIsNotNone(ad_account_analytics) + self.assertIsNotNone(ad_account_analytics.raw_response) + + +class TestGetTargetingAnalytics(BaseTestCase): + + def test_get_ad_targeting_analytics_success(self): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(2), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["ADVERTISER_ID","PIN_PROMOTION_ID","SPEND_IN_DOLLAR"], + 'granularity': 'DAY', + } + ad_account = AdAccount( + ad_account_id=self.ad_account_utils.get_default_ad_account_id(), + client=self.test_client + ) + ad_analytics = ad_account.get_targeting_analytics(**analytics_info_dict) + self.assertIsNotNone(ad_analytics) + self.assertIsNotNone(ad_analytics.raw_response) + self.assertIsNotNone(ad_analytics.raw_response.get('data')) diff --git a/integration_tests/ads/test_ad_groups.py b/integration_tests/ads/test_ad_groups.py index d147e26..1c96df2 100644 --- a/integration_tests/ads/test_ad_groups.py +++ b/integration_tests/ads/test_ad_groups.py @@ -1,9 +1,15 @@ ''' Test AdGroup Model ''' +from datetime import date +from datetime import timedelta +from parameterized import parameterized + +from openapi_generated.pinterest_client.exceptions import ApiException +from openapi_generated.pinterest_client.model.targeting_spec import TargetingSpec from integration_tests.base_test import BaseTestCase -from integration_tests.config import DEFAULT_AD_ACCOUNT_ID +from integration_tests.config import DEFAULT_AD_ACCOUNT_ID, DEFAULT_AD_GROUP_ID from pinterest.ads.ad_groups import AdGroup @@ -63,9 +69,7 @@ def test_update_success(self): ) new_name = "SDK_AD_GROUP_NEW_NAME" - new_spec = { - "GENDER": ["male"] - } + new_spec = TargetingSpec(gender=["male"]) ad_group.update_fields( name=new_name, @@ -221,3 +225,138 @@ def test_disable_auto_targeting(self): ad_group_id=getattr(ad_group_0, "_id") ) self.assertFalse(getattr(ad_group_1, "_auto_targeting_enabled")) + + +class TestGetAnalytics(BaseTestCase): + """ + Test getting Ad Group analytics + """ + DAYS_BACK = 2 + FURTHEST_DAYS_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + FURTHEST_DAYS_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_hour", "HOUR"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_ad_group_analytics_success(self, name, granularity): + + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","TOTAL_ENGAGEMENT","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + ad_group = AdGroup( + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + ad_group_id=DEFAULT_AD_GROUP_ID, + client=self.test_client, + ) + + ad_group_analytics = ad_group.get_analytics(**analytics_info_dict) + self.assertIsNotNone(ad_group_analytics) + self.assertIsNotNone(ad_group_analytics.raw_response) + analytics_response = ad_group_analytics.raw_response.get('value') + for dict_item in analytics_response: + for column in analytics_info_dict.get('columns'): + self.assertIn(column, dict_item) + if granularity != 'TOTAL': + self.assertIn('DATE', dict_item) + + @parameterized.expand( + [ + ("granularity_total","TOTAL", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_day", "DAY", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_hour", "HOUR", FURTHEST_DAYS_BACK_HOUR + 1), + ("granularity_week", "WEEK", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_month", "MONTH", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ] + ) + def test_get_analytics_fail(self, name, granularity, days_back): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(days_back), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","PIN_PROMOTION_ID","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + ad_group = AdGroup( + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + ad_group_id=DEFAULT_AD_GROUP_ID, + client=self.test_client, + ) + with self.assertRaises(ApiException): + ad_group.get_analytics(**analytics_info_dict) + +class TestGetTargetingAnalytics(BaseTestCase): + """ + Test getting targeting analytics for Ad Group + """ + DAYS_BACK = 2 + FURTHEST_DAYS_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + FURTHEST_DAYS_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_ad_group_targeting_analytics_success(self, name, granularity): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["SPEND_IN_MICRO_DOLLAR","SPEND_IN_DOLLAR", "TOTAL_ENGAGEMENT"], + 'granularity': granularity, + } + ad_group = AdGroup( + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + ad_group_id=DEFAULT_AD_GROUP_ID, + client=self.test_client, + ) + ad_group_analytics = ad_group.get_targeting_analytics(**analytics_info_dict) + self.assertIsNotNone(ad_group_analytics) + self.assertIsNotNone(ad_group_analytics.raw_response) + analytics_response = ad_group_analytics.raw_response.get('data') + for dict_item in analytics_response: + self.assertIsNotNone(dict_item.get('metrics')) + for column in analytics_info_dict.get('columns'): + self.assertIn(column, dict_item.get('metrics')) + if granularity != 'TOTAL': + self.assertIn('DATE', dict_item.get('metrics')) + + @parameterized.expand( + [ + ("granularity_total","TOTAL", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_day", "DAY", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_hour", "HOUR", FURTHEST_DAYS_BACK_HOUR + 1), + ("granularity_week", "WEEK", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_month", "MONTH", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ] + ) + def test_get_ad_group_targeting_analytics_success(self, name, granularity, days_back): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(days_back), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["SPEND_IN_MICRO_DOLLAR","SPEND_IN_DOLLAR", "TOTAL_ENGAGEMENT"], + 'granularity': granularity, + } + ad_group = AdGroup( + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + ad_group_id=DEFAULT_AD_GROUP_ID, + client=self.test_client, + ) + with self.assertRaises(ApiException): + ad_group.get_targeting_analytics(**analytics_info_dict) diff --git a/integration_tests/ads/test_ads.py b/integration_tests/ads/test_ads.py index 0032c29..8fe10e7 100644 --- a/integration_tests/ads/test_ads.py +++ b/integration_tests/ads/test_ads.py @@ -1,15 +1,23 @@ """ Test Ad Model """ +from datetime import date +from datetime import timedelta +from unittest.mock import DEFAULT +from parameterized import parameterized from pinterest.ads.ads import Ad +from openapi_generated.pinterest_client.exceptions import ApiException from openapi_generated.pinterest_client.exceptions import ApiValueError from openapi_generated.pinterest_client.exceptions import NotFoundException from openapi_generated.pinterest_client.model.entity_status import EntityStatus from integration_tests.base_test import BaseTestCase -from integration_tests.config import DEFAULT_PIN_ID, DEFAULT_AD_ACCOUNT_ID +from integration_tests.config import DEFAULT_PIN_ID +from integration_tests.config import DEFAULT_AD_ACCOUNT_ID +from integration_tests.config import DEFAULT_AD_ID +from integration_tests.config import DEFAULT_AD_GROUP_ID class TestCreateAd(BaseTestCase): @@ -23,16 +31,15 @@ def test_create_ad_success(self): """ ad = Ad.create( ad_account_id=DEFAULT_AD_ACCOUNT_ID, - ad_group_id=self.ad_group_utils.get_ad_group_id(), - creative_type="REGULAR", + ad_group_id=DEFAULT_AD_GROUP_ID, + creative_type="IDEA", pin_id=DEFAULT_PIN_ID, name="Test_create_ad", - status="ACTIVE", + status="PAUSED", is_pin_deleted=False, is_removable=False, client=self.test_client, ) - assert ad assert getattr(ad, "_id") assert getattr(ad, "_name") == "Test_create_ad" @@ -43,7 +50,7 @@ def test_create_ad_failure_without_creative_type(self): """ ad_arguments = dict( ad_account_id=DEFAULT_AD_ACCOUNT_ID, - ad_group_id=self.ad_group_utils.get_ad_group_id(), + ad_group_id=DEFAULT_AD_GROUP_ID, pin_id=DEFAULT_PIN_ID, name="Test_create_ad", status="ACTIVE", @@ -59,7 +66,7 @@ def test_create_ad_failure_with_incorrect_creative_type(self): """ ad_arguments = dict( ad_account_id=DEFAULT_AD_ACCOUNT_ID, - ad_group_id=self.ad_group_utils.get_ad_group_id(), + ad_group_id=DEFAULT_AD_GROUP_ID, creative_type="NOT", pin_id=DEFAULT_PIN_ID, name="Test_create_ad", @@ -80,12 +87,12 @@ def test_get_ad_success(self): """ ad = Ad( ad_account_id=DEFAULT_AD_ACCOUNT_ID, - ad_id=self.ad_utils.get_ad_id(), + ad_id=DEFAULT_AD_ID, client=self.test_client, ) assert ad - assert getattr(ad, "_id") == self.ad_utils.get_ad_id() + assert getattr(ad, "_id") == DEFAULT_AD_ID def test_get_ad_fail_with_invalid_id(self): """ @@ -145,7 +152,7 @@ def test_update_ad_success(self): """ ad = Ad( ad_account_id=DEFAULT_AD_ACCOUNT_ID, - ad_id=self.ad_utils.get_ad_id(), + ad_id=DEFAULT_AD_ID, client=self.test_client, ) @@ -160,3 +167,138 @@ def test_update_ad_success(self): assert ad assert getattr(ad, "_name") == new_name assert getattr(ad, "_status") == EntityStatus(new_status) + +class TestGetAnalytics(BaseTestCase): + """ + Test getting Ad analytics + """ + DAYS_BACK = 2 + FURTHEST_DAYS_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + FURTHEST_DAYS_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_hour", "HOUR"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_ad_analytics_success(self, name, granularity): + + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","PIN_PROMOTION_ID","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + ad = Ad( + ad_account_id=analytics_info_dict.pop('ad_account_id'), + ad_id=DEFAULT_AD_ID, + client=self.test_client, + ) + + ad_analytics = ad.get_analytics(**analytics_info_dict) + self.assertIsNotNone(ad_analytics) + self.assertIsNotNone(ad_analytics.raw_response) + analytics_response = ad_analytics.raw_response.get('value') + for dict_item in analytics_response: + for column in analytics_info_dict.get('columns'): + self.assertIn(column, dict_item) + if granularity != 'TOTAL': + self.assertIn('DATE', dict_item) + + @parameterized.expand( + [ + ("granularity_total","TOTAL", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_day", "DAY", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_hour", "HOUR", FURTHEST_DAYS_BACK_HOUR + 1), + ("granularity_week", "WEEK", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_month", "MONTH", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ] + ) + def test_get_analytics_fail(self, name, granularity, days_back): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(days_back), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","PIN_PROMOTION_ID","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + ad = Ad( + ad_account_id=analytics_info_dict.pop('ad_account_id'), + ad_id=DEFAULT_AD_ID, + client=self.test_client, + ) + with self.assertRaises(ApiException): + ad.get_analytics(**analytics_info_dict) + + +class TestGetTargetingAnalytics(BaseTestCase): + """ + Test getting targeting analytics + """ + DAYS_BACK = 2 + FURTHEST_DAYS_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + FURTHEST_DAYS_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_ad_targeting_analytics_success(self, name, granularity): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["SPEND_IN_MICRO_DOLLAR","SPEND_IN_DOLLAR", "TOTAL_ENGAGEMENT"], + 'granularity': granularity, + } + ad = Ad( + ad_account_id=analytics_info_dict.pop('ad_account_id'), + ad_id=DEFAULT_AD_ID, + client=self.test_client, + ) + ad_analytics = ad.get_targeting_analytics(**analytics_info_dict) + self.assertIsNotNone(ad_analytics) + self.assertIsNotNone(ad_analytics.raw_response) + analytics_response = ad_analytics.raw_response.get('data') + for dict_item in analytics_response: + self.assertIsNotNone(dict_item.get('metrics')) + for column in analytics_info_dict.get('columns'): + self.assertIn(column, dict_item.get('metrics')) + if granularity != 'TOTAL': + self.assertIn('DATE', dict_item.get('metrics')) + + @parameterized.expand( + [ + ("granularity_total","TOTAL", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_day", "DAY", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_hour", "HOUR", FURTHEST_DAYS_BACK_HOUR + 1), + ("granularity_week", "WEEK", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_month", "MONTH", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ] + ) + def test_get_ad_targeting_analytics_fail(self, name, granularity, days_back): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(days_back), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["SPEND_IN_MICRO_DOLLAR","SPEND_IN_DOLLAR", "TOTAL_ENGAGEMENT"], + 'granularity': granularity, + } + ad = Ad( + ad_account_id=analytics_info_dict.pop('ad_account_id'), + ad_id=DEFAULT_AD_ID, + client=self.test_client, + ) + with self.assertRaises(ApiException): + ad.get_targeting_analytics(**analytics_info_dict) diff --git a/integration_tests/ads/test_campaigns.py b/integration_tests/ads/test_campaigns.py index f56cb01..e76d8cf 100644 --- a/integration_tests/ads/test_campaigns.py +++ b/integration_tests/ads/test_campaigns.py @@ -1,10 +1,13 @@ """ Test Campaign Model """ - +from datetime import date +from datetime import timedelta +from parameterized import parameterized from unittest.mock import patch from openapi_generated.pinterest_client.model.objective_type import ObjectiveType +from openapi_generated.pinterest_client.exceptions import ApiException from openapi_generated.pinterest_client.exceptions import ApiValueError from openapi_generated.pinterest_client.exceptions import NotFoundException @@ -15,6 +18,7 @@ from integration_tests.base_test import BaseTestCase from integration_tests.config import DEFAULT_AD_ACCOUNT_ID +from integration_tests.config import DEFAULT_CAMPAIGN_ID class TestCreateCampaign(BaseTestCase): @@ -233,3 +237,138 @@ def test_get_next_page_of_campaigns(self): assert isinstance(campaign, Campaign) assert created_campaign_ids == get_all_campaigns_ids + + +class TestGetAnalytics(BaseTestCase): + """ + Test getting Campaign analytics + """ + DAYS_BACK = 2 + FURTHEST_DAYS_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + FURTHEST_DAYS_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_hour", "HOUR"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_campaign_analytics_success(self, name, granularity): + + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","TOTAL_ENGAGEMENT","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + campaign = Campaign( + client=self.test_client, + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + campaign_id=DEFAULT_CAMPAIGN_ID, + ) + + campaign_analytics = campaign.get_analytics(**analytics_info_dict) + self.assertIsNotNone(campaign_analytics) + self.assertIsNotNone(campaign_analytics.raw_response) + analytics_response = campaign_analytics.raw_response.get('value') + for dict_item in analytics_response: + for column in analytics_info_dict.get('columns'): + self.assertIn(column, dict_item) + if granularity != 'TOTAL': + self.assertIn('DATE', dict_item) + + @parameterized.expand( + [ + ("granularity_total","TOTAL", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_day", "DAY", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_hour", "HOUR", FURTHEST_DAYS_BACK_HOUR + 1), + ("granularity_week", "WEEK", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_month", "MONTH", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ] + ) + def test_get_campaign_analytics_fail(self, name, granularity, days_back): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(days_back), + 'end_date': date.today(), + 'columns': ["ADVERTISER_ID","PIN_PROMOTION_ID","SPEND_IN_DOLLAR"], + 'granularity': granularity, + } + campaign = Campaign( + client=self.test_client, + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + campaign_id=DEFAULT_CAMPAIGN_ID, + ) + with self.assertRaises(ApiException): + campaign.get_analytics(**analytics_info_dict) + +class TestGetTargetingAnalytics(BaseTestCase): + """ + Test getting targeting analytics for Campaigns + """ + DAYS_BACK = 2 + FURTHEST_DAYS_BACK_HOUR = 7 # Futhest allowed days back for granularity HOUR + FURTHEST_DAYS_BACK_NOT_HOUR = 90 # Futhest allowed days back for any granularity but HOUR + + @parameterized.expand( + [ + ("granularity_total","TOTAL"), + ("granularity_day", "DAY"), + ("granularity_week", "WEEK"), + ("granularity_month", "MONTH"), + ] + ) + def test_get_campaign_targeting_analytics_success(self, name, granularity): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["SPEND_IN_MICRO_DOLLAR","SPEND_IN_DOLLAR", "TOTAL_ENGAGEMENT"], + 'granularity': granularity, + } + campaign = Campaign( + client=self.test_client, + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + campaign_id=DEFAULT_CAMPAIGN_ID, + ) + campaign_analytics = campaign.get_targeting_analytics(**analytics_info_dict) + self.assertIsNotNone(campaign_analytics) + self.assertIsNotNone(campaign_analytics.raw_response) + analytics_response = campaign_analytics.raw_response.get('data') + for dict_item in analytics_response: + self.assertIsNotNone(dict_item.get('metrics')) + for column in analytics_info_dict.get('columns'): + self.assertIn(column, dict_item.get('metrics')) + if granularity != 'TOTAL': + self.assertIn('DATE', dict_item.get('metrics')) + + @parameterized.expand( + [ + ("granularity_total","TOTAL", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_day", "DAY", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_hour", "HOUR", FURTHEST_DAYS_BACK_HOUR + 1), + ("granularity_week", "WEEK", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ("granularity_month", "MONTH", FURTHEST_DAYS_BACK_NOT_HOUR + 1), + ] + ) + def test_get_campaign_targeting_analytics_fail(self, name, granularity, days_back): + analytics_info_dict = { + 'ad_account_id': DEFAULT_AD_ACCOUNT_ID, + 'start_date': date.today() - timedelta(days_back), + 'end_date': date.today(), + 'targeting_types':["GENDER"], + 'columns': ["SPEND_IN_MICRO_DOLLAR","SPEND_IN_DOLLAR", "TOTAL_ENGAGEMENT"], + 'granularity': granularity, + } + campaign = Campaign( + client=self.test_client, + ad_account_id=DEFAULT_AD_ACCOUNT_ID, + campaign_id=DEFAULT_CAMPAIGN_ID, + ) + with self.assertRaises(ApiException): + campaign.get_targeting_analytics(**analytics_info_dict) diff --git a/integration_tests/ads/test_conversion_events.py b/integration_tests/ads/test_conversion_events.py index b3b9fba..0e203b1 100644 --- a/integration_tests/ads/test_conversion_events.py +++ b/integration_tests/ads/test_conversion_events.py @@ -8,6 +8,8 @@ from pinterest.client import PinterestSDKClient from pinterest.ads.conversion_events import Conversion +from openapi_generated.pinterest_client.exceptions import ApiException + class TestSendConversionEvent(BaseTestCase): """ Test send Conversion Event @@ -79,17 +81,12 @@ def test_send_conversion_fail(self): for _ in range(NUMBER_OF_CONVERSION_EVENTS) ] - response = Conversion.send_conversion_events( - client = client, - ad_account_id = DEFAULT_AD_ACCOUNT_ID, - conversion_events = conversion_events, - test = True, - ) + with self.assertRaises(ApiException): + Conversion.send_conversion_events( + client = client, + ad_account_id = DEFAULT_AD_ACCOUNT_ID, + conversion_events = conversion_events, + test = True, + ) - assert response - assert response.num_events_received == 2 - assert response.num_events_processed == 0 - assert len(response.events) == 2 - assert 'hashed format' in response.events[0].error_message - assert 'hashed format' in response.events[0].error_message diff --git a/integration_tests/clean_organic_data.py b/integration_tests/clean_organic_data.py index 92339c0..a73dfcc 100644 --- a/integration_tests/clean_organic_data.py +++ b/integration_tests/clean_organic_data.py @@ -9,10 +9,5 @@ def test_delete_organic_data(): """ Delete organic boards from default client """ - all_boards, _ = Board.get_all() - for board in all_boards: - if board.id == DEFAULT_BOARD_ID: - continue - Board.delete(board_id=board.id) + pass - assert len(Board.get_all()[0]) == 1 diff --git a/integration_tests/config.py b/integration_tests/config.py index 2aceb4c..51bc751 100644 --- a/integration_tests/config.py +++ b/integration_tests/config.py @@ -9,3 +9,6 @@ DEFAULT_BOARD_SECTION_ID = os.environ.get('DEFAULT_BOARD_SECTION_ID', "") OWNER_USER_ID = os.environ.get('OWNER_USER_ID', "") DEFAULT_AD_ACCOUNT_ID = os.environ.get('DEFAULT_AD_ACCOUNT_ID', "") +DEFAULT_AD_ID = os.environ.get('DEFAULT_AD_ID', "") +DEFAULT_AD_GROUP_ID = os.environ.get('DEFAULT_AD_GROUP_ID', "") +DEFAULT_CAMPAIGN_ID = os.environ.get('DEFAULT_CAMPAIGN_ID', "") diff --git a/integration_tests/organic/__init__.py b/integration_tests/organic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration_tests/organic/test_pins.py b/integration_tests/organic/test_pins.py index 7ce8379..e6364a4 100644 --- a/integration_tests/organic/test_pins.py +++ b/integration_tests/organic/test_pins.py @@ -3,9 +3,12 @@ *NOTE*: Do not forget to delete pin after the test. """ +from datetime import date +from datetime import timedelta from parameterized import parameterized from openapi_generated.pinterest_client.exceptions import NotFoundException +from openapi_generated.pinterest_client.exceptions import ApiException from pinterest.organic.pins import Pin @@ -102,6 +105,7 @@ def test_save_pin_success(self, pin_save_kwargs): """ pin = self.pin_utils.create_new_pin(title="Test Saving Pin") assert pin + assert pin.title == "Test Saving Pin" pin.save(**pin_save_kwargs) @@ -117,3 +121,51 @@ def test_save_pin_success(self, pin_save_kwargs): pin_id=pin.id, client=self.test_client ) + +class TestGetAnalytics(BaseTestCase): + """ + Test getting a Pin analytics + """ + DAYS_BACK = 4 + EXPECTED_NUM_OF_DAILY_METRICS = DAYS_BACK + 1 + DAYS_BACK_OUT_OF_RANGE = 91 + METRIC_TYPES = "IMPRESSION,OUTBOUND_CLICK,PIN_CLICK,SAVE,SAVE_RATE,TOTAL_COMMENTS,TOTAL_REACTIONS" + + def validate_raw_response(self, raw_response): + self.assertIsNotNone(raw_response) + self.assertIsNotNone(raw_response.get('all')) + self.assertIsNotNone(raw_response.get('all').get('daily_metrics')) + self.assertIsNotNone(raw_response.get('all').get('lifetime_metrics')) + self.assertIsNotNone(raw_response.get('all').get('summary_metrics')) + self.assertEqual(len(raw_response.get('all').get('daily_metrics')), self.EXPECTED_NUM_OF_DAILY_METRICS) + for metric in self.METRIC_TYPES.split(','): + self.assertIn(metric, {**raw_response.get('all').get('summary_metrics'),**raw_response.get('all').get('lifetime_metrics')}) + + + def test_get_analytics_success(self): + """ + Test request the Pin analitycs + """ + analytics_info_dict = { + 'pin_id': DEFAULT_PIN_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK), + 'end_date': date.today(), + 'metric_types': ["IMPRESSION,OUTBOUND_CLICK,PIN_CLICK,SAVE,SAVE_RATE,TOTAL_COMMENTS,TOTAL_REACTIONS"], + } + pin = Pin(pin_id=analytics_info_dict.pop('pin_id')) + analytics = pin.get_analytics(**analytics_info_dict) + self.assertIsNotNone(analytics) + self.validate_raw_response(analytics.raw_response) + + def test_get_analytics_failure(self): + """Test request the Pin analytics from before 90 days ago. + """ + analytics_info_dict = { + 'pin_id': DEFAULT_PIN_ID, + 'start_date': date.today() - timedelta(self.DAYS_BACK_OUT_OF_RANGE), + 'end_date': date.today(), + 'metric_types': ["IMPRESSION,OUTBOUND_CLICK,PIN_CLICK,SAVE,SAVE_RATE,TOTAL_COMMENTS,TOTAL_REACTIONS"], + } + pin = Pin(pin_id=analytics_info_dict.pop('pin_id')) + with self.assertRaises(ApiException): + pin.get_analytics(**analytics_info_dict) \ No newline at end of file diff --git a/integration_tests/utils/ads_utils.py b/integration_tests/utils/ads_utils.py index 5aaa22d..c57de14 100644 --- a/integration_tests/utils/ads_utils.py +++ b/integration_tests/utils/ads_utils.py @@ -13,7 +13,7 @@ from pinterest.ads.conversion_tags import ConversionTag -from integration_tests.config import DEFAULT_PIN_ID, OWNER_USER_ID, DEFAULT_AD_ACCOUNT_ID +from integration_tests.config import DEFAULT_AD_GROUP_ID, DEFAULT_PIN_ID, OWNER_USER_ID, DEFAULT_AD_ACCOUNT_ID def _merge_default_params_with_params(default_params, params): @@ -212,7 +212,7 @@ def __init__(self, client=None): self.ad = Ad.create( ad_account_id=DEFAULT_AD_ACCOUNT_ID, ad_group_id=getattr(self.ad_group, "_id"), - creative_type="REGULAR", + creative_type="IDEA", pin_id=DEFAULT_PIN_ID, name="Test_create_ad", status="ACTIVE", @@ -232,7 +232,7 @@ def get_default_params(self): return dict( ad_account_id=DEFAULT_AD_ACCOUNT_ID, ad_group_id=getattr(self.ad_group, "_id"), - creative_type="REGULAR", + creative_type="IDEA", pin_id=DEFAULT_PIN_ID, name="Test_create_ad", status="ACTIVE", diff --git a/integration_tests/utils/organic_utils.py b/integration_tests/utils/organic_utils.py index b1ef35b..d11bcdb 100644 --- a/integration_tests/utils/organic_utils.py +++ b/integration_tests/utils/organic_utils.py @@ -79,4 +79,5 @@ def create_new_pin(self, **kwargs): return Pin.create(**_merge_default_params_with_params(self.get_default_params(), kwargs)) def delete_pin(self, pin_id): - return Pin.delete(pin_id=pin_id, client=self.test_client) + if pin_id != DEFAULT_PIN_ID: # Make sure default pin is not being deleted + return Pin.delete(pin_id=pin_id, client=self.test_client) diff --git a/pinterest/ads/ad_accounts.py b/pinterest/ads/ad_accounts.py index 4c18707..d367e16 100644 --- a/pinterest/ads/ad_accounts.py +++ b/pinterest/ads/ad_accounts.py @@ -2,11 +2,13 @@ AdAccount Class for Pinterest Python SDK """ from __future__ import annotations +from datetime import date from openapi_generated.pinterest_client.model.country import Country from openapi_generated.pinterest_client.model.ad_account_owner import AdAccountOwner from openapi_generated.pinterest_client.model.currency import Currency - +from openapi_generated.pinterest_client.model.ads_analytics_targeting_type import AdsAnalyticsTargetingType +from openapi_generated.pinterest_client.model.conversion_report_attribution_type import ConversionReportAttributionType from openapi_generated.pinterest_client.api.ad_accounts_api import AdAccountsApi from openapi_generated.pinterest_client.model.ad_account import AdAccount as GeneratedAdAccount from openapi_generated.pinterest_client.model.ad_account_create_request import AdAccountCreateRequest @@ -17,6 +19,7 @@ from pinterest.ads.customer_lists import CustomerList from pinterest.utils.base_model import PinterestBaseModel from pinterest.utils.bookmark import Bookmark +from pinterest.utils.analytics import AnalyticsResponse, AnalyticsUtils class AdAccount(PinterestBaseModel): @@ -32,7 +35,6 @@ def __init__( ) -> None: """ Initialize an object of an AdAccount. - Args: ad_account_id (str): Unique identifier of an ad account. client (PinterestSDKClient, optional): PinterestSDKClient Object. Defaults to `default_api_client`. @@ -116,16 +118,13 @@ def create( For more, see \ Create an advertiser account. - Args: name (str): Ad Account name owner_user_id (str): Ad Account's owning user ID country (str): Country ID from ISO 3166-1 alpha-2. Example: "US" or "RU". client (PinterestSDKClient): PinterestSDKClient Object - Keyword Args: Any valid keyword arguments or query parameters for endpoint. - Returns: AdAccount: AdAccount Object """ @@ -160,7 +159,6 @@ def list_campaigns( roles granted to them via\ \ Business Access: Admin, Analyst, Campaign Manager. - Args: campaign_ids (list[str], optional): List of Campaign Ids to use to filter the results. Defaults to None. entity_statuses (list[str], optional): Possible Entity Statuses "ACTIVE", "PAUSED" or "ARCHIVED". Defaults @@ -172,10 +170,8 @@ def list_campaigns( Note that higher-value IDs are associated with more-recently added items. Defaults to "ASCENDING". bookmark (str, optional): Cursor used to fetch the next page of items. Defaults to None. - Keyword Args: Any valid keyword arguments or query parameters for endpoint. - Returns: list[Campaign]: List of Campaign Objects Bookmark: Bookmark for pagination if present, else None. @@ -202,7 +198,6 @@ def list_audiences( # pylint: disable=too-many-arguments """ Get a list of the audiences in the AdAccount, filtered by the specified arguments - Args: entity_statuses (list[str], optional): Possible Entity Statuses "ACTIVE", "PAUSED" or "ARCHIVED". Defaults to None. @@ -213,7 +208,6 @@ def list_audiences( Note that higher-value IDs are associated with more-recently added items. Defaults to "ASCENDING". bookmark (str, optional): Cursor used to fetch the next page of items. Defaults to None. - Returns: list[Audience]: List of Audience Objects Bookmark: Bookmark for pagination if present, else None. @@ -238,7 +232,6 @@ def list_customer_lists( # pylint: disable=too-many-arguments """ Get a list of customer lists in the AdAccount, filtered by the specified arguments - Args: page_size (int[1..100], optional): Maximum number of items to include in a single page of the response. See documentation on Pagination for more information. Defaults to None @@ -247,7 +240,6 @@ def list_customer_lists( Note that higher-value IDs are associated with more-recently added items. Defaults to "ASCENDING". bookmark (str, optional): Cursor used to fetch the next page of items. Defaults to None. - Returns: list[CustomerList]: List of Audience Objects Bookmark: Bookmark for pagination if present, else None. @@ -260,3 +252,150 @@ def list_customer_lists( client=self._client, **kwargs ) + + def get_analytics( + self, + start_date:date, + end_date:date, + columns:list[str], + granularity:str, + click_window_days:int=30, + engagement_window_days:int=30, + view_window_days:int=1, + conversion_report_time:str="TIME_OF_AD_ACTION", + **kwargs + ) -> AnalyticsResponse: + """ + Get analytics for the specified ad_account_id, filtered by the specified options. + - The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + Args: + start_date (date): Metric report start date (UTC). + end_date (date): Metric report end date (UTC). + columns (list[str]): Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as + MICRO_DOLLARS returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, + or $0.000001 - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of + the advertiser'scurrency. For example, if the advertiser's currency is GBP (British pound sterling), all + MICRO_DOLLARS fields will be in GBP microunits (1/1,000,000 British pound). If a column has no value, + it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Enum: 1 7 30 60. Number of days to use as the conversion attribution + window for a pin click action. Applies to Pinterest Tag conversion metrics. Prior conversion tags use + their defined attribution windows.. Defaults to 30. + engagement_window_days (int, optional): Enum: 1 7 30 60 Number of days to use as the conversion attribution + window for an engagement action. Engagements include saves, closeups, link clicks, and carousel card + swipes. Applies to Pinterest Tag conversion metrics. Prior conversion tags use their defined attribution + windows. Defaults to 30. + view_window_days (int, optional): Enum: 1 7 30 60. Number of days to use as the conversion attribution + window for a view action. Applies to Pinterest Tag conversion metrics. Prior conversion tags use their + defined attribution windows. Defaults to 1. + conversion_report_time (str, optional): Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION". The date by which + the conversion metrics returned from this endpoint will be reported. There are two dates associated + with a conversion event: the date that the user interacted with the ad, and the date that the user + completed a conversion event. Defaults to "TIME_OF_AD_ACTION". + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.id + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + + ad_account_analytics_response = AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=AdAccountsApi, + analytics_fn=AdAccountsApi.ad_account_analytics, + entity=AdAccount, + client=self._client + ) + + return ad_account_analytics_response + + def get_targeting_analytics( + self, + start_date:date, + end_date:date, + targeting_types:list[str], + columns:list[str], + granularity:str, + click_window_days:int=30, + engagement_window_days:int=30, + view_window_days:int=1, + conversion_report_time:str = "TIME_OF_AD_ACTION", + attribution_types:str = None, + **kwargs + ) -> AnalyticsResponse: + """ + Get analytics for the specified ad_account_id, filtered by the specified options. + - The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + Args: + start_date (date): Metric report start date (UTC). + end_date (date): Metric report end date (UTC). + targeting_types (list[str]): Items Enum: "KEYWORD" "APPTYPE" "GENDER" "LOCATION" "PLACEMENT" "COUNTRY" + "TARGETED_INTEREST" "PINNER_INTEREST" "AUDIENCE_INCLUDE" "AUDIENCE_EXCLUDE" "GEO" "AGE_BUCKET" "REGION" + Targeting type breakdowns for the report. The reporting per targeting type + is independent from each other. + columns (list[str]): Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as + MICRO_DOLLARS returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, + or $0.000001 - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of + the advertiser'scurrency. For example, if the advertiser's currency is GBP (British pound sterling), all + MICRO_DOLLARS fields will be in GBP microunits (1/1,000,000 British pound). If a column has no value, + it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Enum: 1 7 30 60. Number of days to use as the conversion attribution + window for a pin click action. Applies to Pinterest Tag conversion metrics. Prior conversion tags use + their defined attribution windows.. Defaults to 30. + engagement_window_days (int, optional): Enum: 1 7 30 60 Number of days to use as the conversion attribution + window for an engagement action. Engagements include saves, closeups, link clicks, and carousel card + swipes. Applies to Pinterest Tag conversion metrics. Prior conversion tags use their defined attribution + windows. Defaults to 30. + view_window_days (int, optional): Enum: 1 7 30 60. Number of days to use as the conversion attribution + window for a view action. Applies to Pinterest Tag conversion metrics. Prior conversion tags use their + defined attribution windows. Defaults to 1. + conversion_report_time (str, optional): Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION". The date by which + the conversion metrics returned from this endpoint will be reported. There are two dates associated + with a conversion event: the date that the user interacted with the ad, and the date that the user + completed a conversion event. Defaults to "TIME_OF_AD_ACTION". + attribution_types (str): Enum: "INDIVIDUAL" "HOUSEHOLD" + List of types of attribution for the conversion report + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.id + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['targeting_types'] = [AdsAnalyticsTargetingType(targeting_type) for targeting_type in targeting_types] + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + if attribution_types: + kwargs['attribution_types'] = ConversionReportAttributionType(attribution_types) + + ad_account_analytics_response = AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=AdAccountsApi, + analytics_fn=AdAccountsApi.ad_account_targeting_analytics_get, + entity=AdAccount, + client=self._client + ) + + return ad_account_analytics_response diff --git a/pinterest/ads/ad_groups.py b/pinterest/ads/ad_groups.py index 1df2599..d70e70f 100644 --- a/pinterest/ads/ad_groups.py +++ b/pinterest/ads/ad_groups.py @@ -3,8 +3,11 @@ """ from __future__ import annotations -from openapi_generated.pinterest_client.api.ad_groups_api import AdGroupsApi +from datetime import date +from openapi_generated.pinterest_client.model.conversion_report_attribution_type import ConversionReportAttributionType +from openapi_generated.pinterest_client.model.ads_analytics_targeting_type import AdsAnalyticsTargetingType +from openapi_generated.pinterest_client.api.ad_groups_api import AdGroupsApi from openapi_generated.pinterest_client.model.action_type import ActionType from openapi_generated.pinterest_client.model.budget_type import BudgetType from openapi_generated.pinterest_client.model.ad_group_response import AdGroupResponse @@ -15,6 +18,7 @@ from pinterest.utils.base_model import PinterestBaseModel from pinterest.ads.ads import Ad from pinterest.utils.bookmark import Bookmark +from pinterest.utils.analytics import AnalyticsResponse, AnalyticsUtils class AdGroup(PinterestBaseModel): @@ -484,3 +488,173 @@ def disable_auto_targeting(self): bool: true if ad group disable auto_targeting_enabled """ return self.update_fields(auto_targeting_enabled=False) + + def get_targeting_analytics( + self, + start_date: date, + end_date: date, + targeting_types: list[str], + columns: list[str], + granularity: str, + click_window_days: int = 30, + engagement_window_days: int = 30, + view_window_days: int = 1, + conversion_report_time: str = "TIME_OF_AD_ACTION", + attribution_types: str = None, + **kwargs + ) -> AnalyticsResponse: + """ + Get targeting analytics for one or more ad groups. For the requested ad group(s) and metrics, the response will + include the requested metric information (e.g. SPEND_IN_DOLLAR) for the requested target type + (e.g. "age_bucket") for applicable values (e.g. "45-49"). + + - The token's user_account must either be the Owner of the specified ad account, or have one of the + necessary roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + + + Args: + start_date (date): Metric report start date (UTC). Format: YYYY-MM-DD + end_date (date): Metric report end date (UTC). Format: YYYY-MM-DD + targeting_types (list[str]): Example: targeting_types=APPTYPE + Targeting type breakdowns for the report. The reporting per targeting type is independent from + each other. + columns (list[str]): Example: columns=SPEND_IN_DOLLAR + Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as MICRO_DOLLARS + returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, or $0.000001 - + one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of the advertiser's + currency. + + For example, if the advertiser's currency is GBP (British pound sterling), all MICRO_DOLLARS fields will + be in GBP microunits (1/1,000,000 British pound). + + If a column has no value, it may not be returned + + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Example: click_window_days=1 + Number of days to use as the conversion attribution window for a pin click action. Applies to Pinterest + Tag conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 30 days. + engagement_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for an engagement action. Engagements include + saves, closeups, link clicks, and carousel card swipes. Applies to Pinterest Tag conversion metrics. + Prior conversion tags use their defined attribution windows. If not specified, defaults to 30 days. + view_window_days (int, optional): Default: 1 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for a view action. Applies to Pinterest Tag + conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 1 day. + conversion_report_time (str, optional): Default: "TIME_OF_AD_ACTION" + Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION" + Example: conversion_report_time=TIME_OF_AD_ACTION + The date by which the conversion metrics returned from this endpoint will be reported. There are two + dates associated with a conversion event: the date that the user interacted with the ad, and the date + that the user completed a conversion event. + attribution_types (str): Enum: "INDIVIDUAL" "HOUSEHOLD" + Example: attribution_types=INDIVIDUAL + List of types of attribution for the conversion report + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.ad_account_id + kwargs['ad_group_ids'] = [self.id] + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['targeting_types'] = [AdsAnalyticsTargetingType(targeting_type) for targeting_type in targeting_types] + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + + if attribution_types: + kwargs['attribution_types'] = ConversionReportAttributionType(attribution_types) + + return AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=AdGroupsApi, + analytics_fn=AdGroupsApi.ad_groups_targeting_analytics_get, + entity=AdGroup, + client=self._client + ) + + def get_analytics( + self, + start_date: date, + end_date: date, + columns: list[str], + granularity: str, + click_window_days: int = 30, + engagement_window_days: int = 30, + view_window_days: int = 1, + conversion_report_time: str = "TIME_OF_AD_ACTION", + **kwargs + ) -> AnalyticsResponse: + """ + Get analytics for the specified ad groups in the specified ad_account_id, filtered by the specified options. + - The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + Args: + start_date (date): Metric report start date (UTC). Format: YYYY-MM-DD + end_date (date): Metric report end date (UTC). Format: YYYY-MM-DD + columns (list[str]): Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as + MICRO_DOLLARS returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, + or $0.000001 - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of + the advertiser's currency.For example, if the advertiser's currency is GBP (British pound sterling), + all MICRO_DOLLARS fields will be in GBP microunits (1/1,000,000 British pound). If a column has no + value, it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for an engagement action. Engagements include + saves, closeups, link clicks, and carousel card swipes. Applies to Pinterest Tag conversion metrics. + Prior conversion tags use their defined attribution windows. If not specified, defaults to 30 days. + engagement_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for an engagement action. Engagements include + saves, closeups, link clicks, and carousel card swipes. Applies to Pinterest Tag conversion metrics. + Prior conversion tags use their defined attribution windows. If not specified, defaults to 30 days. + view_window_days (int, optional): Default: 1 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for a view action. Applies to Pinterest Tag + conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 1 day. + conversion_report_time (str, optional): Default: "TIME_OF_AD_ACTION" + Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION" + Example: conversion_report_time=TIME_OF_AD_ACTION + The date by which the conversion metrics returned from this endpoint will be reported. There are two + dates associated with a conversion event: the date that the user interacted with the ad, and the date + that the user completed a conversion event. + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_group_ids'] = [self.id] + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + + return AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=AdGroupsApi, + analytics_fn=AdGroupsApi.ad_groups_analytics, + entity=AdGroup, + client=self._client + ) diff --git a/pinterest/ads/ads.py b/pinterest/ads/ads.py index 1d98774..197a94b 100644 --- a/pinterest/ads/ads.py +++ b/pinterest/ads/ads.py @@ -3,8 +3,11 @@ """ from __future__ import annotations -from openapi_generated.pinterest_client.api.ads_api import AdsApi +from datetime import date +from openapi_generated.pinterest_client.model.ads_analytics_targeting_type import AdsAnalyticsTargetingType +from openapi_generated.pinterest_client.model.conversion_report_attribution_type import ConversionReportAttributionType +from openapi_generated.pinterest_client.api.ads_api import AdsApi from openapi_generated.pinterest_client.model.ad_response import AdResponse from openapi_generated.pinterest_client.model.ad_create_request import AdCreateRequest from openapi_generated.pinterest_client.model.creative_type import CreativeType @@ -14,6 +17,7 @@ from pinterest.client import PinterestSDKClient from pinterest.utils.base_model import PinterestBaseModel from pinterest.utils.bookmark import Bookmark +from pinterest.utils.analytics import AnalyticsResponse, AnalyticsUtils class Ad(PinterestBaseModel): @@ -418,3 +422,175 @@ def update_fields(self, **kwargs) -> bool: update_fn=AdsApi.ads_update, **kwargs ) + + def get_analytics( + self, + start_date: date, + end_date: date, + columns: list[str], + granularity: str, + click_window_days: int = 30, + engagement_window_days: int = 30, + view_window_days: int = 1, + conversion_report_time: str = "TIME_OF_AD_ACTION", + **kwargs + ) -> AnalyticsResponse: + """ + Get analytics for the specified ads in the specified ad_account_id, filtered by the specified options. + + - The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + + + Args: + start_date (date): Metric report start date (UTC). Format: YYYY-MM-DD. + end_date (date): Metric report end date (UTC). Format: YYYY-MM-DD. + columns (list[str]): Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as + MICRO_DOLLARS returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, + or $0.000001 - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of + the advertiser's currency.For example, if the advertiser's currency is GBP (British pound sterling), + all MICRO_DOLLARS fields will be in GBP microunits (1/1,000,000 British pound).If a column has no value, + it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): + Default: 30 + Enum: 1 7 30 60 + Example: click_window_days=1 + Number of days to use as the conversion attribution window for a pin click action. Applies to Pinterest + Tag conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 30 days. + engagement_window_days (int, optional): + Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for an engagement action. Engagements include + saves, closeups, link clicks, and carousel card swipes. Applies to Pinterest Tag conversion metrics. + Prior conversion tags use their defined attribution windows. If not specified, defaults to 30 days. + view_window_days (int, optional): + Default: 1 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for a view action. Applies to Pinterest Tag + conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 1 day. + conversion_report_time (str, optional): + Default: "TIME_OF_AD_ACTION" + Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION" + Example: conversion_report_time=TIME_OF_AD_ACTION + The date by which the conversion metrics returned from this endpoint will be reported. There are two + dates associated with a conversion event: the date that the user interacted with the ad, and the date + that the user completed a conversion event. + + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.ad_account_id + kwargs['ad_ids'] = [self.id] + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + + return AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=AdsApi, + analytics_fn=AdsApi.ads_analytics, + entity=Ad, + client=self._client + ) + + def get_targeting_analytics( + self, + start_date: date, + end_date: date, + targeting_types: list[str], + columns: list[str], + granularity: str, + click_window_days: int = 30, + engagement_window_days: int = 30, + view_window_days: int = 1, + conversion_report_time: str = "TIME_OF_AD_ACTION", + attribution_types: str = None, + **kwargs + ) -> AnalyticsResponse: + """ + Get targeting analytics for one or more ads. For the requested ad(s) and metrics, the response will include the + requested metric information (e.g. SPEND_IN_DOLLAR) for the requested target type (e.g. "age_bucket") for + applicable values (e.g. "45-49"). + The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + Args: + start_date (date): Metric report start date (UTC). Format: YYYY-MM-DD + end_date (date): Metric report end date (UTC). Format: YYYY-MM-DD + targeting_types (list[str]): Targeting type breakdowns for the report. The reporting per targeting type + is independent from each other. + columns (list[str]): Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as + MICRO_DOLLARS returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, + or $0.000001 - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of + the advertiser's currency. For example, if the advertiser's currency is GBP (British pound sterling), + all MICRO_DOLLARS fields will be in GBP microunits (1/1,000,000 British pound). If a column has no + value, it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Example: click_window_days=1 + Number of days to use as the conversion attribution window for a pin click action. Applies to Pinterest + Tag conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 30 days. + engagement_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for an engagement action. Engagements + include saves, closeups, link clicks, and carousel card swipes. Applies to Pinterest Tag conversion + metrics. Prior conversion tags use their defined attribution windows. If not specified, defaults to + 30 days. + view_window_days (int, optional): Default: 1 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for a view action. Applies to Pinterest Tag + conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 1 day. + conversion_report_time (str, optional): Default: "TIME_OF_AD_ACTION" + Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION" + Example: conversion_report_time=TIME_OF_AD_ACTION + The date by which the conversion metrics returned from this endpoint will be reported. There are two + dates associated with a conversion event: the date that the user interacted with the ad, and the date + that the user completed a conversion event. + attribution_types (str): Enum: "INDIVIDUAL" "HOUSEHOLD" + Example: attribution_types=INDIVIDUAL + List of types of attribution for the conversion report + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.ad_account_id + kwargs['ad_ids'] = [self.id] + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['targeting_types'] = [AdsAnalyticsTargetingType(targeting_type) for targeting_type in targeting_types] + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + + if attribution_types: + kwargs['attribution_types'] = ConversionReportAttributionType(attribution_types) + + return AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=AdsApi, + analytics_fn=AdsApi.ad_targeting_analytics_get, + entity=Ad, + client=self._client + ) diff --git a/pinterest/ads/campaigns.py b/pinterest/ads/campaigns.py index a67c25f..285682f 100644 --- a/pinterest/ads/campaigns.py +++ b/pinterest/ads/campaigns.py @@ -3,12 +3,14 @@ """ from __future__ import annotations -from openapi_generated.pinterest_client.api.campaigns_api import CampaignsApi +from datetime import date +from openapi_generated.pinterest_client.model.conversion_report_attribution_type import ConversionReportAttributionType +from openapi_generated.pinterest_client.model.ads_analytics_targeting_type import AdsAnalyticsTargetingType +from openapi_generated.pinterest_client.api.campaigns_api import CampaignsApi from openapi_generated.pinterest_client.model.campaign_response import CampaignResponse from openapi_generated.pinterest_client.model.campaign_create_request import CampaignCreateRequest from openapi_generated.pinterest_client.model.campaign_update_request import CampaignUpdateRequest - from openapi_generated.pinterest_client.model.objective_type import ObjectiveType from pinterest.ads.ad_groups import AdGroup @@ -16,6 +18,7 @@ from pinterest.utils.error_handling import verify_api_response from pinterest.utils.base_model import PinterestBaseModel from pinterest.utils.bookmark import Bookmark +from pinterest.utils.analytics import AnalyticsUtils, AnalyticsResponse class Campaign(PinterestBaseModel): @@ -539,3 +542,180 @@ def list_ad_groups( order=order, bookmark=bookmark, **kwargs ) + + def get_analytics( + self, + start_date: date, + end_date: date, + columns: list[str], + granularity: str, + click_window_days: int = 30, + engagement_window_days: int = 30, + view_window_days: int = 1, + conversion_report_time: str = "TIME_OF_AD_ACTION", + **kwargs + ) -> AnalyticsResponse: + """ + Get analytics for the specified campaigns in the specified ad_account_id, filtered by the specified options. + + - The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + + - If granularity is not HOUR, the furthest back you can are allowed to pull data is 914 days before the current + date in UTC time and the max time range supported is 186 days. + + - If granularity is HOUR, the furthest back you can are allowed to pull data is 8 days before the current date + in UTC time and the max time range supported is 3 days. + + Args: + start_date (date): Metric report start date (UTC). Format: YYYY-MM-DD + end_date (date): Metric report end date (UTC). Format: YYYY-MM-DD + columns (list[str]): Example: columns=SPEND_IN_DOLLAR + Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as MICRO_DOLLARS + returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, or $0.000001 + - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of the + advertiser's currency. + + For example, if the advertiser's currency is GBP (British pound sterling), all MICRO_DOLLARS fields + will be in GBP microunits (1/1,000,000 British pound). + + If a column has no value, it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for a pin click action. Applies to Pinterest + Tag conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 30 days. + engagement_window_days (int, optional): Default: 30 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for an engagement action. Engagements include + saves, closeups, link clicks, and carousel card swipes. Applies to Pinterest Tag conversion metrics. + Prior conversion tags use their defined attribution windows. If not specified, defaults to 30 days. + view_window_days (int, optional): Default: 1 + Enum: 1 7 30 60 + Number of days to use as the conversion attribution window for a view action. Applies to Pinterest Tag + conversion metrics. Prior conversion tags use their defined attribution windows. If not specified, + defaults to 1 day. + conversion_report_time (str, optional): Default: "TIME_OF_AD_ACTION" + Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION" + Example: conversion_report_time=TIME_OF_AD_ACTION + The date by which the conversion metrics returned from this endpoint will be reported. There are two + dates associated with a conversion event: the date that the user interacted with the ad, and the date + that the user completed a conversion event. + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.ad_account_id + kwargs['campaign_ids'] = [self.id] + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + + return AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=CampaignsApi, + analytics_fn=CampaignsApi.campaigns_analytics, + entity=Campaign, + client=self._client + ) + + def get_targeting_analytics( + self, + start_date: date, + end_date: date, + targeting_types: list[str], + columns: list[str], + granularity: str, + click_window_days: int=30, + engagement_window_days: int=30, + view_window_days: int=1, + conversion_report_time: str = "TIME_OF_AD_ACTION", + attribution_types: str = None, + **kwargs + ) -> AnalyticsResponse: + """ + Get targeting analytics for one or more campaigns. For the requested account and metrics, the response will + include the requested metric information (e.g. SPEND_IN_DOLLAR) for the requested target type + (e.g. "age_bucket") for applicable values (e.g. "45-49"). + + - The token's user_account must either be the Owner of the specified ad account, or have one of the necessary + roles granted to them via Business Access: Admin, Analyst, Campaign Manager. + + - If granularity is not HOUR, the furthest back you can are allowed to pull data is 914 days before the current + date in UTC time and the max time range supported is 186 days. + + - If granularity is HOUR, the furthest back you can are allowed to pull data is 8 days before the current date + in UTC time and the max time range supported is 3 days. + + Args: + start_date (date): Metric report start date (UTC). + end_date (date): Metric report end date (UTC). + targeting_types (list[str]): Items Enum: "KEYWORD" "APPTYPE" "GENDER" "LOCATION" "PLACEMENT" "COUNTRY" + "TARGETED_INTEREST" "PINNER_INTEREST" "AUDIENCE_INCLUDE" "AUDIENCE_EXCLUDE" "GEO" "AGE_BUCKET" "REGION" + Targeting type breakdowns for the report. The reporting per targeting type + is independent from each other. + columns (list[str]): Columns to retrieve, encoded as a comma-separated string. NOTE: Any metrics defined as + MICRO_DOLLARS returns a value based on the advertiser profile's currency field. For USD,($1/1,000,000, + or $0.000001 - one one-ten-thousandth of a cent). it's microdollars. Otherwise, it's in microunits of + the advertiser'scurrency. For example, if the advertiser's currency is GBP (British pound sterling), all + MICRO_DOLLARS fields will be in GBP microunits (1/1,000,000 British pound). If a column has no value, + it may not be returned + granularity (str): Enum: "TOTAL" "DAY" "HOUR" "WEEK" "MONTH" + TOTAL - metrics are aggregated over the specified date range. + DAY - metrics are broken down daily. + HOUR - metrics are broken down hourly. + WEEKLY - metrics are broken down weekly. + MONTHLY - metrics are broken down monthly + click_window_days (int, optional): Enum: 1 7 30 60. Number of days to use as the conversion attribution + window for a pin click action. Applies to Pinterest Tag conversion metrics. Prior conversion tags use + their defined attribution windows.. Defaults to 30. + engagement_window_days (int, optional): Enum: 1 7 30 60 Number of days to use as the conversion attribution + window for an engagement action. Engagements include saves, closeups, link clicks, and carousel card + swipes. Applies to Pinterest Tag conversion metrics. Prior conversion tags use their defined attribution + windows. Defaults to 30. + view_window_days (int, optional): Enum: 1 7 30 60. Number of days to use as the conversion attribution + window for a view action. Applies to Pinterest Tag conversion metrics. Prior conversion tags use their + defined attribution windows. Defaults to 1. + conversion_report_time (str, optional): Enum: "TIME_OF_AD_ACTION" "TIME_OF_CONVERSION". The date by which + the conversion metrics returned from this endpoint will be reported. There are two dates associated + with a conversion event: the date that the user interacted with the ad, and the date that the user + completed a conversion event. Defaults to "TIME_OF_AD_ACTION". + attribution_types (str): Enum: "INDIVIDUAL" "HOUSEHOLD" + List of types of attribution for the conversion report + + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['ad_account_id'] = self.ad_account_id + kwargs['campaign_ids'] = [self.id] + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['targeting_types'] = [AdsAnalyticsTargetingType(targeting_type) for targeting_type in targeting_types] + kwargs['columns'] = columns + kwargs['granularity'] = granularity + kwargs['click_window_days'] = click_window_days + kwargs['engagement_window_days'] = engagement_window_days + kwargs['view_window_days'] = view_window_days + kwargs['conversion_report_time'] = conversion_report_time + if attribution_types: + kwargs['attribution_types'] = ConversionReportAttributionType(attribution_types) + + ad_account_analytics_response = AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=CampaignsApi, + analytics_fn=CampaignsApi.campaign_targeting_analytics_get, + entity=Campaign, + client=self._client + ) + + return ad_account_analytics_response diff --git a/pinterest/organic/boards.py b/pinterest/organic/boards.py index 0b57eee..7821eb0 100644 --- a/pinterest/organic/boards.py +++ b/pinterest/organic/boards.py @@ -205,7 +205,14 @@ def __init__( client (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided. """ self._id = None + self._created_at = None + self._board_pins_modified_at = None self._name = None + self._collaborator_count = None + self._pin_count = None + self._follower_count = None + self._media = None + self._owner = None self._description = None self._owner = None self._privacy = None @@ -227,11 +234,41 @@ def id(self) -> str: # pylint: disable=missing-function-docstring return self._id + @property + def created_at(self) -> str: + # pylint: disable=missing-function-docstring + return self._created_at + + @property + def board_pins_modified_at(self) -> str: + # pylint: disable=missing-function-docstring + return self._board_pins_modified_at + @property def name(self) -> str: # pylint: disable=missing-function-docstring return self._name + @property + def collaborator_count(self) -> int: + # pylint: disable=missing-function-docstring + return self._collaborator_count + + @property + def pin_count(self) -> int: + # pylint: disable=missing-function-docstring + return self._pin_count + + @property + def follower_count(self) -> int: + # pylint: disable=missing-function-docstring + return self._follower_count + + @property + def media(self): + # pylint: disable=missing-function-docstring + return self._media + @property def description(self) -> str: # pylint: disable=missing-function-docstring diff --git a/pinterest/organic/pins.py b/pinterest/organic/pins.py index 5e7829c..53c11c1 100644 --- a/pinterest/organic/pins.py +++ b/pinterest/organic/pins.py @@ -3,6 +3,8 @@ """ from __future__ import annotations +from datetime import date + from openapi_generated.pinterest_client.api.pins_api import PinsApi from openapi_generated.pinterest_client.model.pin import Pin as GeneratedPin from openapi_generated.pinterest_client.model.pin_create import PinCreate as GeneratedPinCreate @@ -11,6 +13,8 @@ from pinterest.client import PinterestSDKClient from pinterest.utils.base_model import PinterestBaseModel from pinterest.utils.error_handling import verify_api_response +from pinterest.utils.analytics import AnalyticsResponse, AnalyticsUtils + class Pin(PinterestBaseModel): """ @@ -41,8 +45,12 @@ def __init__( client (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided. """ self._ad_account_id = None - self._id = None + self._is_owner = None + self._is_standard = None + self._note = None + self._has_been_promoted = None + self._creative_type = None self._created_at = None self._link = None self._title = None @@ -75,6 +83,31 @@ def id(self) -> str: # pylint: disable=missing-function-docstring return self._id + @property + def is_owner(self) -> str: + # pylint: disable=missing-function-docstring + return self._is_owner + + @property + def is_standard(self) -> str: + # pylint: disable=missing-function-docstring + return self._is_standard + + @property + def note(self) -> str: + # pylint: disable=missing-function-docstring + return self._note + + @property + def has_been_promoted(self) -> str: + # pylint: disable=missing-function-docstring + return self._has_been_promoted + + @property + def creative_type(self) -> str: + # pylint: disable=missing-function-docstring + return self._creative_type + @property def created_at(self) -> str: # pylint: disable=missing-function-docstring @@ -276,3 +309,62 @@ def save( verify_api_response(api_response) self._populate_fields(_model_data=api_response.to_dict()) + + def get_analytics( + self, + start_date: date, + end_date: date, + app_types: str = "ALL", + metric_types: list[str] = None, + split_field: str = None, + **kwargs + ) -> AnalyticsResponse: + """ + Get analytics for a Pin owned by the "operation user_account" - or on a group board that has been shared with + this account. + + - By default, the "operation user_account" is the token user_account. + + Optional: Business Access: Specify an ad_account_id (obtained via List ad accounts) to use the owner of that + ad_account as the "operation user_account". In order to do this, the token user_account must have one of the + following Business Access roles on the ad_account: + + - For Pins on public or protected boards: Admin, Analyst. + - For Pins on secret boards: Admin. + + Args: + start_date (date): Metric report start date (UTC). Format: YYYY-MM-DD + end_date (date): Metric report end date (UTC). Format: YYYY-MM-DD + app_types (str): Default: "ALL" + Enum: "ALL" "MOBILE" "TABLET" "WEB" + Apps or devices to get data for, default is all. + metric_types (list[str], str): + - Standard Pin metric types: "OUTBOUND_CLICK" "PIN_CLICK" "IMPRESSION" "SAVE" "SAVE_RATE" + - Video Pin metric types :"OUTBOUND_CLICK" "IMPRESSION" "SAVE" "QUARTILE_95_PERCENT_VIEW" + "VIDEO_10S_VIEW" "VIDEO_AVG_WATCH_TIME" "VIDEO_MRC_VIEW" "VIDEO_START" "VIDEO_V50_WATCH_TIME" + Pin metric types to get data for, default is all. + split_field (str): Default: "NO_SPLIT" + Enum: "NO_SPLIT" "APP_TYPE" + How to split the data into groups. Not including this param means data won't be split. + Returns: + AnalyticsResponse: AnalyticsResponse object. + """ + kwargs['pin_id'] = self._id + if self._ad_account_id: + kwargs['ad_account_id'] = self._ad_account_id + if app_types: + kwargs['app_types'] = app_types + if split_field: + kwargs['split_field'] = split_field + + kwargs['start_date'] = start_date + kwargs['end_date'] = end_date + kwargs['metric_types'] = metric_types + + return AnalyticsUtils.get_entity_analytics( + params=kwargs, + api=PinsApi, + analytics_fn=PinsApi.pins_analytics, + entity=Pin, + client=self._client + ) diff --git a/pinterest/utils/analytics.py b/pinterest/utils/analytics.py new file mode 100644 index 0000000..eb79342 --- /dev/null +++ b/pinterest/utils/analytics.py @@ -0,0 +1,82 @@ +""" +Analytics Class for Pinterest Python SDK +""" +from __future__ import annotations +from typing import Callable + +from pinterest.utils.base_model import PinterestBaseModel + +from pinterest.client import PinterestSDKClient + + +class AnalyticsUtils: + """ + Utility class with functions to make model specific analytics api calls. + """ + @classmethod + def get_entity_analytics( + cls, + params: list, + api: type, + analytics_fn: Callable, + entity: PinterestBaseModel, + client: PinterestSDKClient = None, + **kwargs + ) -> AnalyticsResponse: + """ + Helper function used to get ad entity analytics. + Args: + params (list): List of params + api (type): + analytics_fn (Callable): + entity (PinterestBaseModel): + client (PinterestSDKClient, optional): + Returns: + AnalyticsResponse: + """ + + return AnalyticsResponse( + entity_type=entity, + fields=params.get('columns', []), + raw_response=getattr(api(client), analytics_fn.__name__)(**params, **kwargs) + ) + + +class AnalyticsResponse(): + """ + AnalyticsResponse model + """ + def __init__( + self, + entity_type:PinterestBaseModel, + fields:list[str], + raw_response:dict, + ) -> None: + """ + Initialize an Ads Analytics object. + Args: + entity_type (PinterestBaseModel): Entity Type identifier. Enum: ad_account, campaign, ad_group, ad. + fields (list[str]): _description_ + raw_response (dict): _description_ + """ + self._entity_type = entity_type.__name__.lower() + self._fields = fields + self._raw_response = raw_response + + @property + def entity_type(self) -> str: + # pylint: disable=missing-function-docstring + return self._entity_type + + @property + def fields(self) -> list[str]: + # pylint: disable=missing-function-docstring + return self._fields + + @property + def raw_response(self) -> dict: + # pylint: disable=missing-function-docstring + return self._raw_response + + def __str__(self) -> str: + return f"{self.raw_response}" diff --git a/pinterest/utils/validations.py b/pinterest/utils/validations.py new file mode 100644 index 0000000..d35315c --- /dev/null +++ b/pinterest/utils/validations.py @@ -0,0 +1,7 @@ +from enum import Enum + +class AdsEntityType(Enum): + AD_ACCOUNT = "ad_account" + CAMPAIGN = "campaign" + ADGROUP = "ad_group" + AD = "ad" diff --git a/requirements.txt b/requirements.txt index 101fee2..b998c63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Pinterest-Generated-Client==0.1.7 +Pinterest-Generated-Client==0.1.8 python-dateutil==2.8.2 six==1.16.0 urllib3==1.26.12 diff --git a/setup.py b/setup.py index edadc0c..1fc0703 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,10 @@ def _get_prod_version(): "python-dateutil", "python-dotenv==0.20.0", "six==1.16.0", - "Pinterest-Generated-Client==0.1.7" + "Pinterest-Generated-Client==0.1.8" ] -long_description = (Path(__file__).parent / "README.md").read_text() +long_description = (Path(__file__).parent / "README.md").read_text(encoding='UTF-8') package_root = os.path.abspath(os.path.dirname(__file__)) __version__ = None diff --git a/tests/src/pinterest/ads/test_ad_groups.py b/tests/src/pinterest/ads/test_ad_groups.py index af61403..8e9935f 100644 --- a/tests/src/pinterest/ads/test_ad_groups.py +++ b/tests/src/pinterest/ads/test_ad_groups.py @@ -8,6 +8,7 @@ from openapi_generated.pinterest_client.model.ad_group_response import AdGroupResponse from openapi_generated.pinterest_client.model.ad_group_array_response import AdGroupArrayResponse from openapi_generated.pinterest_client.model.ad_group_array_response_element import AdGroupArrayResponseElement +from openapi_generated.pinterest_client.model.targeting_spec import TargetingSpec from pinterest.ads.ad_groups import AdGroup from pinterest.ads.ads import Ad @@ -101,9 +102,7 @@ def test_update_ad_group(self, get_mock, update_mock): """ update_mock.__name__ = "ad_groups_update" new_name = "SDK_AD_GROUP_NEW_NAME" - new_spec = { - "GENDER": ["male"] - } + new_spec = TargetingSpec(gender=["male"]) get_mock.return_value = AdGroupResponse( id=self.test_ad_group_id,