Skip to content

Commit ab79ffe

Browse files
authored
v6 release (#223)
* v6: Expose concurrent job limit - Analytics.queue_async_stats_job() now returns instance rather than dict - Analytics.queue_async_stats_job() can now be called directly (with specifying entity parameter) * Add support for PROMOTED_ACCOUNT stats entity * Rename CITIES segmentation name to METROS * remove Scoped Timeline * as_user_id is now required parameter * Add additional optimization enum * remove tailored_audience_type request parameter * rename lookalike_expansion to audience_expansion * Media identifier consistency https://twittercommunity.com/t/ads-api-version-6/129060#heading--media-identifier-consistency * change API_VERSION * Revert "remove Scoped Timeline" This reverts commit 34264d3. * mark scoped_timeline as deprecated * update comment * update example * Add helper function - split_list() can be used for splitting entity ids by a given number
1 parent 163b0d9 commit ab79ffe

15 files changed

+178
-94
lines changed

examples/analytics.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from twitter_ads.client import Client
1414
from twitter_ads.campaign import LineItem
1515
from twitter_ads.enum import METRIC_GROUP
16+
from twitter_ads.utils import split_list
1617

1718
CONSUMER_KEY = 'your consumer key'
1819
CONSUMER_SECRET = 'your consumer secret'
@@ -42,20 +43,29 @@
4243
print('Error: A minimum of 1 items must be provided for entity_ids')
4344
sys.exit()
4445

45-
LineItem.all_stats(account, ids, metric_groups)
46+
sync_data = []
47+
# Sync/Async endpoint can handle max 20 entity IDs per request
48+
# so split the ids list into multiple requests
49+
for chunk_ids in split_list(ids, 20):
50+
sync_data.append(LineItem.all_stats(account, chunk_ids, metric_groups))
4651

47-
# fetching async stats on the instance
48-
queued_job = LineItem.queue_async_stats_job(account, ids, metric_groups)
52+
print(sync_data)
4953

50-
# get the job_id:
51-
job_id = queued_job['id']
54+
# create async stats jobs and get job ids
55+
queued_job_ids = []
56+
for chunk_ids in split_list(ids, 20):
57+
queued_job_ids.append(LineItem.queue_async_stats_job(account, chunk_ids, metric_groups).id)
58+
59+
print(queued_job_ids)
5260

5361
# let the job complete
54-
seconds = 15
62+
seconds = 30
5563
time.sleep(seconds)
5664

57-
async_stats_job_result = LineItem.async_stats_job_result(account, [job_id]).first
65+
async_stats_job_results = LineItem.async_stats_job_result(account, queued_job_ids)
5866

59-
async_data = LineItem.async_stats_job_data(account, async_stats_job_result.url)
67+
async_data = []
68+
for result in async_stats_job_results:
69+
async_data.append(LineItem.async_stats_job_data(account, result.url))
6070

6171
print(async_data)

examples/draft_tweet.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from twitter_ads.client import Client
22
from twitter_ads.campaign import Tweet
33
from twitter_ads.creative import DraftTweet
4+
from twitter_ads.restapi import UserIdLookup
45

56

67
CONSUMER_KEY = 'your consumer key'
@@ -15,6 +16,9 @@
1516
# load the advertiser account instance
1617
account = client.accounts(ACCOUNT_ID)
1718

19+
# get user_id for as_user_id parameter
20+
user_id = UserIdLookup.load(account, screen_name='your_twitter_handle_name').id
21+
1822
# fetch draft tweets from a given account
1923
tweets = DraftTweet.all(account)
2024
for tweet in tweets:
@@ -24,6 +28,7 @@
2428
# create a new draft tweet
2529
draft_tweet = DraftTweet(account)
2630
draft_tweet.text = 'draft tweet - new'
31+
draft_tweet.as_user_id = user_id
2732
draft_tweet = draft_tweet.save()
2833
print(draft_tweet.id_str)
2934
print(draft_tweet.text)
@@ -41,7 +46,7 @@
4146
print(draft_tweet.text)
4247

4348
# create a nullcasted tweet using draft tweet metadata
44-
tweet = Tweet.create(account, text=draft_tweet.text)
49+
tweet = Tweet.create(account, text=draft_tweet.text, as_user_id=user_id)
4550
print(tweet)
4651

4752
# delete draft tweet

examples/promoted_tweet.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from twitter_ads.client import Client
44
from twitter_ads.campaign import Tweet
55
from twitter_ads.creative import PromotedTweet, WebsiteCard
6+
from twitter_ads.restapi import UserIdLookup
67

78
CONSUMER_KEY = 'your consumer key'
89
CONSUMER_SECRET = 'your consumer secret'
@@ -15,15 +16,23 @@
1516

1617
# load up the account instance, campaign and line item
1718
account = client.accounts(ACCOUNT_ID)
19+
20+
# get user_id for as_user_id parameter
21+
user_id = UserIdLookup.load(account, screen_name='your_twitter_handle_name').id
22+
1823
campaign = account.campaigns().next()
1924
line_item = account.line_items(None, campaign_ids=campaign.id).next()
2025

2126
# create request for a simple nullcasted tweet
22-
tweet1 = Tweet.create(account, text='There can be only one...')
27+
tweet1 = Tweet.create(account, text='There can be only one...', as_user_id=user_id)
2328

2429
# create request for a nullcasted tweet with a website card
2530
website_card = WebsiteCard.all(account).next()
26-
tweet2 = Tweet.create(account, text='Fine. There can be two.', card_uri=website_card.card_uri)
31+
tweet2 = Tweet.create(
32+
account,
33+
text='Fine. There can be two.',
34+
as_user_id=user_id,
35+
card_uri=website_card.card_uri)
2736

2837
# promote the tweet using our line item
2938
tweet_ids = [tweet1['id'], tweet2['id']]

examples/scheduled_tweet.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
from datetime import datetime, timedelta
22

33
from twitter_ads.client import Client
4-
from twitter_ads.campaign import LineItem, ScheduledPromotedTweet
4+
from twitter_ads.campaign import ScheduledPromotedTweet
55
from twitter_ads.creative import ScheduledTweet
6+
from twitter_ads.restapi import UserIdLookup
67

78
CONSUMER_KEY = 'your consumer key'
89
CONSUMER_SECRET = 'your consumer secret'
9-
ACCESS_TOKEN = 'access token'
10-
ACCESS_TOKEN_SECRET = 'access token secret'
11-
ACCOUNT_ID = 'account id'
10+
ACCESS_TOKEN = 'user access token'
11+
ACCESS_TOKEN_SECRET = 'user access token secret'
12+
ACCOUNT_ID = 'ads account id'
1213

1314
# initialize the client
1415
client = Client(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
1516

1617
# load the advertiser account instance
1718
account = client.accounts(ACCOUNT_ID)
1819

20+
# get user_id for as_user_id parameter
21+
user_id = UserIdLookup.load(account, screen_name='your_twitter_handle_name').id
22+
1923
# create the Scheduled Tweet
2024
scheduled_tweet = ScheduledTweet(account)
2125
scheduled_tweet.text = 'Future'
26+
scheduled_tweet.as_user_id = user_id
2227
scheduled_tweet.scheduled_at = datetime.utcnow() + timedelta(days=2)
2328
scheduled_tweet.save()
2429

tests/test_analytics_async.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from twitter_ads.client import Client
77
from twitter_ads.campaign import Campaign
88
from twitter_ads.resource import Analytics
9-
from twitter_ads.enum import METRIC_GROUP, GRANULARITY
9+
from twitter_ads.enum import ENTITY, METRIC_GROUP, GRANULARITY
1010
from twitter_ads import API_VERSION
1111

1212

@@ -20,7 +20,11 @@ def test_analytics_async():
2020
responses.add(responses.POST,
2121
with_resource('/' + API_VERSION + '/stats/jobs/accounts/2iqph'),
2222
body=with_fixture('analytics_async_post'),
23-
content_type='application/json')
23+
content_type='application/json',
24+
headers={
25+
'x-concurrent-job-limit': '100',
26+
'x-concurrent-job-limit-remaining': '99'
27+
})
2428

2529
responses.add(responses.GET,
2630
with_resource('/' + API_VERSION + '/stats/jobs/accounts/2iqph'),
@@ -45,14 +49,30 @@ def test_analytics_async():
4549
granularity=GRANULARITY.TOTAL
4650
)
4751

48-
# test POST request response - queue_async_stats_job()
52+
# call queue_async_stats_job() through Campaign class (inheritance)
4953
assert 'granularity=TOTAL' in responses.calls[1].request.url
5054
assert stats is not None
51-
assert isinstance(stats, dict)
52-
assert stats['entity_ids'] == ids
55+
assert isinstance(stats, Analytics)
56+
assert stats.entity_ids == ids
57+
assert stats.concurrent_job_limit == '100'
58+
59+
stats2 = Analytics.queue_async_stats_job(
60+
account,
61+
ids,
62+
metric_groups,
63+
granularity=GRANULARITY.TOTAL,
64+
entity=ENTITY.CAMPAIGN
65+
)
66+
67+
# call queue_async_stats_job() from Analytics class directly
68+
assert 'entity=CAMPAIGN' in responses.calls[1].request.url
69+
assert stats2 is not None
70+
assert isinstance(stats2, Analytics)
71+
assert stats2.entity_ids == ids
72+
assert stats2.concurrent_job_limit == '100'
5373

5474
# call async_stats_job_result() through Campaign class (inheritance)
55-
job_id = stats['id_str']
75+
job_id = stats.id_str
5676
job_result = Campaign.async_stats_job_result(
5777
account,
5878
[job_id]).first

tests/test_rate_limit.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ def test_rate_limit_handle_with_retry_success_1(monkeypatch):
7777
assert len(responses.calls) == 4
7878
assert cursor is not None
7979
assert isinstance(cursor, Cursor)
80-
assert cursor.rate_limit is None
81-
assert cursor.account_rate_limit == '10000'
80+
assert cursor.account_rate_limit_limit == '10000'
8281
assert cursor.account_rate_limit_remaining == '9999'
8382
assert cursor.account_rate_limit_reset == '1546300800'
8483

@@ -146,8 +145,7 @@ def test_rate_limit_handle_with_retry_success_2(monkeypatch):
146145
assert len(responses.calls) == 4
147146
assert cursor is not None
148147
assert isinstance(cursor, Cursor)
149-
assert cursor.rate_limit is None
150-
assert cursor.account_rate_limit == '10000'
148+
assert cursor.account_rate_limit_limit == '10000'
151149
assert cursor.account_rate_limit_remaining == '9999'
152150
assert cursor.account_rate_limit_reset == '1546300800'
153151

@@ -199,8 +197,7 @@ def test_rate_limit_handle_success(monkeypatch):
199197
assert len(responses.calls) == 3
200198
assert cursor is not None
201199
assert isinstance(cursor, Cursor)
202-
assert cursor.rate_limit is None
203-
assert cursor.account_rate_limit == '10000'
200+
assert cursor.account_rate_limit_limit == '10000'
204201
assert cursor.account_rate_limit_remaining == '9999'
205202
assert cursor.account_rate_limit_reset == '1546300800'
206203

@@ -287,8 +284,7 @@ def test_rate_limit_cursor_class_access():
287284
cursor = Campaign.all(account)
288285
assert cursor is not None
289286
assert isinstance(cursor, Cursor)
290-
assert cursor.rate_limit is None
291-
assert cursor.account_rate_limit == '10000'
287+
assert cursor.account_rate_limit_limit == '10000'
292288
assert cursor.account_rate_limit_remaining == '9999'
293289
assert cursor.account_rate_limit_reset == '1546300800'
294290

@@ -333,7 +329,6 @@ def test_rate_limit_resource_class_access():
333329
assert isinstance(data, Resource)
334330
assert data.id == '2wap7'
335331
assert data.entity_status == 'ACTIVE'
336-
assert data.rate_limit is None
337-
assert data.account_rate_limit == '10000'
332+
assert data.account_rate_limit_limit == '10000'
338333
assert data.account_rate_limit_remaining == '9999'
339334
assert data.account_rate_limit_reset == '1546300800'

tests/test_retry_count.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ def test_retry_count_success(monkeypatch):
6161
assert len(responses.calls) == 3
6262
assert cursor is not None
6363
assert isinstance(cursor, Cursor)
64-
assert cursor.rate_limit is None
65-
assert cursor.account_rate_limit == '10000'
64+
assert cursor.account_rate_limit_limit == '10000'
6665
assert cursor.account_rate_limit_remaining == '9999'
6766
assert cursor.account_rate_limit_reset == '1546300800'
6867

twitter_ads/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (C) 2015 Twitter, Inc.
22

3-
VERSION = (5, 3, 0)
4-
API_VERSION = '5'
3+
VERSION = (6, 0, 0)
4+
API_VERSION = '6'
55

66
from twitter_ads.utils import get_version
77

twitter_ads/account.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from twitter_ads.enum import TRANSFORM
77
from twitter_ads.http import Request
88
from twitter_ads.cursor import Cursor
9+
from twitter_ads.utils import Deprecated
910
from twitter_ads import API_VERSION
1011

1112
from twitter_ads.resource import resource_property, Resource
@@ -27,7 +28,7 @@ class Account(Resource):
2728
RESOURCE_COLLECTION = '/' + API_VERSION + '/accounts'
2829
RESOURCE = '/' + API_VERSION + '/accounts/{id}'
2930
FEATURES = '/' + API_VERSION + '/accounts/{id}/features'
30-
SCOPED_TIMELINE = '/' + API_VERSION + '/accounts/{id}/scoped_timeline'
31+
SCOPED_TIMELINE = '/5/accounts/{id}/scoped_timeline'
3132

3233
def __init__(self, client):
3334
self._client = client
@@ -155,6 +156,8 @@ def video_website_cards(self, id=None, **kwargs):
155156
"""
156157
return self._load_resource(VideoWebsiteCard, id, **kwargs)
157158

159+
@Deprecated('This method has been deprecated as of version 5'
160+
'and no longer works in the latest version.')
158161
def scoped_timeline(self, *id, **kwargs):
159162
"""
160163
Returns the most recent promotable Tweets created by the specified Twitter user.

twitter_ads/campaign.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ def tv_shows(klass, account, **kwargs):
143143
resource_property(TargetingCriteria, 'targeting_type')
144144
resource_property(TargetingCriteria, 'targeting_value')
145145
resource_property(TargetingCriteria, 'tailored_audience_expansion')
146-
resource_property(TargetingCriteria, 'tailored_audience_type')
147146
# sdk-only
148147
resource_property(TargetingCriteria, 'to_delete', transform=TRANSFORM.BOOL)
149148

@@ -298,7 +297,7 @@ def targeting_criteria(self, id=None, **kwargs):
298297
resource_property(LineItem, 'end_time', transform=TRANSFORM.TIME)
299298
resource_property(LineItem, 'entity_status')
300299
resource_property(LineItem, 'include_sentiment')
301-
resource_property(LineItem, 'lookalike_expansion')
300+
resource_property(LineItem, 'audience_expansion')
302301
resource_property(LineItem, 'name')
303302
resource_property(LineItem, 'objective')
304303
resource_property(LineItem, 'optimization')
@@ -348,9 +347,9 @@ def create(klass, account, **kwargs):
348347
params = {}
349348
params.update(kwargs)
350349

351-
# handles array to string conversion for media IDs
352-
if 'media_ids' in params and isinstance(params['media_ids'], list):
353-
params['media_ids'] = ','.join(map(str, params['media_ids']))
350+
# handles array to string conversion for media keys
351+
if 'media_keys' in params and isinstance(params['media_keys'], list):
352+
params['media_keys'] = ','.join(map(str, params['media_keys']))
354353

355354
resource = klass.TWEET_CREATE.format(account_id=account.id)
356355
response = Request(account.client, 'post', resource, params=params).perform()

0 commit comments

Comments
 (0)