diff --git a/examples/analytics.py b/examples/analytics.py index b517fc6..6f1d976 100644 --- a/examples/analytics.py +++ b/examples/analytics.py @@ -13,6 +13,7 @@ from twitter_ads.client import Client from twitter_ads.campaign import LineItem from twitter_ads.enum import METRIC_GROUP +from twitter_ads.utils import split_list CONSUMER_KEY = 'your consumer key' CONSUMER_SECRET = 'your consumer secret' @@ -42,20 +43,29 @@ print('Error: A minimum of 1 items must be provided for entity_ids') sys.exit() -LineItem.all_stats(account, ids, metric_groups) +sync_data = [] +# Sync/Async endpoint can handle max 20 entity IDs per request +# so split the ids list into multiple requests +for chunk_ids in split_list(ids, 20): + sync_data.append(LineItem.all_stats(account, chunk_ids, metric_groups)) -# fetching async stats on the instance -queued_job = LineItem.queue_async_stats_job(account, ids, metric_groups) +print(sync_data) -# get the job_id: -job_id = queued_job['id'] +# create async stats jobs and get job ids +queued_job_ids = [] +for chunk_ids in split_list(ids, 20): + queued_job_ids.append(LineItem.queue_async_stats_job(account, chunk_ids, metric_groups).id) + +print(queued_job_ids) # let the job complete -seconds = 15 +seconds = 30 time.sleep(seconds) -async_stats_job_result = LineItem.async_stats_job_result(account, [job_id]).first +async_stats_job_results = LineItem.async_stats_job_result(account, queued_job_ids) -async_data = LineItem.async_stats_job_data(account, async_stats_job_result.url) +async_data = [] +for result in async_stats_job_results: + async_data.append(LineItem.async_stats_job_data(account, result.url)) print(async_data) diff --git a/examples/draft_tweet.py b/examples/draft_tweet.py index 1e0934e..0c15e22 100644 --- a/examples/draft_tweet.py +++ b/examples/draft_tweet.py @@ -1,6 +1,7 @@ from twitter_ads.client import Client from twitter_ads.campaign import Tweet from twitter_ads.creative import DraftTweet +from twitter_ads.restapi import UserIdLookup CONSUMER_KEY = 'your consumer key' @@ -15,6 +16,9 @@ # load the advertiser account instance account = client.accounts(ACCOUNT_ID) +# get user_id for as_user_id parameter +user_id = UserIdLookup.load(account, screen_name='your_twitter_handle_name').id + # fetch draft tweets from a given account tweets = DraftTweet.all(account) for tweet in tweets: @@ -24,6 +28,7 @@ # create a new draft tweet draft_tweet = DraftTweet(account) draft_tweet.text = 'draft tweet - new' +draft_tweet.as_user_id = user_id draft_tweet = draft_tweet.save() print(draft_tweet.id_str) print(draft_tweet.text) @@ -41,7 +46,7 @@ print(draft_tweet.text) # create a nullcasted tweet using draft tweet metadata -tweet = Tweet.create(account, text=draft_tweet.text) +tweet = Tweet.create(account, text=draft_tweet.text, as_user_id=user_id) print(tweet) # delete draft tweet diff --git a/examples/promoted_tweet.py b/examples/promoted_tweet.py index b40d103..adfbe17 100644 --- a/examples/promoted_tweet.py +++ b/examples/promoted_tweet.py @@ -3,6 +3,7 @@ from twitter_ads.client import Client from twitter_ads.campaign import Tweet from twitter_ads.creative import PromotedTweet, WebsiteCard +from twitter_ads.restapi import UserIdLookup CONSUMER_KEY = 'your consumer key' CONSUMER_SECRET = 'your consumer secret' @@ -15,15 +16,23 @@ # load up the account instance, campaign and line item account = client.accounts(ACCOUNT_ID) + +# get user_id for as_user_id parameter +user_id = UserIdLookup.load(account, screen_name='your_twitter_handle_name').id + campaign = account.campaigns().next() line_item = account.line_items(None, campaign_ids=campaign.id).next() # create request for a simple nullcasted tweet -tweet1 = Tweet.create(account, text='There can be only one...') +tweet1 = Tweet.create(account, text='There can be only one...', as_user_id=user_id) # create request for a nullcasted tweet with a website card website_card = WebsiteCard.all(account).next() -tweet2 = Tweet.create(account, text='Fine. There can be two.', card_uri=website_card.card_uri) +tweet2 = Tweet.create( + account, + text='Fine. There can be two.', + as_user_id=user_id, + card_uri=website_card.card_uri) # promote the tweet using our line item tweet_ids = [tweet1['id'], tweet2['id']] diff --git a/examples/scheduled_tweet.py b/examples/scheduled_tweet.py index b387055..67c7236 100644 --- a/examples/scheduled_tweet.py +++ b/examples/scheduled_tweet.py @@ -1,14 +1,15 @@ from datetime import datetime, timedelta from twitter_ads.client import Client -from twitter_ads.campaign import LineItem, ScheduledPromotedTweet +from twitter_ads.campaign import ScheduledPromotedTweet from twitter_ads.creative import ScheduledTweet +from twitter_ads.restapi import UserIdLookup CONSUMER_KEY = 'your consumer key' CONSUMER_SECRET = 'your consumer secret' -ACCESS_TOKEN = 'access token' -ACCESS_TOKEN_SECRET = 'access token secret' -ACCOUNT_ID = 'account id' +ACCESS_TOKEN = 'user access token' +ACCESS_TOKEN_SECRET = 'user access token secret' +ACCOUNT_ID = 'ads account id' # initialize the client client = Client(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) @@ -16,9 +17,13 @@ # load the advertiser account instance account = client.accounts(ACCOUNT_ID) +# get user_id for as_user_id parameter +user_id = UserIdLookup.load(account, screen_name='your_twitter_handle_name').id + # create the Scheduled Tweet scheduled_tweet = ScheduledTweet(account) scheduled_tweet.text = 'Future' +scheduled_tweet.as_user_id = user_id scheduled_tweet.scheduled_at = datetime.utcnow() + timedelta(days=2) scheduled_tweet.save() diff --git a/tests/test_analytics_async.py b/tests/test_analytics_async.py index 7919bbf..e8c5918 100644 --- a/tests/test_analytics_async.py +++ b/tests/test_analytics_async.py @@ -6,7 +6,7 @@ from twitter_ads.client import Client from twitter_ads.campaign import Campaign from twitter_ads.resource import Analytics -from twitter_ads.enum import METRIC_GROUP, GRANULARITY +from twitter_ads.enum import ENTITY, METRIC_GROUP, GRANULARITY from twitter_ads import API_VERSION @@ -20,7 +20,11 @@ def test_analytics_async(): responses.add(responses.POST, with_resource('/' + API_VERSION + '/stats/jobs/accounts/2iqph'), body=with_fixture('analytics_async_post'), - content_type='application/json') + content_type='application/json', + headers={ + 'x-concurrent-job-limit': '100', + 'x-concurrent-job-limit-remaining': '99' + }) responses.add(responses.GET, with_resource('/' + API_VERSION + '/stats/jobs/accounts/2iqph'), @@ -45,14 +49,30 @@ def test_analytics_async(): granularity=GRANULARITY.TOTAL ) - # test POST request response - queue_async_stats_job() + # call queue_async_stats_job() through Campaign class (inheritance) assert 'granularity=TOTAL' in responses.calls[1].request.url assert stats is not None - assert isinstance(stats, dict) - assert stats['entity_ids'] == ids + assert isinstance(stats, Analytics) + assert stats.entity_ids == ids + assert stats.concurrent_job_limit == '100' + + stats2 = Analytics.queue_async_stats_job( + account, + ids, + metric_groups, + granularity=GRANULARITY.TOTAL, + entity=ENTITY.CAMPAIGN + ) + + # call queue_async_stats_job() from Analytics class directly + assert 'entity=CAMPAIGN' in responses.calls[1].request.url + assert stats2 is not None + assert isinstance(stats2, Analytics) + assert stats2.entity_ids == ids + assert stats2.concurrent_job_limit == '100' # call async_stats_job_result() through Campaign class (inheritance) - job_id = stats['id_str'] + job_id = stats.id_str job_result = Campaign.async_stats_job_result( account, [job_id]).first diff --git a/tests/test_rate_limit.py b/tests/test_rate_limit.py index 3e89e05..64ec371 100644 --- a/tests/test_rate_limit.py +++ b/tests/test_rate_limit.py @@ -77,8 +77,7 @@ def test_rate_limit_handle_with_retry_success_1(monkeypatch): assert len(responses.calls) == 4 assert cursor is not None assert isinstance(cursor, Cursor) - assert cursor.rate_limit is None - assert cursor.account_rate_limit == '10000' + assert cursor.account_rate_limit_limit == '10000' assert cursor.account_rate_limit_remaining == '9999' assert cursor.account_rate_limit_reset == '1546300800' @@ -146,8 +145,7 @@ def test_rate_limit_handle_with_retry_success_2(monkeypatch): assert len(responses.calls) == 4 assert cursor is not None assert isinstance(cursor, Cursor) - assert cursor.rate_limit is None - assert cursor.account_rate_limit == '10000' + assert cursor.account_rate_limit_limit == '10000' assert cursor.account_rate_limit_remaining == '9999' assert cursor.account_rate_limit_reset == '1546300800' @@ -199,8 +197,7 @@ def test_rate_limit_handle_success(monkeypatch): assert len(responses.calls) == 3 assert cursor is not None assert isinstance(cursor, Cursor) - assert cursor.rate_limit is None - assert cursor.account_rate_limit == '10000' + assert cursor.account_rate_limit_limit == '10000' assert cursor.account_rate_limit_remaining == '9999' assert cursor.account_rate_limit_reset == '1546300800' @@ -287,8 +284,7 @@ def test_rate_limit_cursor_class_access(): cursor = Campaign.all(account) assert cursor is not None assert isinstance(cursor, Cursor) - assert cursor.rate_limit is None - assert cursor.account_rate_limit == '10000' + assert cursor.account_rate_limit_limit == '10000' assert cursor.account_rate_limit_remaining == '9999' assert cursor.account_rate_limit_reset == '1546300800' @@ -333,7 +329,6 @@ def test_rate_limit_resource_class_access(): assert isinstance(data, Resource) assert data.id == '2wap7' assert data.entity_status == 'ACTIVE' - assert data.rate_limit is None - assert data.account_rate_limit == '10000' + assert data.account_rate_limit_limit == '10000' assert data.account_rate_limit_remaining == '9999' assert data.account_rate_limit_reset == '1546300800' diff --git a/tests/test_retry_count.py b/tests/test_retry_count.py index 270126d..7cfa689 100644 --- a/tests/test_retry_count.py +++ b/tests/test_retry_count.py @@ -61,8 +61,7 @@ def test_retry_count_success(monkeypatch): assert len(responses.calls) == 3 assert cursor is not None assert isinstance(cursor, Cursor) - assert cursor.rate_limit is None - assert cursor.account_rate_limit == '10000' + assert cursor.account_rate_limit_limit == '10000' assert cursor.account_rate_limit_remaining == '9999' assert cursor.account_rate_limit_reset == '1546300800' diff --git a/twitter_ads/__init__.py b/twitter_ads/__init__.py index d83416c..d4098fc 100644 --- a/twitter_ads/__init__.py +++ b/twitter_ads/__init__.py @@ -1,7 +1,7 @@ # Copyright (C) 2015 Twitter, Inc. -VERSION = (5, 3, 0) -API_VERSION = '5' +VERSION = (6, 0, 0) +API_VERSION = '6' from twitter_ads.utils import get_version diff --git a/twitter_ads/account.py b/twitter_ads/account.py index 1ff1475..e3f2443 100644 --- a/twitter_ads/account.py +++ b/twitter_ads/account.py @@ -6,6 +6,7 @@ from twitter_ads.enum import TRANSFORM from twitter_ads.http import Request from twitter_ads.cursor import Cursor +from twitter_ads.utils import Deprecated from twitter_ads import API_VERSION from twitter_ads.resource import resource_property, Resource @@ -27,7 +28,7 @@ class Account(Resource): RESOURCE_COLLECTION = '/' + API_VERSION + '/accounts' RESOURCE = '/' + API_VERSION + '/accounts/{id}' FEATURES = '/' + API_VERSION + '/accounts/{id}/features' - SCOPED_TIMELINE = '/' + API_VERSION + '/accounts/{id}/scoped_timeline' + SCOPED_TIMELINE = '/5/accounts/{id}/scoped_timeline' def __init__(self, client): self._client = client @@ -155,6 +156,8 @@ def video_website_cards(self, id=None, **kwargs): """ return self._load_resource(VideoWebsiteCard, id, **kwargs) + @Deprecated('This method has been deprecated as of version 5' + 'and no longer works in the latest version.') def scoped_timeline(self, *id, **kwargs): """ Returns the most recent promotable Tweets created by the specified Twitter user. diff --git a/twitter_ads/campaign.py b/twitter_ads/campaign.py index 1a73076..212a7f2 100644 --- a/twitter_ads/campaign.py +++ b/twitter_ads/campaign.py @@ -143,7 +143,6 @@ def tv_shows(klass, account, **kwargs): resource_property(TargetingCriteria, 'targeting_type') resource_property(TargetingCriteria, 'targeting_value') resource_property(TargetingCriteria, 'tailored_audience_expansion') -resource_property(TargetingCriteria, 'tailored_audience_type') # sdk-only resource_property(TargetingCriteria, 'to_delete', transform=TRANSFORM.BOOL) @@ -298,7 +297,7 @@ def targeting_criteria(self, id=None, **kwargs): resource_property(LineItem, 'end_time', transform=TRANSFORM.TIME) resource_property(LineItem, 'entity_status') resource_property(LineItem, 'include_sentiment') -resource_property(LineItem, 'lookalike_expansion') +resource_property(LineItem, 'audience_expansion') resource_property(LineItem, 'name') resource_property(LineItem, 'objective') resource_property(LineItem, 'optimization') @@ -348,9 +347,9 @@ def create(klass, account, **kwargs): params = {} params.update(kwargs) - # handles array to string conversion for media IDs - if 'media_ids' in params and isinstance(params['media_ids'], list): - params['media_ids'] = ','.join(map(str, params['media_ids'])) + # handles array to string conversion for media keys + if 'media_keys' in params and isinstance(params['media_keys'], list): + params['media_keys'] = ','.join(map(str, params['media_keys'])) resource = klass.TWEET_CREATE.format(account_id=account.id) response = Request(account.client, 'post', resource, params=params).perform() diff --git a/twitter_ads/creative.py b/twitter_ads/creative.py index 2ccaa8a..9cddddc 100644 --- a/twitter_ads/creative.py +++ b/twitter_ads/creative.py @@ -11,7 +11,7 @@ from twitter_ads.utils import Deprecated -class PromotedAccount(Resource, Persistence): +class PromotedAccount(Analytics, Resource, Persistence): PROPERTIES = {} @@ -98,12 +98,10 @@ class AccountMedia(Resource, Persistence): resource_property(AccountMedia, 'created_at', readonly=True, transform=TRANSFORM.TIME) resource_property(AccountMedia, 'deleted', readonly=True, transform=TRANSFORM.BOOL) resource_property(AccountMedia, 'id', readonly=True) +resource_property(AccountMedia, 'creative_type', readonly=True) resource_property(AccountMedia, 'media_url', readonly=True) +resource_property(AccountMedia, 'media_key', readonly=True) resource_property(AccountMedia, 'updated_at', readonly=True, transform=TRANSFORM.TIME) -# writable -resource_property(AccountMedia, 'creative_type') -resource_property(AccountMedia, 'media_id') -resource_property(AccountMedia, 'video_id') class MediaCreative(Analytics, Resource, Persistence): @@ -143,7 +141,7 @@ class WebsiteCard(Resource, Persistence): resource_property(WebsiteCard, 'card_uri', readonly=True) resource_property(WebsiteCard, 'created_at', readonly=True, transform=TRANSFORM.TIME) resource_property(WebsiteCard, 'id', readonly=True) -resource_property(WebsiteCard, 'image', readonly=True) +resource_property(WebsiteCard, 'media_url', readonly=True) resource_property(WebsiteCard, 'image_display_height', readonly=True) resource_property(WebsiteCard, 'image_display_width', readonly=True) resource_property(WebsiteCard, 'deleted', readonly=True, transform=TRANSFORM.BOOL) @@ -151,7 +149,7 @@ class WebsiteCard(Resource, Persistence): resource_property(WebsiteCard, 'website_display_url', readonly=True) resource_property(WebsiteCard, 'updated_at', readonly=True, transform=TRANSFORM.TIME) # writable -resource_property(WebsiteCard, 'image_media_id') +resource_property(WebsiteCard, 'media_key') resource_property(WebsiteCard, 'name') resource_property(WebsiteCard, 'website_title') resource_property(WebsiteCard, 'website_url') @@ -174,21 +172,19 @@ class VideoWebsiteCard(Resource, Persistence): resource_property(VideoWebsiteCard, 'deleted', readonly=True, transform=TRANSFORM.BOOL) resource_property(VideoWebsiteCard, 'id', readonly=True) resource_property(VideoWebsiteCard, 'updated_at', readonly=True, transform=TRANSFORM.TIME) -resource_property(VideoWebsiteCard, 'video_content_id', readonly=True) resource_property(VideoWebsiteCard, 'video_height', readonly=True) -resource_property(VideoWebsiteCard, 'video_hls_url', readonly=True) resource_property(VideoWebsiteCard, 'video_owner_id', readonly=True) resource_property(VideoWebsiteCard, 'video_poster_height', readonly=True) -resource_property(VideoWebsiteCard, 'video_poster_url', readonly=True) +resource_property(VideoWebsiteCard, 'poster_media_url', readonly=True) resource_property(VideoWebsiteCard, 'video_poster_width', readonly=True) -resource_property(VideoWebsiteCard, 'video_url', readonly=True) +resource_property(VideoWebsiteCard, 'media_url', readonly=True) resource_property(VideoWebsiteCard, 'video_width', readonly=True) resource_property(VideoWebsiteCard, 'website_dest_url', readonly=True) resource_property(VideoWebsiteCard, 'website_display_url', readonly=True) # writable resource_property(VideoWebsiteCard, 'name') resource_property(VideoWebsiteCard, 'title') -resource_property(VideoWebsiteCard, 'video_id') +resource_property(VideoWebsiteCard, 'media_key') resource_property(VideoWebsiteCard, 'website_url') @@ -205,7 +201,7 @@ class ImageAppDownloadCard(Resource, Persistence): resource_property(ImageAppDownloadCard, 'id', readonly=True) resource_property(ImageAppDownloadCard, 'image_display_height', readonly=True) resource_property(ImageAppDownloadCard, 'image_display_width', readonly=True) -resource_property(ImageAppDownloadCard, 'wide_app_image', readonly=True) +resource_property(ImageAppDownloadCard, 'media_url', readonly=True) resource_property(ImageAppDownloadCard, 'card_uri', readonly=True) resource_property(ImageAppDownloadCard, 'card_type', readonly=True) resource_property(ImageAppDownloadCard, 'created_at', readonly=True, transform=TRANSFORM.TIME) @@ -221,7 +217,7 @@ class ImageAppDownloadCard(Resource, Persistence): resource_property(ImageAppDownloadCard, 'googleplay_app_id') resource_property(ImageAppDownloadCard, 'googleplay_deep_link') resource_property(ImageAppDownloadCard, 'name') -resource_property(ImageAppDownloadCard, 'wide_app_image_media_id') +resource_property(ImageAppDownloadCard, 'media_key') class VideoAppDownloadCard(Resource, Persistence): @@ -240,15 +236,13 @@ class VideoAppDownloadCard(Resource, Persistence): resource_property(VideoAppDownloadCard, 'deleted', readonly=True, transform=TRANSFORM.BOOL) resource_property(VideoAppDownloadCard, 'id', readonly=True) resource_property(VideoAppDownloadCard, 'updated_at', readonly=True, transform=TRANSFORM.TIME) -resource_property(VideoAppDownloadCard, 'video_content_id', readonly=True) -resource_property(VideoAppDownloadCard, 'video_hls_url', readonly=True) resource_property(VideoAppDownloadCard, 'video_owner_id', readonly=True) -resource_property(VideoAppDownloadCard, 'video_poster_url', readonly=True) -resource_property(VideoAppDownloadCard, 'video_url', readonly=True) +resource_property(VideoAppDownloadCard, 'poster_media_url', readonly=True) +resource_property(VideoAppDownloadCard, 'media_url', readonly=True) # writable resource_property(VideoAppDownloadCard, 'country_code') resource_property(VideoAppDownloadCard, 'app_cta') -resource_property(VideoAppDownloadCard, 'image_media_id') +resource_property(VideoAppDownloadCard, 'poster_media_key') resource_property(VideoAppDownloadCard, 'ipad_app_id') resource_property(VideoAppDownloadCard, 'ipad_deep_link') resource_property(VideoAppDownloadCard, 'iphone_app_id') @@ -256,7 +250,7 @@ class VideoAppDownloadCard(Resource, Persistence): resource_property(VideoAppDownloadCard, 'googleplay_app_id') resource_property(VideoAppDownloadCard, 'googleplay_deep_link') resource_property(VideoAppDownloadCard, 'name') -resource_property(VideoAppDownloadCard, 'video_id') +resource_property(VideoAppDownloadCard, 'media_key') class ImageConversationCard(Resource, Persistence): @@ -274,13 +268,13 @@ class ImageConversationCard(Resource, Persistence): resource_property(ImageConversationCard, 'created_at', readonly=True, transform=TRANSFORM.TIME) resource_property(ImageConversationCard, 'deleted', readonly=True, transform=TRANSFORM.BOOL) resource_property(ImageConversationCard, 'id', readonly=True) -resource_property(ImageConversationCard, 'image', readonly=True) +resource_property(ImageConversationCard, 'media_url', readonly=True) resource_property(ImageConversationCard, 'updated_at', readonly=True, transform=TRANSFORM.TIME) # writable -resource_property(ImageConversationCard, 'cover_image_id') +resource_property(ImageConversationCard, 'unlocked_image_media_key') resource_property(ImageConversationCard, 'fouth_cta') resource_property(ImageConversationCard, 'fouth_cta_tweet') -resource_property(ImageConversationCard, 'image_media_id') +resource_property(ImageConversationCard, 'media_key') resource_property(ImageConversationCard, 'first_cta') resource_property(ImageConversationCard, 'first_cta_tweet') resource_property(ImageConversationCard, 'name') @@ -309,15 +303,15 @@ class VideoConversationCard(Resource, Persistence): resource_property(VideoConversationCard, 'created_at', readonly=True, transform=TRANSFORM.TIME) resource_property(VideoConversationCard, 'deleted', readonly=True, transform=TRANSFORM.BOOL) resource_property(VideoConversationCard, 'id', readonly=True) -resource_property(VideoConversationCard, 'video_url', readonly=True) -resource_property(VideoConversationCard, 'video_poster_url', readonly=True) +resource_property(VideoConversationCard, 'media_url', readonly=True) +resource_property(VideoConversationCard, 'poster_media_url', readonly=True) resource_property(VideoConversationCard, 'updated_at', readonly=True, transform=TRANSFORM.TIME) # writable -resource_property(ImageConversationCard, 'cover_image_id') -resource_property(ImageConversationCard, 'cover_video_id') +resource_property(ImageConversationCard, 'unlocked_image_media_key') +resource_property(ImageConversationCard, 'unlocked_video_media_key') resource_property(ImageConversationCard, 'fouth_cta') resource_property(ImageConversationCard, 'fouth_cta_tweet') -resource_property(ImageConversationCard, 'image_media_id') +resource_property(ImageConversationCard, 'poster_media_key') resource_property(ImageConversationCard, 'first_cta') resource_property(ImageConversationCard, 'first_cta_tweet') resource_property(ImageConversationCard, 'name') @@ -328,7 +322,7 @@ class VideoConversationCard(Resource, Persistence): resource_property(ImageConversationCard, 'third_cta') resource_property(ImageConversationCard, 'third_cta_tweet') resource_property(ImageConversationCard, 'title') -resource_property(ImageConversationCard, 'video_id') +resource_property(ImageConversationCard, 'media_key') class ScheduledTweet(Resource, Persistence): @@ -345,7 +339,6 @@ class ScheduledTweet(Resource, Persistence): resource_property(ScheduledTweet, 'completed_at', read_only=True, transform=TRANSFORM.TIME) resource_property(ScheduledTweet, 'id', read_only=True) resource_property(ScheduledTweet, 'id_str', read_only=True) -resource_property(ScheduledTweet, 'media_keys', readonly=True, transform=TRANSFORM.LIST) resource_property(ScheduledTweet, 'scheduled_status', read_only=True) resource_property(ScheduledTweet, 'tweet_id', readonly=True) resource_property(ScheduledTweet, 'updated_at', readonly=True, transform=TRANSFORM.TIME) @@ -353,7 +346,7 @@ class ScheduledTweet(Resource, Persistence): # writable resource_property(ScheduledTweet, 'as_user_id') resource_property(ScheduledTweet, 'card_uri') -resource_property(ScheduledTweet, 'media_ids', transform=TRANSFORM.LIST) +resource_property(ScheduledTweet, 'media_keys', transform=TRANSFORM.LIST) resource_property(ScheduledTweet, 'nullcast', transform=TRANSFORM.BOOL) resource_property(ScheduledTweet, 'scheduled_at', transform=TRANSFORM.TIME) resource_property(ScheduledTweet, 'text') @@ -371,14 +364,13 @@ class DraftTweet(Resource, Persistence): # read-only resource_property(DraftTweet, 'id', read_only=True) resource_property(DraftTweet, 'id_str', read_only=True) -resource_property(DraftTweet, 'media_keys', readonly=True, transform=TRANSFORM.LIST) resource_property(DraftTweet, 'created_at', read_only=True, transform=TRANSFORM.TIME) resource_property(DraftTweet, 'updated_at', readonly=True, transform=TRANSFORM.TIME) resource_property(DraftTweet, 'user_id', read_only=True) # writable resource_property(DraftTweet, 'as_user_id') resource_property(DraftTweet, 'card_uri') -resource_property(DraftTweet, 'media_ids', transform=TRANSFORM.LIST) +resource_property(DraftTweet, 'media_keys', transform=TRANSFORM.LIST) resource_property(DraftTweet, 'nullcast', transform=TRANSFORM.BOOL) resource_property(DraftTweet, 'text') @@ -428,17 +420,15 @@ def delete(self): resource_property(MediaLibrary, 'media_status', readonly=True) resource_property(MediaLibrary, 'media_type', readonly=True) resource_property(MediaLibrary, 'media_url', readonly=True) +resource_property(MediaLibrary, 'poster_media_url', readonly=True) resource_property(MediaLibrary, 'tweeted', readonly=True, transform=TRANSFORM.BOOL) resource_property(MediaLibrary, 'updated_at', readonly=True, transform=TRANSFORM.TIME) # writable -resource_property(MediaLibrary, 'media_category') -resource_property(MediaLibrary, 'media_id') resource_property(MediaLibrary, 'media_key') resource_property(MediaLibrary, 'description') resource_property(MediaLibrary, 'file_name') resource_property(MediaLibrary, 'name') -resource_property(MediaLibrary, 'poster_image_media_id') -resource_property(MediaLibrary, 'poster_image_media_key') +resource_property(MediaLibrary, 'poster_media_key') resource_property(MediaLibrary, 'title') diff --git a/twitter_ads/enum.py b/twitter_ads/enum.py index 4b3619f..967cd93 100644 --- a/twitter_ads/enum.py +++ b/twitter_ads/enum.py @@ -78,7 +78,8 @@ def enum(**enums): MEDIA_CREATIVE='MEDIA_CREATIVE', PROMOTED_TWEET='PROMOTED_TWEET', ORGANIC_TWEET='ORGANIC_TWEET', - TARGETING_CRITERION='TARGETING_CRITERION' + TARGETING_CRITERION='TARGETING_CRITERION', + PROMOTED_ACCOUNT='PROMOTED_ACCOUNT' ) ENTITY_STATUS = enum( @@ -138,7 +139,10 @@ def enum(**enums): ) OPTIMIZATIONS = enum( + APP_CLICKS='APP_CLICKS', + APP_INSTALLS='APP_INSTALLS', DEFAULT='DEFAULT', + ENGAGEMENTS='ENGAGEMENTS', WEBSITE_CONVERSIONS='WEBSITE_CONVERSIONS' ) @@ -195,7 +199,7 @@ def enum(**enums): AGE='AGE', APP_STORE_CATEGORY='APP_STORE_CATEGORY', AUDIENCES='AUDIENCES', - CITIES='CITIES', + METROS='METROS', CONVERSATIONS='CONVERSATIONS', CONVERSION_TAGS='CONVERSION_TAGS', DEVICES='DEVICES', diff --git a/twitter_ads/resource.py b/twitter_ads/resource.py index 02de25f..9e5700d 100644 --- a/twitter_ads/resource.py +++ b/twitter_ads/resource.py @@ -230,7 +230,8 @@ class Analytics(Resource): 'LineItem': ENTITY.LINE_ITEM, 'MediaCreative': ENTITY.MEDIA_CREATIVE, 'OrganicTweet': ENTITY.ORGANIC_TWEET, - 'PromotedTweet': ENTITY.PROMOTED_TWEET + 'PromotedTweet': ENTITY.PROMOTED_TWEET, + 'PromotedAccount': ENTITY.PROMOTED_ACCOUNT } RESOURCE_SYNC = '/' + API_VERSION + '/stats/accounts/{account_id}' @@ -252,13 +253,14 @@ def _standard_params(klass, ids, metric_groups, **kwargs): start_time = kwargs.get('start_time', end_time - timedelta(seconds=604800)) granularity = kwargs.get('granularity', GRANULARITY.HOUR) placement = kwargs.get('placement', PLACEMENT.ALL_ON_TWITTER) + entity = kwargs.get('entity', None) params = { 'metric_groups': ','.join(map(str, metric_groups)), 'start_time': to_time(start_time, granularity), 'end_time': to_time(end_time, granularity), 'granularity': granularity.upper(), - 'entity': klass.ANALYTICS_MAP[klass.__name__], + 'entity': entity or klass.ANALYTICS_MAP[klass.__name__], 'placement': placement } @@ -290,7 +292,7 @@ def queue_async_stats_job(klass, account, ids, metric_groups, **kwargs): resource = klass.RESOURCE_ASYNC.format(account_id=account.id) response = Request(account.client, 'post', resource, params=params).perform() - return response.body['data'] + return Analytics(account).from_response(response.body['data'], headers=response.headers) @classmethod def async_stats_job_result(klass, account, job_ids=None, **kwargs): @@ -351,12 +353,20 @@ def active_entities(klass, account, start_time, end_time, **kwargs): return response.body['data'] -# async_stats_job_result() properties +# Analytics properties # read-only resource_property(Analytics, 'id', readonly=True) resource_property(Analytics, 'id_str', readonly=True) resource_property(Analytics, 'status', readonly=True) resource_property(Analytics, 'url', readonly=True) -resource_property(Analytics, 'created_at', readonly=True) -resource_property(Analytics, 'expires_at', readonly=True) -resource_property(Analytics, 'updated_at', readonly=True) +resource_property(Analytics, 'created_at', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'expires_at', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'updated_at', readonly=True, transform=TRANSFORM.TIME) + +resource_property(Analytics, 'start_time', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'end_time', readonly=True, transform=TRANSFORM.TIME) +resource_property(Analytics, 'entity', readonly=True) +resource_property(Analytics, 'entity_ids', readonly=True) +resource_property(Analytics, 'placement', readonly=True) +resource_property(Analytics, 'granularity', readonly=True) +resource_property(Analytics, 'metric_groups', readonly=True) diff --git a/twitter_ads/restapi.py b/twitter_ads/restapi.py new file mode 100644 index 0000000..de9cc62 --- /dev/null +++ b/twitter_ads/restapi.py @@ -0,0 +1,30 @@ +from twitter_ads.http import Request +from twitter_ads.resource import resource_property, Resource + + +class UserIdLookup(Resource): + PROPERTIES = {} + + DOMAIN = 'https://api.twitter.com' + RESOURCE = '/1.1/users/show.json' + + @classmethod + def load(klass, account, screen_name): + params = {} + params['screen_name'] = screen_name + + response = Request( + account.client, + 'get', + resource=klass.RESOURCE, + params=params, + domain=klass.DOMAIN + ).perform() + return klass(account).from_response(response.body, response.headers) + + +# users/show endpoint properties +# read-only +resource_property(UserIdLookup, 'id', readonly=True) +resource_property(UserIdLookup, 'id_str', readonly=True) +resource_property(UserIdLookup, 'screen_name', readonly=True) diff --git a/twitter_ads/utils.py b/twitter_ads/utils.py index 765e1f3..587e45f 100644 --- a/twitter_ads/utils.py +++ b/twitter_ads/utils.py @@ -4,6 +4,7 @@ """Container for all helpers and utilities used throughout the Ads API SDK.""" import datetime +import re import warnings warnings.simplefilter('default', DeprecationWarning) from email.utils import formatdate @@ -71,16 +72,20 @@ def validate_whole_hours(time): def extract_response_headers(headers): values = {} + # only get "X-${name}" custom response headers + reg = re.compile(r"^x-", re.IGNORECASE) + for i in headers: + if reg.match(i): + values[i.lstrip('x-').replace('-', '_')] = headers[i] - values['rate_limit'] = headers.get('x-rate-limit-limit') - values['rate_limit_remaining'] = headers.get('x-rate-limit-remaining') - values['rate_limit_reset'] = headers.get('x-rate-limit-reset') + return values - values['account_rate_limit'] = headers.get('x-account-rate-limit-limit') - values['account_rate_limit_remaining'] = headers.get('x-account-rate-limit-remaining') - values['account_rate_limit_reset'] = headers.get('x-account-rate-limit-reset') - return values +def split_list(list_, n): + """Splits a list by a given number (n) and returns a generator object.""" + list_size = len(list_) + for sp in range(0, list_size, n): + yield list_[sp:min(sp + n, list_size)] class Deprecated(object):