diff --git a/.gitignore b/.gitignore
index 60d1cc63..b9726bcd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,3 +41,4 @@ docs/_build
test.py
.venv
+.idea
\ No newline at end of file
diff --git a/tests/config.py b/tests/config.py
index 21baa69b..5cf7dddf 100644
--- a/tests/config.py
+++ b/tests/config.py
@@ -28,3 +28,7 @@
test_tweet_object = {u'contributors': None, u'truncated': False, u'text': u'http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ https://t.co/67pwRvY6z9 http://t.co/N6InAO4B71', u'in_reply_to_status_id': None, u'id': 349683012054683648, u'favorite_count': 0, u'source': u'web', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [{u'id': 29251354, u'indices': [45, 57], u'id_str': u'29251354', u'screen_name': u'mikehelmick', u'name': u'Mike Helmick'}, {u'id': 1431865928, u'indices': [81, 93], u'id_str': u'1431865928', u'screen_name': u'__twython__', u'name': u'Twython'}], u'hashtags': [{u'indices': [28, 33], u'text': u'cool'}, {u'indices': [62, 73], u'text': u'checkitout'}], u'urls': [{u'url': u'http://t.co/FCmXyI6VHd', u'indices': [0, 22], u'expanded_url': u'http://google.com', u'display_url': u'google.com'}, {u'url': u'https://t.co/67pwRvY6z9', u'indices': [94, 117], u'expanded_url': u'https://github.com', u'display_url': u'github.com'}], u'media': [{u'id': 537884378513162240, u'id_str': u'537884378513162240', u'indices': [118, 140], u'media_url': u'http://pbs.twimg.com/media/B3by_g-CQAAhrO5.jpg', u'media_url_https': u'https://pbs.twimg.com/media/B3by_g-CQAAhrO5.jpg', u'url': u'http://t.co/N6InAO4B71', u'display_url': u'pic.twitter.com/N6InAO4B71', u'expanded_url': u'http://twitter.com/pingofglitch/status/537884380060844032/photo/1', u'type': u'photo', u'sizes': {u'large': {u'w': 1024, u'h': 640, u'resize': u'fit'}, u'thumb': {u'w': 150, u'h': 150, u'resize': u'crop'}, u'medium': {u'w': 600, u'h': 375, u'resize': u'fit'}, u'small': {u'w': 340, u'h': 212, u'resize': u'fit'}}}]}, u'in_reply_to_screen_name': None, u'id_str': u'349683012054683648', u'retweet_count': 0, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'profile_use_background_image': True, u'default_profile_image': True, u'id': 1431865928, u'verified': False, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://si0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png', u'profile_sidebar_fill_color': u'DDEEF6', u'entities': {u'description': {u'urls': []}}, u'followers_count': 1, u'profile_sidebar_border_color': u'C0DEED', u'id_str': u'1431865928', u'profile_background_color': u'3D3D3D', u'listed_count': 0, u'profile_background_image_url_https': u'https://si0.twimg.com/images/themes/theme1/bg.png', u'utc_offset': None, u'statuses_count': 2, u'description': u'', u'friends_count': 1, u'location': u'', u'profile_link_color': u'0084B4', u'profile_image_url': u'http://a0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png', u'following': False, u'geo_enabled': False, u'profile_background_image_url': u'http://a0.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'__twython__', u'lang': u'en', u'profile_background_tile': False, u'favourites_count': 0, u'name': u'Twython', u'notifications': False, u'url': None, u'created_at': u'Thu May 16 01:11:09 +0000 2013', u'contributors_enabled': False, u'time_zone': None, u'protected': False, u'default_profile': False, u'is_translator': False}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'lang': u'en', u'created_at': u'Wed Jun 26 00:18:21 +0000 2013', u'in_reply_to_status_id_str': None, u'place': None}
test_tweet_html = 'google.com is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ github.com pic.twitter.com/N6InAO4B71'
+
+test_account_id = os.environ.get('TEST_ACCOUNT_ID')
+test_funding_instrument_id = os.environ.get('TEST_FUNDING_INSTRUMENT_ID')
+test_campaign_id = os.environ.get('TEST_CAMPAIGN_ID')
diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py
index aa79998e..28cf68b2 100644
--- a/tests/test_endpoints.py
+++ b/tests/test_endpoints.py
@@ -1,15 +1,17 @@
+import base64
+import datetime
+import urllib
+import time
from twython import Twython, TwythonError, TwythonAuthError
from .config import (
app_key, app_secret, oauth_token, oauth_token_secret,
protected_twitter_1, protected_twitter_2, screen_name,
test_tweet_id, test_list_slug, test_list_owner_screen_name,
- access_token, test_tweet_object, test_tweet_html, unittest
+ access_token, test_tweet_object, test_tweet_html, unittest,
+ test_account_id, test_funding_instrument_id, test_campaign_id
)
-import time
-
-
class TwythonEndpointsTestCase(unittest.TestCase):
def setUp(self):
@@ -531,3 +533,261 @@ def test_get_tos(self):
def test_get_application_rate_limit_status(self):
"""Test getting application rate limit status succeeds"""
self.oauth2_api.get_application_rate_limit_status()
+
+
+class TwythonEndpointsAdsTestCase(unittest.TestCase):
+ TEST_CAMPAIGN = {
+ 'name': 'Test Twitter campaign - Twython',
+ 'funding_instrument_id': test_funding_instrument_id,
+ 'start_time': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'daily_budget_amount_local_micro': 10 * 1000000,
+ 'paused': True
+ }
+
+ TEST_WEBSITE_CLICKS_LINE_ITEM = {
+ 'bid_type': 'MAX',
+ 'bid_amount_local_micro': 2000000,
+ 'product_type': 'PROMOTED_TWEETS',
+ 'placements': 'ALL_ON_TWITTER',
+ 'objective': 'WEBSITE_CLICKS',
+ 'paused': True
+ }
+
+ def setUp(self):
+ client_args = {
+ 'headers': {
+ 'User-Agent': '__twython__ Test'
+ },
+ 'allow_redirects': False
+ }
+
+ # This is so we can hit coverage that Twython sets
+ # User-Agent for us if none is supplied
+ oauth2_client_args = {
+ 'headers': {}
+ }
+
+ self.api = Twython(app_key, app_secret,
+ oauth_token, oauth_token_secret,
+ client_args=client_args)
+
+ self.oauth2_api = Twython(app_key, access_token=access_token,
+ client_args=oauth2_client_args)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_accounts(self):
+ accounts = self.api.get_accounts()
+ self.assertTrue(len(accounts) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_account(self):
+ account = self.api.get_account(test_account_id)
+ self.assertEqual(account['id'], test_account_id)
+ with self.assertRaises(TwythonError):
+ self.api.get_account('1234')
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_account_features(self):
+ account_features = self.api.get_account_features(test_account_id)
+ self.assertTrue(len(account_features) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_funding_instruments(self):
+ funding_instruments = self.api.get_funding_instruments(test_account_id)
+ self.assertTrue(len(funding_instruments) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_funding_instrument(self):
+ funding_instrument = self.api.get_funding_instrument(test_account_id, test_funding_instrument_id)
+ self.assertEqual(funding_instrument['id'], test_funding_instrument_id)
+ self.assertEqual(funding_instrument['account_id'], test_account_id)
+ with self.assertRaises(TwythonError):
+ self.api.get_funding_instrument('1234', '1234')
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_iab_categories(self):
+ iab_categories = self.api.get_iab_categories()
+ self.assertTrue(len(iab_categories) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_available_platforms(self):
+ available_platforms = self.api.get_available_platforms()
+ self.assertTrue(len(available_platforms) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_available_locations(self):
+ params = {
+ 'location_type': 'CITY',
+ 'country_code': 'US'
+ }
+ available_locations = self.api.get_available_locations(**params)
+ self.assertTrue(len(available_locations) > 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_campaigns(self):
+ campaigns = self.api.get_campaigns(test_account_id)
+ self.assertTrue(len(campaigns) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_create_and_delete_campaign(self):
+ campaign_id = self._create_test_campaign()
+ campaign_check = self.api.get_campaign(test_account_id, campaign_id)
+ self.assertEqual(campaign_check['id'], campaign_id)
+ self._delete_test_campaign(campaign_id)
+
+ def _create_test_campaign(self):
+ campaign = self.api.create_campaign(test_account_id, **self.TEST_CAMPAIGN)
+ campaign_id = campaign['id']
+ self.assertEqual(campaign['account_id'], test_account_id)
+ self.assertIsNotNone(campaign_id)
+ return campaign_id
+
+ def _delete_test_campaign(self, campaign_id):
+ is_deleted = self.api.delete_campaign(test_account_id, campaign_id)
+ self.assertTrue(is_deleted)
+
+ @unittest.skip('skipping non-updated test')
+ def test_create_and_delete_line_item(self):
+ campaign_id = self._create_test_campaign()
+ line_item_id = self._create_test_line_item(campaign_id)
+ line_items = self.api.get_line_items(test_account_id, campaign_id)
+ self.assertTrue(len(line_items) > 0)
+ self._delete_test_line_item(line_item_id)
+ self._delete_test_campaign(campaign_id)
+
+ def _create_test_line_item(self, campaign_id):
+ response = self.api.create_line_item(test_account_id, campaign_id, **self.TEST_WEBSITE_CLICKS_LINE_ITEM)
+ line_item_id = response['id']
+ self.assertEqual(response['account_id'], test_account_id)
+ self.assertEqual(response['campaign_id'], campaign_id)
+ self.assertIsNotNone(line_item_id)
+ return line_item_id
+
+ def _delete_test_line_item(self, line_item_id):
+ is_deleted = self.api.delete_line_item(test_account_id, line_item_id)
+ self.assertTrue(is_deleted)
+
+ @unittest.skip('skipping non-updated test')
+ def test_upload_image(self):
+ response = self._upload_test_image()
+ self.assertIsNotNone(response['media_id'])
+
+ def _upload_test_image(self):
+ image_file = urllib.urlopen('https://openclipart.org/image/800px/svg_to_png/190042/1389527622.png').read()
+ image_file_encoded = base64.b64encode(image_file)
+ upload_data = {
+ 'media_data': image_file_encoded
+ # the line below will have to be provided once we start uploading photos on behalf of advertisers
+ # 'additional_owners': ''
+ }
+ response = self.api.upload_image(**upload_data)
+ return response
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_website_cards(self):
+ response = self.api.get_website_cards(test_account_id)
+ self.assertTrue(len(response) >= 0)
+
+ @unittest.skip('skipping non-updated test')
+ def test_create_and_delete_website_card(self):
+ card_id = self._create_test_website_card()
+ card = self.api.get_website_card(test_account_id, card_id)
+ self.assertEqual(card['id'], card_id)
+ self._delete_test_website_card(card_id)
+
+ def _create_test_website_card(self):
+ uploaded_image = self._upload_test_image()
+ test_website_card = {
+ 'name': 'Zemanta Partnered with AdsNative for Programmatic Native Supply',
+ 'website_title': 'Zemanta Partnered with AdsNative for Programmatic Native Supply',
+ 'website_url': 'http://r1.zemanta.com/r/u1tllsoizjls/facebook/1009/92325/',
+ 'website_cta': 'READ_MORE',
+ 'image_media_id': uploaded_image['media_id_string']
+ }
+ response_create = self.api.create_website_card(test_account_id, **test_website_card)
+ card_id = response_create['id']
+ self.assertEqual(response_create['account_id'], test_account_id)
+ self.assertIsNotNone(card_id)
+ return card_id
+
+ def _delete_test_website_card(self, card_id):
+ response_delete = self.api.delete_website_card(test_account_id, card_id)
+ self.assertEqual(response_delete['id'], card_id)
+
+ @unittest.skip('skipping non-updated test')
+ def test_create_promoted_only_tweet(self):
+ card_id, tweet_id = self._create_test_promoted_only_tweet()
+ self._delete_test_website_card(card_id)
+
+ def _create_test_promoted_only_tweet(self):
+ card_id = self._create_test_website_card()
+ card = self.api.get_website_card(test_account_id, card_id)
+ test_promoted_only_tweet = {
+ 'status': 'This is test tweet for website card: %s' % card['preview_url'],
+ # 'as_user_id': '',
+ }
+ response = self.api.create_promoted_only_tweet(test_account_id, **test_promoted_only_tweet)
+ tweet_id = response['id']
+ self.assertIsNotNone(tweet_id)
+ return card_id, tweet_id
+
+ @unittest.skip('skipping non-updated test')
+ def test_promote_and_unpromote_tweet(self):
+ campaign_id = self._create_test_campaign()
+ line_item_id = self._create_test_line_item(campaign_id)
+ card_id, tweet_id = self._create_test_promoted_only_tweet()
+ test_tweet_promotion = {
+ 'line_item_id': line_item_id,
+ 'tweet_ids': [tweet_id]
+ }
+ result_promote = self.api.promote_tweet(test_account_id, **test_tweet_promotion)
+ self.assertTrue(len(result_promote) > 0)
+ self.assertEqual(int(result_promote[0]['tweet_id']), tweet_id)
+ promotion_id = result_promote[0]['id']
+ self.assertIsNotNone(promotion_id)
+ promoted_tweets = self.api.get_promoted_tweets(test_account_id, line_item_id)
+ self.assertTrue(len(promoted_tweets) == 1)
+ result_unpromotion = self.api.unpromote_tweet(test_account_id, promotion_id)
+ self.assertTrue(result_unpromotion['deleted'])
+ self.assertEqual(result_unpromotion['id'], promotion_id)
+ self._delete_test_campaign(campaign_id)
+ self._delete_test_website_card(card_id)
+
+ @unittest.skip('skipping non-updated test')
+ def test_add_targeting_criteria(self):
+ campaign_id = self._create_test_campaign()
+ line_item_id = self._create_test_line_item(campaign_id)
+ criteria_ios_id = self._create_test_targeting_criteria(line_item_id, 'PLATFORM', '0')
+ criteria_android_id = self._create_test_targeting_criteria(line_item_id, 'PLATFORM', '1')
+ criteria_desktop_id = self._create_test_targeting_criteria(line_item_id, 'PLATFORM', '4')
+ criteria_new_york_id = self._create_test_targeting_criteria(line_item_id, 'LOCATION', 'b6c2e04f1673337f')
+ # since all the targeting criteria share the same id, we only have to do the removal once.
+ self.api.remove_targeting_criteria(test_account_id, criteria_ios_id)
+ self.api.remove_targeting_criteria(test_account_id, criteria_android_id)
+ self.api.remove_targeting_criteria(test_account_id, criteria_desktop_id)
+ self.api.remove_targeting_criteria(test_account_id, criteria_new_york_id)
+ self._delete_test_line_item(line_item_id)
+ self._delete_test_campaign(campaign_id)
+
+ def _create_test_targeting_criteria(self, line_item_id, targeting_type, targeting_value):
+ test_targeting_criteria_ios = {
+ 'targeting_type': targeting_type,
+ 'targeting_value': targeting_value
+ }
+ response_add = self.api.add_targeting_criteria(test_account_id, line_item_id, **test_targeting_criteria_ios)
+ self.assertEqual(response_add['account_id'], test_account_id)
+ self.assertEquals(response_add['line_item_id'], line_item_id)
+ return response_add['id']
+
+ @unittest.skip('skipping non-updated test')
+ def test_get_stats_promoted_tweets(self):
+ line_items = self.api.get_line_items(test_account_id, test_campaign_id)
+ promoted_tweets = self.api.get_promoted_tweets(test_account_id, line_items[0]['id'])
+ promoted_ids = [tweet['id'] for tweet in promoted_tweets]
+ stats_query = {
+ 'start_time': '2015-10-29T00:00:00Z',
+ 'end_time': '2015-10-29T23:59:59Z',
+ 'granularity': 'TOTAL'
+ }
+ stats = self.api.get_stats_promoted_tweets(test_account_id, promoted_ids, **stats_query)
+ self.assertTrue(len(stats) >= 0)
diff --git a/twython/api.py b/twython/api.py
index 2ed50bc2..c4bd6f7d 100644
--- a/twython/api.py
+++ b/twython/api.py
@@ -5,8 +5,8 @@
~~~~~~~~~~~
This module contains functionality for access to core Twitter API calls,
-Twitter Authentication, and miscellaneous methods that are useful when
-dealing with the Twitter API
+Twitter Ads API calls, Twitter Authentication, and miscellaneous methods
+that are useful when dealing with the Twitter API
"""
import warnings
@@ -19,18 +19,19 @@
from . import __version__
from .advisory import TwythonDeprecationWarning
from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2
-from .endpoints import EndpointsMixin
+from .endpoints import EndpointsMixin, EndpointsAdsMixin
from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError
from .helpers import _transparent_params
+from .api_type import API_TYPE_TWITTER, API_TYPE_TWITTER_ADS
warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 >
-class Twython(EndpointsMixin, object):
+class Twython(EndpointsMixin, EndpointsAdsMixin, object):
def __init__(self, app_key=None, app_secret=None, oauth_token=None,
oauth_token_secret=None, access_token=None,
token_type='bearer', oauth_version=1, api_version='1.1',
- client_args=None, auth_endpoint='authenticate'):
+ api_ads_version='0', client_args=None, auth_endpoint='authenticate'):
"""Instantiates an instance of Twython. Takes optional parameters for
authentication and such (see below).
@@ -48,6 +49,8 @@ def __init__(self, app_key=None, app_secret=None, oauth_token=None,
Default: 1
:param api_version: (optional) Choose which Twitter API version to
use. Default: 1.1
+ :param api_ads_version: (optional) Choose which Twitter Ads API version to
+ use. Default: 0
:param client_args: (optional) Accepts some requests Session parameters
and some requests Request parameters.
@@ -64,7 +67,9 @@ def __init__(self, app_key=None, app_secret=None, oauth_token=None,
# API urls, OAuth urls and API version; needed for hitting that there
# API.
self.api_version = api_version
+ self.api_ads_version = api_ads_version
self.api_url = 'https://api.twitter.com/%s'
+ self.api_ads_url = 'https://ads-api.twitter.com/%s'
self.app_key = app_key
self.app_secret = app_secret
@@ -224,7 +229,7 @@ def _get_error_message(self, response):
return error_message
- def request(self, endpoint, method='GET', params=None, version='1.1'):
+ def request(self, endpoint, api_type=API_TYPE_TWITTER, method='GET', params=None, version='1.1'):
"""Return dict of response received from Twitter's API
:param endpoint: (required) Full url or Twitter API endpoint
@@ -251,6 +256,8 @@ def request(self, endpoint, method='GET', params=None, version='1.1'):
# i.e. https://api.twitter.com/1.1/search/tweets.json
if endpoint.startswith('https://'):
url = endpoint
+ elif api_type == API_TYPE_TWITTER_ADS:
+ url = '%s/%s' % (self.api_ads_url % version, endpoint)
else:
url = '%s/%s.json' % (self.api_url % version, endpoint)
@@ -259,13 +266,17 @@ def request(self, endpoint, method='GET', params=None, version='1.1'):
return content
- def get(self, endpoint, params=None, version='1.1'):
+ def get(self, endpoint, api_type=API_TYPE_TWITTER, params=None, version='1.1'):
"""Shortcut for GET requests via :class:`request`"""
- return self.request(endpoint, params=params, version=version)
+ return self.request(endpoint, api_type=api_type, params=params, version=version)
- def post(self, endpoint, params=None, version='1.1'):
+ def post(self, endpoint, api_type=API_TYPE_TWITTER, params=None, version='1.1'):
"""Shortcut for POST requests via :class:`request`"""
- return self.request(endpoint, 'POST', params=params, version=version)
+ return self.request(endpoint, api_type=api_type, method='POST', params=params, version=version)
+
+ def delete(self, endpoint, api_type=API_TYPE_TWITTER, params=None, version='1.1'):
+ """Shortcut for DELETE requests via :class:`request`"""
+ return self.request(endpoint, api_type=api_type, method='DELETE', params=params, version=version)
def get_lastfunction_header(self, header, default_return_value=None):
"""Returns a specific header from the last API call
diff --git a/twython/api_type.py b/twython/api_type.py
new file mode 100644
index 00000000..9778603d
--- /dev/null
+++ b/twython/api_type.py
@@ -0,0 +1,2 @@
+API_TYPE_TWITTER='api'
+API_TYPE_TWITTER_ADS='api_ads'
diff --git a/twython/endpoints.py b/twython/endpoints.py
index 561ec452..cdf1c377 100644
--- a/twython/endpoints.py
+++ b/twython/endpoints.py
@@ -1,19 +1,5 @@
# -*- coding: utf-8 -*-
-"""
-twython.endpoints
-~~~~~~~~~~~~~~~~~
-
-This module provides a mixin for a :class:`Twython ` instance.
-Parameters that need to be embedded in the API url just need to be passed
-as a keyword argument.
-
-e.g. Twython.retweet(id=12345)
-
-This map is organized the order functions are documented at:
-https://dev.twitter.com/docs/api/1.1
-"""
-
import os
import warnings
try:
@@ -22,9 +8,24 @@
from io import StringIO
from .advisory import TwythonDeprecationWarning
+from .api_type import API_TYPE_TWITTER_ADS
class EndpointsMixin(object):
+ """
+ twython.endpoints
+ ~~~~~~~~~~~~~~~~~
+
+ This module provides a mixin for a :class:`Twython ` instance.
+ Parameters that need to be embedded in the API url just need to be passed
+ as a keyword argument.
+
+ e.g. Twython.retweet(id=12345)
+
+ This map is organized the order functions are documented at:
+ https://dev.twitter.com/docs/api/1.1
+ """
+
# Timelines
def get_mentions_timeline(self, **params):
"""Returns the 20 most recent mentions (tweets containing a users's
@@ -1058,3 +1059,173 @@ def get_application_rate_limit_status(self, **params):
couldn\'t be serviced due to some failure within our stack. Try \
again later.'),
}
+
+
+class EndpointsAdsMixin(object):
+ """
+ twython.endpoints_ads
+ ~~~~~~~~~~~~~~~~~
+
+ This module adds Twitter Ads API support to the Twython library.
+ This module provides a mixin for a :class:`TwythonAds ` instance.
+ Parameters that need to be embedded in the API url just need to be passed
+ as a keyword argument.
+
+ e.g. TwythonAds.retweet(id=12345)
+
+ The API functions that are implemented in this module are documented at:
+ https://dev.twitter.com/ads/overview
+ """
+ def get_accounts(self, **params):
+ response = self.get('accounts', params=params, api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def get_account(self, account_id, **params):
+ response = self.get('accounts/%s' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_account_features(self, account_id, **params):
+ response = self.get('accounts/%s/features' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_funding_instruments(self, account_id, **params):
+ response = self.get('accounts/%s/funding_instruments' % account_id, params=params,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def get_funding_instrument(self, account_id, funding_instrument_id, **params):
+ response = self.get('accounts/%s/funding_instruments/%s' % (account_id, funding_instrument_id), params=params,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def get_iab_categories(self, **params):
+ response = self.get('iab_categories', params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_available_platforms(self, **params):
+ response = self.get('targeting_criteria/platforms', params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_available_locations(self, **params):
+ response = self.get('targeting_criteria/locations', params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_campaigns(self, account_id, **params):
+ response = self.get('accounts/%s/campaigns' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_campaign(self, account_id, campaign_id, **params):
+ response = self.get('accounts/%s/campaigns/%s' % (account_id, campaign_id), params=params,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def create_campaign(self, account_id, **params):
+ response = self.post('accounts/%s/campaigns' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def delete_campaign(self, account_id, campaign_id):
+ response = self.delete('accounts/%s/campaigns/%s' % (account_id, campaign_id), api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']['deleted']
+
+ def create_line_item(self, account_id, campaign_id, **params):
+ params_extended = params.copy()
+ params_extended['campaign_id'] = campaign_id
+ response = self.post('accounts/%s/line_items' % account_id, params=params_extended,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def delete_line_item(self, account_id, line_item_id):
+ response = self.delete('accounts/%s/line_items/%s' % (account_id, line_item_id), api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']['deleted']
+
+ def get_line_items(self, account_id, campaign_id=None, **params):
+ params_extended = params.copy()
+ if campaign_id is not None:
+ params_extended['campaign_ids'] = campaign_id
+ response = self.get('accounts/%s/line_items' % account_id, params=params_extended,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def get_website_cards(self, account_id, **params):
+ response = self.get('accounts/%s/cards/website' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def get_website_card(self, account_id, card_id, **params):
+ response = self.get('accounts/%s/cards/website/%s' % (account_id, card_id), params=params,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def create_website_card(self, account_id, **params):
+ # TODO: handle the case where name, website_title, website_url are too long!
+ response = self.post('accounts/%s/cards/website' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def delete_website_card(self, account_id, card_id, **params):
+ response = self.delete('accounts/%s/cards/website/%s' % (account_id, card_id), params=params,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def upload_image(self, **params):
+ response = self.post('https://upload.twitter.com/1.1/media/upload.json', params=params,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response
+
+ def create_promoted_only_tweet(self, account_id, **params):
+ response = self.post('accounts/%s/tweet' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def promote_tweet(self, account_id, **params):
+ response = self.post('accounts/%s/promoted_tweets' % account_id, params=params, api_type=API_TYPE_TWITTER_ADS,
+ version=self.api_ads_version)
+ return response['data']
+
+ def unpromote_tweet(self, account_id, promotion_id):
+ response = self.delete('accounts/%s/promoted_tweets/%s' % (account_id, promotion_id),
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def get_promoted_tweets(self, account_id, line_item_id=None, **params):
+ params_extended = params.copy()
+ if line_item_id is not None:
+ params_extended['line_item_id'] = line_item_id
+ response = self.get('accounts/%s/promoted_tweets' % account_id, params=params_extended,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def add_targeting_criteria(self, account_id, line_item_id, **params):
+ params_extended = params.copy()
+ params_extended['line_item_id'] = line_item_id
+ response = self.post('accounts/%s/targeting_criteria' % account_id, params=params_extended,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def remove_targeting_criteria(self, account_id, criteria_id):
+ response = self.delete('accounts/%s/targeting_criteria/%s' % (account_id, criteria_id),
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ return response['data']
+
+ def get_stats_promoted_tweets(self, account_id, promoted_tweet_ids, **params):
+ # the promoted_tweet_ids contains a list of up to 20 identifiers:
+ # https://dev.twitter.com/ads/reference/get/stats/accounts/%3Aaccount_id/promoted_tweets
+ stats = []
+ max_chunk_size = 20
+ for i in range(0, len(promoted_tweet_ids), max_chunk_size):
+ chunk = promoted_tweet_ids[i:i + max_chunk_size]
+ params_extended = params.copy()
+ params_extended['promoted_tweet_ids'] = ",".join(chunk)
+ response = self.get('stats/accounts/%s/promoted_tweets' % account_id, params=params_extended,
+ api_type=API_TYPE_TWITTER_ADS, version=self.api_ads_version)
+ stats.extend(response['data'])
+ return stats