From 26ef7b47bf041c4ec0aec7a44f7f7ff3c923e4dc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 21 Jun 2013 18:39:45 -0400 Subject: [PATCH 001/204] client_args to modify the request --- twython/streaming/api.py | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 244a550..c2fcec8 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -20,7 +20,8 @@ import time class TwythonStreamer(object): def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, - timeout=300, retry_count=None, retry_in=10, headers=None, handlers=None): + timeout=300, retry_count=None, retry_in=10, client_args=None, + handlers=None): """Streaming class for a friendly streaming user experience Authentication IS required to use the Twitter Streaming API @@ -36,8 +37,9 @@ class TwythonStreamer(object): retired :param retry_in: (optional) Amount of time (in secs) the previous API call should be tried again - :param headers: (optional) Custom headers to send along with the - request + :param client_args: (optional) Accepts some requests Session parameters and some requests Request parameters. + See http://docs.python-requests.org/en/latest/api/#sessionapi and requests section below it for details. + [ex. headers, proxies, verify(SSL verification)] :param handlers: (optional) Array of message types for which corresponding handlers will be called """ @@ -45,16 +47,28 @@ class TwythonStreamer(object): self.auth = OAuth1(app_key, app_secret, oauth_token, oauth_token_secret) - self.headers = {'User-Agent': 'Twython Streaming v' + __version__} - if headers: - self.headers.update(headers) + self.client_args = client_args or {} + default_headers = {'User-Agent': 'Twython Streaming v' + __version__} + if not 'headers' in self.client_args: + # If they didn't set any headers, set our defaults for them + self.client_args['headers'] = default_headers + elif 'User-Agent' not in self.client_args['headers']: + # If they set headers, but didn't include User-Agent.. set it for them + self.client_args['headers'].update(default_headers) + self.client_args['timeout'] = timeout self.client = requests.Session() self.client.auth = self.auth - self.client.headers = self.headers self.client.stream = True - self.timeout = timeout + # Make a copy of the client args and iterate over them + # Pop out all the acceptable args at this point because they will + # Never be used again. + client_args_copy = self.client_args.copy() + for k, v in client_args_copy.items(): + if k in ('cert', 'headers', 'hooks', 'max_redirects', 'proxies'): + setattr(self.client, k, v) + self.client_args.pop(k) # Pop, pop! self.api_version = '1.1' @@ -80,12 +94,20 @@ class TwythonStreamer(object): func = getattr(self.client, method) def _send(retry_counter): + requests_args = {} + for k, v in self.client_args.items(): + # Maybe this should be set as a class variable and only done once? + if k in ('timeout', 'allow_redirects', 'verify'): + requests_args[k] = v + while self.connected: try: if method == 'get': - response = func(url, params=params, timeout=self.timeout) + requests_args['params'] = params else: - response = func(url, data=params, timeout=self.timeout) + requests_args['data'] = params + + response = func(url, **requests_args) except requests.exceptions.Timeout: self.on_timeout() else: -- 2.39.5 From 6511dcf4bd6ea5080ee8051386b9c4a9132a4c10 Mon Sep 17 00:00:00 2001 From: Jesse Panganiban Date: Mon, 24 Jun 2013 04:12:06 +0800 Subject: [PATCH 002/204] Pick-up streamed data < 512 (ie. first streamed tweet) --- twython/streaming/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 244a550..8135715 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -102,7 +102,8 @@ class TwythonStreamer(object): while self.connected: response = _send(retry_counter) - for line in response.iter_lines(): + # So that first tweets get picked-up. + for line in response.iter_lines(chunk_size=1): if not self.connected: break if line: -- 2.39.5 From 4fb410fdeb8f4a620bef94a60d6348c129cd16dd Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Tue, 25 Jun 2013 20:19:33 +0200 Subject: [PATCH 003/204] wrap requests exceptions in TwythonError class --- twython/api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index a133590..f4a7a0b 100644 --- a/twython/api.py +++ b/twython/api.py @@ -9,6 +9,8 @@ Twitter Authentication, and miscellaneous methods that are useful when dealing with the Twitter API """ +import sys + import requests from requests.auth import HTTPBasicAuth from requests_oauthlib import OAuth1, OAuth2 @@ -131,7 +133,10 @@ class Twython(EndpointsMixin, object): 'data': params, 'files': files, }) - response = func(url, **requests_args) + try: + response = func(url, **requests_args) + except requests.RequestException as e: + raise TwythonError, str(e), sys.exc_info()[2] content = response.content.decode('utf-8') # create stash for last function intel -- 2.39.5 From 6ca284cc2111b8977b6039495c83d211f45ffb29 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 25 Jun 2013 15:39:17 -0300 Subject: [PATCH 004/204] Update streaming example [ci skip] --- examples/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/stream.py b/examples/stream.py index a571711..0ff5c04 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -16,5 +16,5 @@ stream = MyStreamer(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) stream.statuses.filter(track='twitter') -#stream.user(track='twitter') +#stream.user() # Read the authenticated users home timeline (what they see on Twitter) in real-time #stream.site(follow='twitter') -- 2.39.5 From 6cab7bf95d8cfb0442341b7f908f2d7d26387c30 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 25 Jun 2013 16:31:12 -0400 Subject: [PATCH 005/204] Fix Python 3 compat [ci skip] --- twython/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index f4a7a0b..d265186 100644 --- a/twython/api.py +++ b/twython/api.py @@ -9,8 +9,6 @@ Twitter Authentication, and miscellaneous methods that are useful when dealing with the Twitter API """ -import sys - import requests from requests.auth import HTTPBasicAuth from requests_oauthlib import OAuth1, OAuth2 @@ -136,7 +134,7 @@ class Twython(EndpointsMixin, object): try: response = func(url, **requests_args) except requests.RequestException as e: - raise TwythonError, str(e), sys.exc_info()[2] + raise TwythonError(str(e)) content = response.content.decode('utf-8') # create stash for last function intel -- 2.39.5 From 173adee4a68b3a01292a3878e7fc33fa9622ee32 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 25 Jun 2013 21:58:10 -0400 Subject: [PATCH 006/204] HTML for tweet fixes #224 --- twython/api.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/twython/api.py b/twython/api.py index d265186..c67868f 100644 --- a/twython/api.py +++ b/twython/api.py @@ -407,3 +407,41 @@ class Twython(EndpointsMixin, object): if is_py2 and isinstance(text, (str)): return Twython.unicode2utf8(text) return str(text) + + @staticmethod + def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False): + if 'retweeted_status' in tweet: + tweet = tweet['retweeted_status'] + + if 'entities' in tweet: + text = tweet['text'] + entities = tweet['entities'] + + # Mentions + for entity in entities['user_mentions']: + start, end = entity['indices'][0], entity['indices'][1] + + mention_html = '@%(screen_name)s' + text = text.replace(tweet['text'][start:end], mention_html % {'url': entity['screen_name']}) + + # Hashtags + for entity in entities['hashtags']: + start, end = entity['indices'][0], entity['indices'][1] + + hashtag_html = '#%(hashtag)s' + text = text.replace(tweet['text'][start:end], hashtag_html % {'hashtag': entity['text']}) + + # Urls + for entity in entities['urls']: + start, end = entity['indices'][0], entity['indices'][1] + if use_display_url and entity.get('display_url'): + shown_url = entity['display_url'] + elif use_expanded_url and entity.get('expanded_url'): + shown_url = entity['expanded_url'] + else: + shown_url = entity['url'] + + url_html = '%s' + text = text.replace(tweet['text'][start:end], url_html % (entity['url'], shown_url)) + + return text -- 2.39.5 From 3c637ddc7d19cd47cc3432892cfe72b067de57c1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 27 Jun 2013 19:20:43 -0400 Subject: [PATCH 007/204] Tests and documentation --- docs/index.rst | 1 + docs/usage/special_functions.rst | 47 ++++++++++++++++++++++++++++++++ docs/usage/starting_out.rst | 1 + tests/config.py | 3 ++ tests/test_core.py | 8 +++++- twython/api.py | 14 ++++++++-- 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 docs/usage/special_functions.rst diff --git a/docs/index.rst b/docs/index.rst index 5fa8848..e971e45 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,6 +37,7 @@ Usage usage/basic_usage usage/advanced_usage usage/streaming_api + usage/special_functions Twython API Documentation ------------------------- diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst new file mode 100644 index 0000000..3d791b6 --- /dev/null +++ b/docs/usage/special_functions.rst @@ -0,0 +1,47 @@ +.. special-functions: + +Special Functions +================= + +This section covers methods to are part of Twython but not necessarily connected to the Twitter API. + +******************************************************************************* + +HTML for Tweet +-------------- + +This function takes a tweet object received from the Twitter API and returns an string formatted in HTML with the links, user mentions and hashtags replaced. + +.. code-block:: python + + from twython import Twython + + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + user_tweets = twitter.get_user_timeline(screen_name='mikehelmick', + include_rts=True) + for tweet in user_tweets: + tweet['text'] = Twython.html_for_tweet(tweet) + print tweet['text'] + +The above code takes all the tweets from a specific users timeline, loops over them and replaces the value of ``tweet['text']`` with HTML. + +So: + + http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ https://t.co/67pwRvY6z9 + +will be replaced with: + + google.com is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ github.com + +.. note:: When converting the string to HTML we add a class to each HTML tag so that you can maninpulate the DOM later on. + +- For urls that are replaced we add ``class="twython-url"`` to the anchor tag +- For user mentions that are replaced we add ``class="twython-mention"`` to the anchor tag +- For hashtags that are replaced we add ``class="twython-hashtag"`` to the anchor tag + +This function excepts two parameters: ``use_display_url`` and ``use_expanded_url`` +By default, ``use_display_url`` is ``True``. Meaning the link displayed in the tweet text will appear as (ex. google.com, github.com) +If ``use_expanded_url`` is ``True``, it overrides ``use_display_url``. The urls will then be displayed as (ex. http://google.com, https://github.com) +If ``use_display_url`` and ``use_expanded_url`` is ``False``, short url will be used (t.co/xxxxx) \ No newline at end of file diff --git a/docs/usage/starting_out.rst b/docs/usage/starting_out.rst index 076e9fa..15a5dc8 100644 --- a/docs/usage/starting_out.rst +++ b/docs/usage/starting_out.rst @@ -81,6 +81,7 @@ You'll want to extract the ``oauth_verifier`` from the url. Django example: .. code-block:: python + oauth_verifier = request.GET['oauth_verifier'] Now that you have the ``oauth_verifier`` stored to a variable, you'll want to create a new instance of Twython and grab the final user tokens diff --git a/tests/config.py b/tests/config.py index f927d04..af098cd 100644 --- a/tests/config.py +++ b/tests/config.py @@ -17,3 +17,6 @@ protected_twitter_2 = os.environ.get('PROTECTED_TWITTER_2', 'TwythonSecure2') # Test Ids test_tweet_id = os.environ.get('TEST_TWEET_ID', '318577428610031617') test_list_id = os.environ.get('TEST_LIST_ID', '574') # 574 is @twitter/team + +test_tweet_object = {u'contributors': None, u'truncated': False, u'text': u'http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ https://t.co/67pwRvY6z9', 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': [104, 116], 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': [65, 76], u'text': u'checkitout'}, {u'indices': [90, 97], u'text': u'thanks'}], 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': [117, 140], u'expanded_url': u'https://github.com', u'display_url': u'github.com'}]}, 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 should #checkitout. If you can! #thanks Love, @__twython__ github.com' diff --git a/tests/test_core.py b/tests/test_core.py index d4ddfb3..1eff01e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,7 +3,8 @@ 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_id, access_token + test_tweet_id, test_list_id, access_token, test_tweet_object, + test_tweet_html ) import time @@ -82,6 +83,11 @@ class TwythonAPITestCase(unittest.TestCase): """Test encoding UTF-8 works""" self.api.encode('Twython is awesome!') + def test_html_for_tweet(self): + """Test HTML for Twitter returns what we want""" + tweet_text = self.api.html_for_tweet(test_tweet_object) + self.assertEqual(test_tweet_html, tweet_text) + # Timelines def test_get_mentions_timeline(self): """Test returning mentions timeline for authenticated user succeeds""" diff --git a/twython/api.py b/twython/api.py index c67868f..c4dd59a 100644 --- a/twython/api.py +++ b/twython/api.py @@ -410,6 +410,16 @@ class Twython(EndpointsMixin, object): @staticmethod def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False): + """Return HTML for a tweet (urls, mentions, hashtags replaced with links) + + :param tweet: Tweet object from received from Twitter API + :param use_display_url: Use display URL to represent link (ex. google.com, github.com). Default: True + :param use_expanded_url: Use expanded URL to represent link (e.g. http://google.com). Default False + + If use_expanded_url is True, it overrides use_display_url. + If use_display_url and use_expanded_url is False, short url will be used (t.co/xxxxx) + + """ if 'retweeted_status' in tweet: tweet = tweet['retweeted_status'] @@ -422,7 +432,7 @@ class Twython(EndpointsMixin, object): start, end = entity['indices'][0], entity['indices'][1] mention_html = '@%(screen_name)s' - text = text.replace(tweet['text'][start:end], mention_html % {'url': entity['screen_name']}) + text = text.replace(tweet['text'][start:end], mention_html % {'screen_name': entity['screen_name']}) # Hashtags for entity in entities['hashtags']: @@ -434,7 +444,7 @@ class Twython(EndpointsMixin, object): # Urls for entity in entities['urls']: start, end = entity['indices'][0], entity['indices'][1] - if use_display_url and entity.get('display_url'): + if use_display_url and entity.get('display_url') and not use_expanded_url: shown_url = entity['display_url'] elif use_expanded_url and entity.get('expanded_url'): shown_url = entity['expanded_url'] -- 2.39.5 From acdf73a04e574e3c131b8cbaea90e74efe5cc18e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 27 Jun 2013 22:37:02 -0400 Subject: [PATCH 008/204] More tests, coverage, and excluding lines from being covered There are some lines that will never be hit in tests, excluding those from being covered! --- tests/test_auth.py | 7 +++++++ tests/test_core.py | 20 +++++++++++++++++++- tests/test_streaming.py | 10 ++++++++++ twython/api.py | 10 +++++----- twython/endpoints.py | 4 ++-- twython/streaming/api.py | 4 ++-- 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index ecdd4dc..12f54b2 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -11,6 +11,8 @@ class TwythonAuthTestCase(unittest.TestCase): self.bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET') self.oauth2_api = Twython(app_key, app_secret, oauth_version=2) + self.oauth2_bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET', + oauth_version=2) def test_get_authentication_tokens(self): """Test getting authentication tokens works""" @@ -44,6 +46,11 @@ class TwythonAuthTestCase(unittest.TestCase): """Test obtaining an Application Only OAuth 2 access token succeeds""" self.oauth2_api.obtain_access_token() + def test_obtain_access_token_bad_tokens(self): + """Test obtaining an Application Only OAuth 2 access token using bad app tokens fails""" + self.assertRaises(TwythonAuthError, + self.oauth2_bad_api.obtain_access_token) + def test_obtain_access_token_raises_error_when_oauth1(self): """Test when API is set for OAuth 1, obtain_access_token raises a TwythonError""" diff --git a/tests/test_core.py b/tests/test_core.py index 1eff01e..5466160 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -84,10 +84,28 @@ class TwythonAPITestCase(unittest.TestCase): self.api.encode('Twython is awesome!') def test_html_for_tweet(self): - """Test HTML for Twitter returns what we want""" + """Test HTML for Tweet returns what we want""" tweet_text = self.api.html_for_tweet(test_tweet_object) self.assertEqual(test_tweet_html, tweet_text) + def test_html_for_tweet_expanded_url(self): + """Test using expanded url in HTML for Tweet displays full urls""" + tweet_text = self.api.html_for_tweet(test_tweet_object, + use_expanded_url=True) + # Make sure full url is in HTML + self.assertTrue('http://google.com' in tweet_text) + + def test_html_for_tweet_short_url(self): + """Test using expanded url in HTML for Tweet displays full urls""" + tweet_text = self.api.html_for_tweet(test_tweet_object, False) + # Make sure HTML doesn't contain the display OR exapanded url + self.assertTrue(not 'http://google.com' in tweet_text) + self.assertTrue(not 'google.com' in tweet_text) + + def test_raise_error_on_bad_ssl_cert(self): + """Test TwythonError is raised by a RequestException when an actual HTTP happens""" + self.assertRaises(TwythonError, self.api.get, 'https://example.com') + # Timelines def test_get_mentions_timeline(self): """Test returning mentions timeline for authenticated user succeeds""" diff --git a/tests/test_streaming.py b/tests/test_streaming.py index a75d98c..1b8db6c 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -19,6 +19,16 @@ class TwythonStreamTestCase(unittest.TestCase): self.api = MyStreamer(app_key, app_secret, oauth_token, oauth_token_secret) + client_args = { + 'headers': { + 'User-Agent': '__twython__ Stream Test' + } + } + # Initialize with header for coverage checking for User-Agent + self.api_with_header = MyStreamer(app_key, app_secret, + oauth_token, oauth_token_secret, + client_args=client_args) + def test_stream_status_filter(self): self.api.statuses.filter(track='twitter') diff --git a/twython/api.py b/twython/api.py index c4dd59a..b1706f2 100644 --- a/twython/api.py +++ b/twython/api.py @@ -169,13 +169,13 @@ class Twython(EndpointsMixin, object): if errors and isinstance(errors, list): error_message = errors[0]['message'] else: - error_message = errors + error_message = errors # pragma: no cover self._last_call['api_error'] = error_message ExceptionType = TwythonError if response.status_code == 429: # Twitter API 1.1, always return 429 when rate limit is exceeded - ExceptionType = TwythonRateLimitError + ExceptionType = TwythonRateLimitError # pragma: no cover elif response.status_code == 401 or 'Bad Authentication data' in error_message: # Twitter API 1.1, returns a 401 Unauthorized or # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens @@ -186,7 +186,7 @@ class Twython(EndpointsMixin, object): retry_after=response.headers.get('retry-after')) # if we have a json error here, then it's not an official Twitter API error - if json_error and not response.status_code in (200, 201, 202): + if json_error and not response.status_code in (200, 201, 202): # pragma: no cover raise TwythonError('Response was not valid JSON, unable to decode.') return content @@ -303,7 +303,7 @@ class Twython(EndpointsMixin, object): if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') - return authorized_tokens + return authorized_tokens # pragma: no cover def obtain_access_token(self): """Returns an OAuth 2 access token to make OAuth 2 authenticated read-only calls. @@ -387,7 +387,7 @@ class Twython(EndpointsMixin, object): try: if not 'since_id' in params: params['since_id'] = (int(content['statuses'][0]['id_str']) + 1) - except (TypeError, ValueError): + except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search results, `page` is not a number.') for tweet in self.search_gen(search_query, **params): diff --git a/twython/endpoints.py b/twython/endpoints.py index 4c34188..21a64d0 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -748,7 +748,7 @@ class EndpointsMixin(object): return self.get('trends/closest', params=params) # Spam Reporting - def report_spam(self, **params): + def report_spam(self, **params): # pragma: no cover """Report the specified user as a spam account to Twitter. Docs: https://dev.twitter.com/docs/api/1.1/post/users/report_spam @@ -757,7 +757,7 @@ class EndpointsMixin(object): return self.post('users/report_spam', params=params) # OAuth - def invalidate_token(self, **params): + def invalidate_token(self, **params): # pragma: no cover """Allows a registered application to revoke an issued OAuth 2 Bearer Token by presenting its client credentials. diff --git a/twython/streaming/api.py b/twython/streaming/api.py index c2fcec8..ef8924a 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -132,13 +132,13 @@ class TwythonStreamer(object): if is_py3: line = line.decode('utf-8') data = json.loads(line) - if self.on_success(data): + if self.on_success(data): # pragma: no cover for message_type in self.handlers: if message_type in data: handler = getattr(self, 'on_' + message_type, None) if handler and callable(handler) and not handler(data.get(message_type)): break - except ValueError: + except ValueError: # pragma: no cover self.on_error(response.status_code, 'Unable to decode response, not valid JSON.') response.close() -- 2.39.5 From f77d9eb523068132479c3dbd9a9e38a84d68af56 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 27 Jun 2013 22:47:07 -0400 Subject: [PATCH 009/204] If bad tokens for oauth 2 obtain token, 'access_token' is never in the dict --- twython/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index b1706f2..a56b9eb 100644 --- a/twython/api.py +++ b/twython/api.py @@ -324,7 +324,7 @@ class Twython(EndpointsMixin, object): except AttributeError: content = json.loads(content) access_token = content['access_token'] - except (ValueError, requests.exceptions.RequestException): + except (KeyError, ValueError, requests.exceptions.RequestException): raise TwythonAuthError('Unable to obtain OAuth 2 access token.') else: return access_token -- 2.39.5 From d37614cb0229552b9b4223c1dd005a0189e82fa1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 27 Jun 2013 22:57:27 -0400 Subject: [PATCH 010/204] Update HISTORY.rst [ci skip] --- HISTORY.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 9fdc5f2..1d31f01 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ History ------- +3.1.0 (2013-xx-xx) +++++++++++++++++++ + +- Added ``html_for_tweet`` static method. This method accepts a tweet object returned from a Twitter API call and will return a string with urls, mentions and hashtags in the tweet replaced with HTML. +- Pass ``client_args`` to the streaming ``__init__``, much like in core Twython (you can pass headers, timeout, hooks, proxies, etc.). +- Streamer has new parameter ``handlers`` which accepts a list of strings related to functions that are apart of the Streaming class and start with "on\_". i.e. ['delete'] is passed, when 'delete' is received from a stream response; ``on_delete`` will be called. +- When an actual request error happens and a ``RequestException`` is raised, it is caught and a ``TwythonError`` is raised instead for convenience. + 3.0.0 (2013-06-18) ++++++++++++++++++ -- 2.39.5 From b983d154eb1687b09afd10096d5d4462313f9003 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 27 Jun 2013 23:02:48 -0400 Subject: [PATCH 011/204] Update AUTHORS.rst [ci skip] --- AUTHORS.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index c46b4da..021339c 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,7 +18,7 @@ Patches and Suggestions - `Erik Scheffers `_, various fixes regarding OAuth callback URLs. - `Jordan Bouvier `_, various fixes regarding OAuth callback URLs. - `Dick Brouwer `_, fixes for OAuth Verifier in ``get_authorized_tokens``. -- `hades `_, Fixes to various initial OAuth issues and keeping ``Twython3k`` up-to-date. +- `hades `_, Fixes to various initial OAuth issues, raise TwythonError on request error. - `Alex Sutton `_, fix for parameter substitution regular expression (catch underscores!). - `Levgen Pyvovarov `_, Various argument fixes, cyrillic text support. - `Mark Liu `_, Missing parameter fix for ``addListMember``. @@ -41,3 +41,4 @@ Patches and Suggestions - `Jonathan Vanasco `_, Debugging support, error_code tracking, Twitter error API tracking, other fixes - `DevDave `_, quick fix for longs with helper._transparent_params - `Ruben Varela Rosa `_, Fixed search example +- `Oleg Anashkin `_, streaming ``handlers`` functionality -- 2.39.5 From 3a198653ebc446273d30c02089ac1ab2b3b9d3d0 Mon Sep 17 00:00:00 2001 From: Steve Hulet Date: Mon, 1 Jul 2013 11:56:20 -0700 Subject: [PATCH 012/204] typo fix --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 40f1e8e..1e09fc5 100644 --- a/README.rst +++ b/README.rst @@ -89,7 +89,7 @@ Now, you'll want to create a Twython instance with your ``Consumer Key`` and ``C .. code-block:: python APP_KEY = 'YOUR_APP_KEY' - APP_SECET = 'YOUR_APP_SECRET' + APP_SECRET = 'YOUR_APP_SECRET' twitter = Twython(APP_KEY, APP_SECRET) -- 2.39.5 From 66237c78712cbbbbfcee0a420039fc7f761fd1e1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 1 Jul 2013 15:00:43 -0400 Subject: [PATCH 013/204] Update starting_out.rst [ci skip] --- docs/usage/starting_out.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/starting_out.rst b/docs/usage/starting_out.rst index 15a5dc8..9678fcf 100644 --- a/docs/usage/starting_out.rst +++ b/docs/usage/starting_out.rst @@ -51,7 +51,7 @@ Obtain Authorization URL .. code-block:: python APP_KEY = 'YOUR_APP_KEY' - APP_SECET = 'YOUR_APP_SECRET' + APP_SECRET = 'YOUR_APP_SECRET' twitter = Twython(APP_KEY, APP_SECRET) auth = twitter.get_authentication_tokens(callback_url='http://mysite.com/callback') -- 2.39.5 From 1f9afbe02c62d70fa4bca05f54a9e336bd7a435f Mon Sep 17 00:00:00 2001 From: Luis Alberto Santana Date: Wed, 17 Jul 2013 15:52:38 -0430 Subject: [PATCH 014/204] Added a new param to the Twython __init__ that lets you select the authentication endpoint for your application. --- twython/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/twython/api.py b/twython/api.py index a56b9eb..7c63444 100644 --- a/twython/api.py +++ b/twython/api.py @@ -23,7 +23,7 @@ from .helpers import _transparent_params class Twython(EndpointsMixin, 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): + oauth_version=1, api_version='1.1', client_args=None, auth_endpoint='authenticate'): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -38,6 +38,9 @@ class Twython(EndpointsMixin, object): :param client_args: (optional) Accepts some requests Session parameters and some requests Request parameters. See http://docs.python-requests.org/en/latest/api/#sessionapi and requests section below it for details. [ex. headers, proxies, verify(SSL verification)] + :param auth_endpoint: (optional) Lets you select which authentication endpoint will use your application. + This will allow the application to have DM access if the endpoint is 'authorize'. + Default: authenticate. """ @@ -54,7 +57,7 @@ class Twython(EndpointsMixin, object): # OAuth 1 self.request_token_url = self.api_url % 'oauth/request_token' self.access_token_url = self.api_url % 'oauth/access_token' - self.authenticate_url = self.api_url % 'oauth/authenticate' + self.authenticate_url = self.api_url % ('oauth/%s' % auth_endpoint) if self.access_token: # If they pass an access token, force OAuth 2 oauth_version = 2 -- 2.39.5 From 123e023dbb48dd3633845dd8eab94230069326ba Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 20:26:03 -0400 Subject: [PATCH 015/204] Update requirements [ci skip] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b7897fb..9dc3247 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage==3.6.0 -requests==1.2.2 +requests==1.2.3 requests_oauthlib==0.3.2 python-coveralls==2.1.0 nose-cov==1.6 -- 2.39.5 From ebd5cd47dd1e594f4bc09a2ed925617a7565cd6d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 20:31:56 -0400 Subject: [PATCH 016/204] General cursor (generator) like object for Twython functions * If the endpoint has an iter_mode, the function is able to be used in Twython.cursor * Deprecate Twython.search_gen Fixes #238 --- HISTORY.rst | 2 ++ twython/api.py | 64 ++++++++++++++++++++++++++++++++++++++------ twython/endpoints.py | 9 +++++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1d31f01..4d60acb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,6 +10,8 @@ History - Pass ``client_args`` to the streaming ``__init__``, much like in core Twython (you can pass headers, timeout, hooks, proxies, etc.). - Streamer has new parameter ``handlers`` which accepts a list of strings related to functions that are apart of the Streaming class and start with "on\_". i.e. ['delete'] is passed, when 'delete' is received from a stream response; ``on_delete`` will be called. - When an actual request error happens and a ``RequestException`` is raised, it is caught and a ``TwythonError`` is raised instead for convenience. +- Added "cursor"-like functionality. Endpoints with the attribute ``iter_mode`` will be able to be passed to ``Twython.cursor`` and returned as a generator. +- ``Twython.search_gen`` has been deprecated. Please use ``twitter.cursor(twitter.search, q='your_query')`` instead, where ``twitter`` is your ``Twython`` instance. 3.0.0 (2013-06-18) ++++++++++++++++++ diff --git a/twython/api.py b/twython/api.py index a56b9eb..f9271ca 100644 --- a/twython/api.py +++ b/twython/api.py @@ -14,11 +14,16 @@ from requests.auth import HTTPBasicAuth from requests_oauthlib import OAuth1, OAuth2 from . import __version__ +from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 from .endpoints import EndpointsMixin from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .helpers import _transparent_params +import warnings + +warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > + class Twython(EndpointsMixin, object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, @@ -376,22 +381,65 @@ class Twython(EndpointsMixin, object): >>> print result """ - content = self.search(q=search_query, **params) + warnings.warn( + 'This method is deprecated. You should use Twython.cursor instead. [eg. Twython.cursor(Twython.search, q=\'your_query\')]', + TwythonDeprecationWarning, + stacklevel=2 + ) + return self.cursor(self.search, q=search_query, **params) - if not content.get('statuses'): + def cursor(self, function, **params): + """Returns a generator for results that match a specified query. + + :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) + :param \*\*params: Extra parameters to send with your request (usually parameters excepted by the Twitter API endpoint) + :rtype: generator + + Usage:: + + >>> from twython import Twython + >>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + >>> results = twitter.cursor(twitter.search, q='python') + >>> for result in results: + >>> print result + + """ + if not hasattr(function, 'iter_mode'): + raise TwythonError('Unable to create generator for Twython method "%s"' % function.__name__) + + content = function(**params) + + if not content: raise StopIteration - for tweet in content['statuses']: - yield tweet + if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': + raise StopIteration + + if hasattr(function, 'iter_key'): + results = content.get(function.iter_key) + else: + results = content + + for result in results: + yield result try: - if not 'since_id' in params: - params['since_id'] = (int(content['statuses'][0]['id_str']) + 1) + if function.iter_mode == 'id': + if not 'max_id' in params: + # Add 1 to the id because since_id and max_id are inclusive + if hasattr(function, 'iter_metadata'): + since_id = content[function.iter_metadata].get('since_id_str') + else: + since_id = content[0]['id_str'] + params['since_id'] = (int(since_id) - 1) + elif function.iter_mode == 'cursor': + params['cursor'] = content['next_cursor_str'] except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - for tweet in self.search_gen(search_query, **params): - yield tweet + for result in self.cursor(function, **params): + yield result @staticmethod def unicode2utf8(text): diff --git a/twython/endpoints.py b/twython/endpoints.py index 21a64d0..247d40d 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -24,6 +24,7 @@ class EndpointsMixin(object): """ return self.get('statuses/mentions_timeline', params=params) + get_mentions_timeline.iter_mode = 'id' def get_user_timeline(self, **params): """Returns a collection of the most recent Tweets posted by the user @@ -33,6 +34,7 @@ class EndpointsMixin(object): """ return self.get('statuses/user_timeline', params=params) + get_user_timeline.iter_mode = 'id' def get_home_timeline(self, **params): """Returns a collection of the most recent Tweets and retweets @@ -42,6 +44,7 @@ class EndpointsMixin(object): """ return self.get('statuses/home_timeline', params=params) + get_home_timeline.iter_mode = 'id' def retweeted_of_me(self, **params): """Returns the most recent tweets authored by the authenticating user @@ -51,6 +54,7 @@ class EndpointsMixin(object): """ return self.get('statuses/retweets_of_me', params=params) + retweeted_of_me.iter_mode = 'id' # Tweets def get_retweets(self, **params): @@ -128,6 +132,9 @@ class EndpointsMixin(object): """ return self.get('search/tweets', params=params) + search.iter_mode = 'id' + search.iter_key = 'statuses' + search.iter_metadata = 'search_metadata' # Direct Messages def get_direct_messages(self, **params): @@ -268,7 +275,9 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/docs/api/1.1/get/friends/list """ + print 'here 1' return self.get('friends/list', params=params) + get_friends_list.iterator_mode = 'cursor' def get_followers_list(self, **params): """Returns a cursored collection of user objects for users -- 2.39.5 From c7af673ad66d02904146971f3f58f094c3a82517 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 20:32:58 -0400 Subject: [PATCH 017/204] Remove print :blush: --- twython/endpoints.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 247d40d..20d42d3 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -275,7 +275,6 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/docs/api/1.1/get/friends/list """ - print 'here 1' return self.get('friends/list', params=params) get_friends_list.iterator_mode = 'cursor' -- 2.39.5 From 5d7ebcbdfcf0266fa5b248337706f1223c4c6640 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 20:34:35 -0400 Subject: [PATCH 018/204] Revert "Update requirements" This reverts commit 123e023dbb48dd3633845dd8eab94230069326ba. [ci skip] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9dc3247..b7897fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage==3.6.0 -requests==1.2.3 +requests==1.2.2 requests_oauthlib==0.3.2 python-coveralls==2.1.0 nose-cov==1.6 -- 2.39.5 From 2dc15b703050bb9ba261cf13f497d96597cd3c4c Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 20:43:02 -0400 Subject: [PATCH 019/204] Fix generator test, remove search_gen from coverage --- tests/test_core.py | 4 ++-- twython/api.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 5466160..1de68f1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -65,9 +65,9 @@ class TwythonAPITestCase(unittest.TestCase): self.assertRaises(TwythonError, self.api.get_lastfunction_header, 'no-api-call-was-made') - def test_search_gen(self): + def test_cursor(self): """Test looping through the generator results works, at least once that is""" - search = self.api.search_gen('twitter', count=1) + search = self.api.cursor(self.api.search, q='twitter', count=1) counter = 0 while counter < 2: counter += 1 diff --git a/twython/api.py b/twython/api.py index f9271ca..6a99a7d 100644 --- a/twython/api.py +++ b/twython/api.py @@ -362,7 +362,7 @@ class Twython(EndpointsMixin, object): ) return '%s?%s' % (api_url, '&'.join(querystring)) - def search_gen(self, search_query, **params): + def search_gen(self, search_query, **params): # pragma: no cover """Returns a generator of tweets that match a specified query. Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets -- 2.39.5 From a1ee2a93ad654a463879831331663e26fe2d1638 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 20:57:31 -0400 Subject: [PATCH 020/204] Attempting to fix some tests that started breaking... --- tests/test_core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 1de68f1..fe301d5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -46,7 +46,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test Twython generic POST request works, with a full url and with just an endpoint""" update_url = 'https://api.twitter.com/1.1/statuses/update.json' - status = self.api.post(update_url, params={'status': 'I love Twython!'}) + status = self.api.post(update_url, params={'status': 'I love Twython! %s' % int(time.time())}) self.api.post('statuses/destroy/%s' % status['id_str']) def test_get_lastfunction_header(self): @@ -148,7 +148,7 @@ class TwythonAPITestCase(unittest.TestCase): def test_update_and_destroy_status(self): """Test updating and deleting a status succeeds""" - status = self.api.update_status(status='Test post just to get deleted :(') + status = self.api.update_status(status='Test post just to get deleted :( %s' % int(time.time())) self.api.destroy_status(id=status['id_str']) def test_get_oembed_tweet(self): @@ -186,7 +186,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test sending a direct message to someone who doesn't follow you fails""" self.assertRaises(TwythonError, self.api.send_direct_message, - screen_name=protected_twitter_2, text='Yo, man!') + screen_name=protected_twitter_2, text='Yo, man! %s' % int(time.time())) # Friends & Followers def test_get_user_ids_of_blocked_retweets(self): -- 2.39.5 From a04a7c155c1455573d2ef89bcf5de491e84af481 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 21:04:39 -0400 Subject: [PATCH 021/204] Fixing up some more tests Even though this should be done in here really :P --- tests/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index fe301d5..c0ae08b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -366,10 +366,10 @@ class TwythonAPITestCase(unittest.TestCase): def test_create_update_destroy_list_add_remove_list_members(self): """Test create a list, adding and removing members then deleting the list succeeds""" - the_list = self.api.create_list(name='Stuff') + the_list = self.api.create_list(name='Stuff %s' % int(time.time())) list_id = the_list['id_str'] - self.api.update_list(list_id=list_id, name='Stuff Renamed') + self.api.update_list(list_id=list_id, name='Stuff Renamed %s' % int(time.time())) screen_names = ['johncena', 'xbox'] # Multi add/delete members -- 2.39.5 From 0fef3369ae6b8f312681cffc498231ef4e0d0da2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 21:16:08 -0400 Subject: [PATCH 022/204] Attempting to fix tests again -___- --- .travis.yml | 3 ++- tests/config.py | 3 ++- tests/test_core.py | 29 +++++++++++++++++++---------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89a3583..eb88c4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,8 @@ env: - PROTECTED_TWITTER_1=TwythonSecure1 - PROTECTED_TWITTER_2=TwythonSecure2 - TEST_TWEET_ID=332992304010899457 - - TEST_LIST_ID=574 + - TEST_LIST_SLUG=team + - TEST_LIST_OWNER_SCREEN_NAME=twitterapi install: pip install -r requirements.txt script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing notifications: diff --git a/tests/config.py b/tests/config.py index af098cd..4e8895e 100644 --- a/tests/config.py +++ b/tests/config.py @@ -16,7 +16,8 @@ protected_twitter_2 = os.environ.get('PROTECTED_TWITTER_2', 'TwythonSecure2') # Test Ids test_tweet_id = os.environ.get('TEST_TWEET_ID', '318577428610031617') -test_list_id = os.environ.get('TEST_LIST_ID', '574') # 574 is @twitter/team +test_list_slug = os.environ.get('TEST_LIST_SLUG', 'team') +test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', 'twitterapi') test_tweet_object = {u'contributors': None, u'truncated': False, u'text': u'http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ https://t.co/67pwRvY6z9', 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': [104, 116], 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': [65, 76], u'text': u'checkitout'}, {u'indices': [90, 97], u'text': u'thanks'}], 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': [117, 140], u'expanded_url': u'https://github.com', u'display_url': u'github.com'}]}, 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 should #checkitout. If you can! #thanks Love, @__twython__ github.com' diff --git a/tests/test_core.py b/tests/test_core.py index c0ae08b..b54f729 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,8 +3,8 @@ 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_id, access_token, test_tweet_object, - test_tweet_html + test_tweet_id, test_list_slug, test_list_owner_screen_name, + access_token, test_tweet_object, test_tweet_html ) import time @@ -361,7 +361,8 @@ class TwythonAPITestCase(unittest.TestCase): def test_get_list_statuses(self): """Test timeline of tweets authored by members of the specified list succeeds""" - self.api.get_list_statuses(list_id=test_list_id) + self.api.get_list_statuses(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) def test_create_update_destroy_list_add_remove_list_members(self): """Test create a list, adding and removing members then @@ -386,28 +387,36 @@ class TwythonAPITestCase(unittest.TestCase): def test_get_list_subscribers(self): """Test list of subscribers of a specific list succeeds""" - self.api.get_list_subscribers(list_id=test_list_id) + self.api.get_list_subscribers(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) def test_subscribe_is_subbed_and_unsubscribe_to_list(self): """Test subscribing, is a list sub and unsubbing to list succeeds""" - self.api.subscribe_to_list(list_id=test_list_id) + self.api.subscribe_to_list(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) # Returns 404 if user is not a subscriber - self.api.is_list_subscriber(list_id=test_list_id, + self.api.is_list_subscriber(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name, screen_name=screen_name) - self.api.unsubscribe_from_list(list_id=test_list_id) + self.api.unsubscribe_from_list(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) def test_is_list_member(self): """Test returning if specified user is member of a list succeeds""" # Returns 404 if not list member - self.api.is_list_member(list_id=test_list_id, screen_name='jack') + self.api.is_list_member(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name, + screen_name='themattharris') def test_get_list_members(self): """Test listing members of the specified list succeeds""" - self.api.get_list_members(list_id=test_list_id) + self.api.get_list_members(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) def test_get_specific_list(self): """Test getting specific list succeeds""" - self.api.get_specific_list(list_id=test_list_id) + self.api.get_specific_list(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) def test_get_list_subscriptions(self): """Test collection of the lists the specified user is -- 2.39.5 From 30db223905714acd185871a80b599d348ec04b2f Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 21:24:30 -0400 Subject: [PATCH 023/204] Spicing up the README more [ci skip] --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 1e09fc5..4727dc5 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,8 @@ Twython ======= +.. image:: https://badge.fury.io/py/twython.png + :target: http://badge.fury.io/py/twython .. image:: https://travis-ci.org/ryanmcgrath/twython.png?branch=master :target: https://travis-ci.org/ryanmcgrath/twython .. image:: https://pypip.in/d/twython/badge.png -- 2.39.5 From 9cfad89d9041685c839f81b297131c47d0978be0 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 21:27:17 -0400 Subject: [PATCH 024/204] Update special_functions.rst [ci skip] --- docs/usage/special_functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index 3d791b6..8688ead 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -44,4 +44,4 @@ will be replaced with: This function excepts two parameters: ``use_display_url`` and ``use_expanded_url`` By default, ``use_display_url`` is ``True``. Meaning the link displayed in the tweet text will appear as (ex. google.com, github.com) If ``use_expanded_url`` is ``True``, it overrides ``use_display_url``. The urls will then be displayed as (ex. http://google.com, https://github.com) -If ``use_display_url`` and ``use_expanded_url`` is ``False``, short url will be used (t.co/xxxxx) \ No newline at end of file +If ``use_display_url`` and ``use_expanded_url`` are ``False``, short url will be used (t.co/xxxxx) -- 2.39.5 From 8efa00e9f31ef7a27ff0b373b69b61147eca1218 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 23:33:10 -0400 Subject: [PATCH 025/204] Fix docstring in types.py [ci skip] --- twython/streaming/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/streaming/types.py b/twython/streaming/types.py index 104f965..39a9ccb 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -55,7 +55,7 @@ class TwythonStreamerTypesStatuses(object): def filter(self, **params): """Stream statuses/filter - :param \*\*params: Paramters to send with your stream request + :param \*\*params: Parameters to send with your stream request Accepted params found at: https://dev.twitter.com/docs/api/1.1/post/statuses/filter @@ -67,7 +67,7 @@ class TwythonStreamerTypesStatuses(object): def sample(self, **params): """Stream statuses/sample - :param \*\*params: Paramters to send with your stream request + :param \*\*params: Parameters to send with your stream request Accepted params found at: https://dev.twitter.com/docs/api/1.1/get/statuses/sample @@ -79,7 +79,7 @@ class TwythonStreamerTypesStatuses(object): def firehose(self, **params): """Stream statuses/firehose - :param \*\*params: Paramters to send with your stream request + :param \*\*params: Parameters to send with your stream request Accepted params found at: https://dev.twitter.com/docs/api/1.1/get/statuses/firehose -- 2.39.5 From 0fa9b631c1fd546d3f6e8f41318a45d242eb6151 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Jul 2013 23:42:52 -0400 Subject: [PATCH 026/204] Update documentation for cursor example [ci skip] --- docs/index.rst | 24 ++++++++++++++++++ docs/usage/special_functions.rst | 43 ++++++++++++++++++++++++++++++++ twython/api.py | 18 ------------- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e971e45..1aa13e0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,14 +29,38 @@ Features Usage ----- +.. + I know it isn't necessary to start a new tree for every section, + but I think it looks a bit cleaner that way! + .. toctree:: :maxdepth: 4 usage/install + +.. toctree:: + :maxdepth: 4 + usage/starting_out + +.. toctree:: + :maxdepth: 4 + usage/basic_usage + +.. toctree:: + :maxdepth: 4 + usage/advanced_usage + +.. toctree:: + :maxdepth: 4 + usage/streaming_api + +.. toctree:: + :maxdepth: 2 + usage/special_functions Twython API Documentation diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index 3d791b6..9f432ee 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -7,6 +7,49 @@ This section covers methods to are part of Twython but not necessarily connected ******************************************************************************* +Cursor +------ + +This function returns a generator for Twitter API endpoints that are able to be pagintated in some way (either by cursor or since_id parameter) + +The Old Way +^^^^^^^^^^^ + +.. code-block:: python + + from twython import Twython + + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + results = twitter.search(q='twitter') + if results.get('statuses'): + for result in results['statuses']: + print result['id_str'] + +The New Way +^^^^^^^^^^^ + +.. code-block:: python + + from twython import Twython + + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + results = twitter.cursor(t.search, q='twitter') + for result in results: + print result['id_str'] + +Another example: + +.. code-block:: python + + results = twitter.cursor(t.get_mentions_timeline) + for result in results: + print result['id_str'] + + HTML for Tweet -------------- diff --git a/twython/api.py b/twython/api.py index 6a99a7d..0cabcda 100644 --- a/twython/api.py +++ b/twython/api.py @@ -363,24 +363,6 @@ class Twython(EndpointsMixin, object): return '%s?%s' % (api_url, '&'.join(querystring)) def search_gen(self, search_query, **params): # pragma: no cover - """Returns a generator of tweets that match a specified query. - - Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets - - :param search_query: Query you intend to search Twitter for - :param \*\*params: Extra parameters to send with your search request - :rtype: generator - - Usage:: - - >>> from twython import Twython - >>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - - >>> search = twitter.search_gen('python') - >>> for result in search: - >>> print result - - """ warnings.warn( 'This method is deprecated. You should use Twython.cursor instead. [eg. Twython.cursor(Twython.search, q=\'your_query\')]', TwythonDeprecationWarning, -- 2.39.5 From 10317bac23672531ffd25d5aaf577d7f557d162b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 20 Jul 2013 14:28:49 -0400 Subject: [PATCH 027/204] Accomedate all endpoints that can be paginated with iter_mode and if needed, iter_key --- twython/endpoints.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 20d42d3..bb70f0e 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -123,6 +123,8 @@ class EndpointsMixin(object): """ return self.get('statuses/retweeters/ids', params=params) + get_retweeters_ids.iter_mode = 'cursor' + get_retweeters_ids.iter_key = 'ids' # Search def search(self, **params): @@ -144,6 +146,7 @@ class EndpointsMixin(object): """ return self.get('direct_messages', params=params) + get_direct_messages.iter_mode = 'id' def get_sent_messages(self, **params): """Returns the 20 most recent direct messages sent by the authenticating user. @@ -152,6 +155,7 @@ class EndpointsMixin(object): """ return self.get('direct_messages/sent', params=params) + get_sent_messages.iter_mode = 'id' def get_direct_message(self, **params): """Returns a single direct message, specified by an id parameter. @@ -195,6 +199,8 @@ class EndpointsMixin(object): """ return self.get('friends/ids', params=params) + get_friends_ids.iter_mode = 'cursor' + get_friends_ids.iter_key = 'ids' def get_followers_ids(self, **params): """Returns a cursored collection of user IDs for every user @@ -204,6 +210,8 @@ class EndpointsMixin(object): """ return self.get('followers/ids', params=params) + get_followers_ids.iter_mode = 'cursor' + get_followers_ids.iter_key = 'ids' def lookup_friendships(self, **params): """Returns the relationships of the authenticating user to the @@ -222,6 +230,8 @@ class EndpointsMixin(object): """ return self.get('friendships/incoming', params=params) + get_incoming_friendship_ids.iter_mode = 'cursor' + get_incoming_friendship_ids.iter_key = 'ids' def get_outgoing_friendship_ids(self, **params): """Returns a collection of numeric IDs for every protected user for @@ -231,6 +241,8 @@ class EndpointsMixin(object): """ return self.get('friendships/outgoing', params=params) + get_outgoing_friendship_ids.iter_mode = 'cursor' + get_outgoing_friendship_ids.iter_key = 'ids' def create_friendship(self, **params): """Allows the authenticating users to follow the user specified @@ -276,7 +288,8 @@ class EndpointsMixin(object): """ return self.get('friends/list', params=params) - get_friends_list.iterator_mode = 'cursor' + get_friends_list.iter_mode = 'cursor' + get_friends_list.iter_key = 'users' def get_followers_list(self, **params): """Returns a cursored collection of user objects for users @@ -286,6 +299,8 @@ class EndpointsMixin(object): """ return self.get('followers/list', params=params) + get_followers_list.iter_mode = 'cursor' + get_followers_list.iter_key = 'users' # Users def get_account_settings(self, **params): @@ -363,6 +378,8 @@ class EndpointsMixin(object): """ return self.get('blocks/list', params=params) + list_blocks.iter_mode = 'cursor' + list_blocks.iter_key = 'users' def list_block_ids(self, **params): """Returns an array of numeric user ids the authenticating user is blocking. @@ -371,6 +388,8 @@ class EndpointsMixin(object): """ return self.get('blocks/ids', params=params) + list_block_ids.iter_mode = 'cursor' + list_block_ids.iter_key = 'ids' def create_block(self, **params): """Blocks the specified user from following the authenticating user. @@ -489,6 +508,7 @@ class EndpointsMixin(object): """ return self.get('favorites/list', params=params) + get_favorites.iter_mode = 'id' def destroy_favorite(self, **params): """Un-favorites the status specified in the ID parameter as the authenticating user. @@ -522,6 +542,7 @@ class EndpointsMixin(object): """ return self.get('lists/statuses', params=params) + get_list_statuses.iter_mode = 'id' def delete_list_member(self, **params): """Removes the specified member from the list. @@ -538,6 +559,8 @@ class EndpointsMixin(object): """ return self.get('lists/subscribers', params=params) + get_list_subscribers.iter_mode = 'cursor' + get_list_subscribers.iter_key = 'users' def subscribe_to_list(self, **params): """Subscribes the authenticated user to the specified list. @@ -587,6 +610,8 @@ class EndpointsMixin(object): """ return self.get('lists/members', params=params) + get_list_members.iter_mode = 'cursor' + get_list_members.iter_key = 'users' def add_list_member(self, **params): """Add a member to a list. @@ -635,6 +660,8 @@ class EndpointsMixin(object): """ return self.get('lists/subscriptions', params=params) + get_list_subscriptions.iter_mode = 'cursor' + get_list_subscriptions.iter_key = 'lists' def delete_list_members(self, **params): """Removes multiple members from a list, by specifying a @@ -652,6 +679,8 @@ class EndpointsMixin(object): """ return self.get('lists/ownerships', params=params) + show_owned_lists.iter_mode = 'cursor' + show_owned_lists.iter_key = 'lists' # Saved Searches def get_saved_searches(self, **params): -- 2.39.5 From dd3727c6f544eda3cb68885d22b3fe672848ab60 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 20 Jul 2013 17:22:07 -0400 Subject: [PATCH 028/204] Fixes #243 --- HISTORY.rst | 1 + tests/test_core.py | 4 ++++ twython/endpoints.py | 10 ++++++++++ 3 files changed, 15 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 4d60acb..e9a8054 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,7 @@ History - When an actual request error happens and a ``RequestException`` is raised, it is caught and a ``TwythonError`` is raised instead for convenience. - Added "cursor"-like functionality. Endpoints with the attribute ``iter_mode`` will be able to be passed to ``Twython.cursor`` and returned as a generator. - ``Twython.search_gen`` has been deprecated. Please use ``twitter.cursor(twitter.search, q='your_query')`` instead, where ``twitter`` is your ``Twython`` instance. +- Added method ``get_list_memberships`` 3.0.0 (2013-06-18) ++++++++++++++++++ diff --git a/tests/test_core.py b/tests/test_core.py index b54f729..3e0cfcc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -385,6 +385,10 @@ class TwythonAPITestCase(unittest.TestCase): self.api.delete_list(list_id=list_id) + def test_get_list_memberships(self): + """Test list of memberhips the authenticated user succeeds""" + self.api.get_list_memberships() + def test_get_list_subscribers(self): """Test list of subscribers of a specific list succeeds""" self.api.get_list_subscribers(slug=test_list_slug, diff --git a/twython/endpoints.py b/twython/endpoints.py index bb70f0e..7f709e0 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -552,6 +552,16 @@ class EndpointsMixin(object): """ return self.post('lists/members/destroy', params=params) + def get_list_memberships(self, **params): + """Returns the lists the specified user has been added to. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/memberships + + """ + return self.get('lists/memberships', params=params) + get_list_memberships.iter_mode = 'cursor' + get_list_memberships.iter_key = 'lists' + def get_list_subscribers(self, **params): """Returns the subscribers of the specified list. -- 2.39.5 From 06f542572498e4a38ee3b1d7575783b4daa07789 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 20 Jul 2013 17:46:13 -0400 Subject: [PATCH 029/204] Fixes #244 --- HISTORY.rst | 2 +- tests/test_core.py | 16 ++++++++++++++++ twython/endpoints.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index e9a8054..de27f4d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,7 +12,7 @@ History - When an actual request error happens and a ``RequestException`` is raised, it is caught and a ``TwythonError`` is raised instead for convenience. - Added "cursor"-like functionality. Endpoints with the attribute ``iter_mode`` will be able to be passed to ``Twython.cursor`` and returned as a generator. - ``Twython.search_gen`` has been deprecated. Please use ``twitter.cursor(twitter.search, q='your_query')`` instead, where ``twitter`` is your ``Twython`` instance. -- Added method ``get_list_memberships`` +- Added methods ``get_list_memberships``, ``get_twitter_configuration``, ``get_supported_languages``, ``get_privacy_policy``, ``get_tos`` 3.0.0 (2013-06-18) ++++++++++++++++++ diff --git a/tests/test_core.py b/tests/test_core.py index 3e0cfcc..710ba1b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -482,6 +482,22 @@ class TwythonAPITestCase(unittest.TestCase): self.api.get_closest_trends(lat='37', long='-122') # Help + def test_get_twitter_configuration(self): + """Test getting Twitter's configuration succeeds""" + self.api.get_twitter_configuration() + + def test_get_supported_languages(self): + """Test getting languages supported by Twitter succeeds""" + self.api.get_supported_languages() + + def test_privacy_policy(self): + """Test getting Twitter's Privacy Policy succeeds""" + self.api.get_privacy_policy() + + def test_get_tos(self): + """Test getting the Twitter Terms of Service succeeds""" + self.api.get_tos() + def test_get_application_rate_limit_status(self): """Test getting application rate limit status succeeds""" self.oauth2_api.get_application_rate_limit_status() diff --git a/twython/endpoints.py b/twython/endpoints.py index 7f709e0..87db2a3 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -814,6 +814,39 @@ class EndpointsMixin(object): return self.post('oauth2/invalidate_token', params=params) # Help + def get_twitter_configuration(self, **params): + """Returns the current configuration used by Twitter + + Docs: https://dev.twitter.com/docs/api/1.1/get/help/configuration + + """ + return self.get('help/configuration', params=params) + + def get_supported_languages(self, **params): + """Returns the list of languages supported by Twitter along with + their ISO 639-1 code. + + Docs: https://dev.twitter.com/docs/api/1.1/get/help/languages + + """ + return self.get('help/languages', params=params) + + def get_privacy_policy(self, **params): + """Returns Twitter's Privacy Policy + + Docs: https://dev.twitter.com/docs/api/1.1/get/help/privacy + + """ + return self.get('help/privacy', params=params) + + def get_tos(self, **params): + """Return the Twitter Terms of Service + + Docs: https://dev.twitter.com/docs/api/1.1/get/help/tos + + """ + return self.get('help/tos', params=params) + def get_application_rate_limit_status(self, **params): """Returns the current rate limits for methods belonging to the specified resource families. -- 2.39.5 From df82888bef468acb61ddc8e15580b060b2fed921 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 23 Jul 2013 11:53:08 -0400 Subject: [PATCH 030/204] Update advanced_usage.rst [ci skip] --- docs/usage/advanced_usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index 419cdff..74246f3 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -22,7 +22,7 @@ Create a Twython instance with your application keys and the users OAuth tokens Updating Status with Image -------------------------- -Documentation: https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials +Documentation: https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media .. code-block:: python @@ -149,4 +149,4 @@ If you wish to access headers (ex. x-rate-limit-remaining, x-rate-limit-reset, c twitter.get_lastfunction_header('x-rate-limit-remaining') -So now you can authenticate, update your status (with or without an image), search Twitter, and a few other things! Good luck! \ No newline at end of file +So now you can authenticate, update your status (with or without an image), search Twitter, and a few other things! Good luck! -- 2.39.5 From 1fa4b507f08411387a2a07f440fc5b48c7a01cd3 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 24 Jul 2013 00:23:11 -0400 Subject: [PATCH 031/204] Update AUTHORS.rst [ci skip] --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 021339c..6cddac6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -42,3 +42,4 @@ Patches and Suggestions - `DevDave `_, quick fix for longs with helper._transparent_params - `Ruben Varela Rosa `_, Fixed search example - `Oleg Anashkin `_, streaming ``handlers`` functionality +- `Luis Alberto Santana Date: Wed, 24 Jul 2013 00:25:05 -0400 Subject: [PATCH 032/204] Update HISTORY.rst [ci skip] --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index de27f4d..3c35a77 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,7 @@ History - Added "cursor"-like functionality. Endpoints with the attribute ``iter_mode`` will be able to be passed to ``Twython.cursor`` and returned as a generator. - ``Twython.search_gen`` has been deprecated. Please use ``twitter.cursor(twitter.search, q='your_query')`` instead, where ``twitter`` is your ``Twython`` instance. - Added methods ``get_list_memberships``, ``get_twitter_configuration``, ``get_supported_languages``, ``get_privacy_policy``, ``get_tos`` +- Added ``auth_endpoint`` parameter to ``Twython.__init__`` for cases when the right parameters weren't being shown during the authentication step. 3.0.0 (2013-06-18) ++++++++++++++++++ -- 2.39.5 From b886ba2a69fddd9940a2502d706b7d8c5890525e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 2 Aug 2013 12:37:49 -0400 Subject: [PATCH 033/204] Update conf.py --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 7a89c0c..1f5311d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.0.0' +version = '3.1.0' # The full version, including alpha/beta/rc tags. -release = '3.0.0' +release = '3.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -- 2.39.5 From efcd5336bf2fa0fe63328f60ade574511c66a4e9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 2 Aug 2013 12:39:51 -0400 Subject: [PATCH 034/204] Updated twython-django submodule [ci skip] --- twython-django | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython-django b/twython-django index 3ffebcc..20e052f 160000 --- a/twython-django +++ b/twython-django @@ -1 +1 @@ -Subproject commit 3ffebcc57f57ad5db1d0ba8f940f2bab02f671a5 +Subproject commit 20e052fb15bd95dcfe354d6e6da10df2f665d649 -- 2.39.5 From c199b12c593a997d8835ea11b3d46586e70e7448 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 15 Aug 2013 15:06:13 -0400 Subject: [PATCH 035/204] Update .coveragerc --- .coveragerc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 664467b..227ed2b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,7 @@ [run] -omit = twython/advisory.py +omit = + ../twython/advisory.py + ../twython/compat.py [report] exclude_lines = -- 2.39.5 From d4f104b8a61987d31e19c2042884e52812fd6b84 Mon Sep 17 00:00:00 2001 From: Jim Rybarski Date: Fri, 16 Aug 2013 13:04:04 -0500 Subject: [PATCH 036/204] fixed problem where cursor stops after 10k results --- twython/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index 3a32deb..44da1b2 100644 --- a/twython/api.py +++ b/twython/api.py @@ -398,9 +398,6 @@ class Twython(EndpointsMixin, object): if not content: raise StopIteration - if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': - raise StopIteration - if hasattr(function, 'iter_key'): results = content.get(function.iter_key) else: @@ -409,6 +406,9 @@ class Twython(EndpointsMixin, object): for result in results: yield result + if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': + raise StopIteration + try: if function.iter_mode == 'id': if not 'max_id' in params: -- 2.39.5 From 79e2cfa78f33a4803bca81aa77d2262a84279130 Mon Sep 17 00:00:00 2001 From: Jim Rybarski Date: Fri, 23 Aug 2013 03:47:22 -0500 Subject: [PATCH 037/204] #255 Change GET to POST for lookup_user --- twython/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 87db2a3..460b3e9 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -414,7 +414,7 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/docs/api/1.1/get/users/lookup """ - return self.get('users/lookup', params=params) + return self.post('users/lookup', params=params) def show_user(self, **params): """Returns a variety of information about the user specified by the -- 2.39.5 From d2d9c97bd2fc5349ad7655485c7652b3815c9c01 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 27 Aug 2013 17:09:08 -0400 Subject: [PATCH 038/204] Update basic_usage.rst Fixes #260 [ci skip] --- docs/usage/basic_usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/basic_usage.rst b/docs/usage/basic_usage.rst index cea60f0..4ed2a3f 100644 --- a/docs/usage/basic_usage.rst +++ b/docs/usage/basic_usage.rst @@ -48,7 +48,7 @@ Updating Status This method makes use of dynamic arguments, :ref:`read more about them ` -Documentation: https://dev.twitter.com/docs/api/1/post/statuses/update +Documentation: https://dev.twitter.com/docs/api/1.1/post/statuses/update .. code-block:: python -- 2.39.5 From feac105c85db1b40365b2c2268b70c5d6e7b673f Mon Sep 17 00:00:00 2001 From: "bool.dev" Date: Fri, 30 Aug 2013 02:14:39 +0530 Subject: [PATCH 039/204] Fixed a docstring typo in get_authentication_tokens. --- twython/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index 44da1b2..19efb96 100644 --- a/twython/api.py +++ b/twython/api.py @@ -256,7 +256,8 @@ class Twython(EndpointsMixin, object): :param callback_url: (optional) Url the user is returned to after they authorize your app (web clients only) :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. - :param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value + :param screen_name: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value + :rtype: dict """ if self.oauth_version != 1: -- 2.39.5 From 37fc3e1aaac55bc7c807609bf1c2c32810c48839 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 5 Sep 2013 20:45:53 -0500 Subject: [PATCH 040/204] Coerce elements to strings as fallback --- twython/helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/twython/helpers.py b/twython/helpers.py index 24ad835..a482610 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -25,7 +25,10 @@ def _transparent_params(_params): elif isinstance(v, basestring) or isinstance(v, numeric_types): params[k] = v elif isinstance(v, list): - params[k] = ','.join(v) + try: + params[k] = ','.join(v) + except TypeError: + params[k] = ','.join(map(str,v)) else: continue # pragma: no cover return params, files -- 2.39.5 From 21ce7edc2b57c027ac742683f044f698ebb2fe1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20M=C3=B6llerstrand?= Date: Fri, 6 Sep 2013 13:16:47 +0300 Subject: [PATCH 041/204] fix typo of APP_SECRET --- docs/usage/starting_out.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/starting_out.rst b/docs/usage/starting_out.rst index 9678fcf..b96e452 100644 --- a/docs/usage/starting_out.rst +++ b/docs/usage/starting_out.rst @@ -122,7 +122,7 @@ Obtain an OAuth 2 Access Token .. code-block:: python APP_KEY = 'YOUR_APP_KEY' - APP_SECET = 'YOUR_APP_SECRET' + APP_SECRET = 'YOUR_APP_SECRET' twitter = Twython(APP_KEY, APP_SECRET, oauth_version=2) ACCESS_TOKEN = twitter.obtain_access_token() -- 2.39.5 From a0b7c884022b80bc07f68ab31ec47017a2e956a8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 24 Sep 2013 15:55:50 -0400 Subject: [PATCH 042/204] Fixing travis encrypted keys, version number and requests requirement --- .travis.yml | 28 ++++++---------------------- requirements.txt | 2 +- setup.py | 4 ++-- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index eb88c4d..692131b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,28 +3,12 @@ python: - 2.6 - 2.7 - 3.3 -env: - global: - - secure: |- - ICEoq4tQdOCGzpQAgLHunOflhLqs7hb1xYuD6mXZCbO/L+hDI4+pImA9mZZd - doyfyFMuNprWfgdUdcAL3axliRR2TyQIt51FLf7bD01jJEPa0GOKlKE/s/+3 - 7oDs+kkSr81w9rvIqpx55nE8+DYzkh+TwDp5oAT+m/EE+lFItfk= - - secure: |- - LNowlSsXwgbFT0g8B4fDlBmOhycAfSxUNFiYBDn70H93WDLr9drEbE7Wl2Mv - AlGJw0kXey4K4oTV4+Bsy6gacsvCINpFk7f4ulTDHCH4MTONtG1n51RlkLW0 - SAH8di1DiXR3PqRv+NKixVsgI0ZCY1B78NuvaoDL68bfu0KEdHs= - - secure: |- - kC9hGpdJJesmZZGMXEoPWK/lzIU6vUeguV/yI2jLgRin0EKPsgds0qR4737x - 2Z2q1+CFUlvHkl+povGcm0/A1rkNqU0KKBcxRBu/XXRxJ3DWp7gIGsmoyWUW - 68kdPOwxywZ+tj6BCD7zmStKn4I3mSzTmGKaWj8ZT0wQ91tl0Y8= - - secure: |- - Y0M90wCpDWmSdBmgPCV2N9mMSaRMdEOis5r5sfUq/5aFTB/KDaSR9scM1g+L - 21OtvUBvaG1bdSzn0T+I5Fs/MkfbtTmuahogy83nsNDRpIZJmRIsHFmJw1fz - nEHD2Kbm4iLMYzrKto77KpxYSQMnc3sQKZjreaI31NLu+7raCAk= - - secure: |- - j1gePLSZF8SRcpF1AU+cBK5MSih5MrM1iGE6N7VWI0wrl+xh7wr3QLtVkAar - AeMFgwkz6QalfrKLsoUPFuNMv7vn+2CthC9pRv+NRk+4xV+37NysHFPR7JRo - xK2EC+DCiw2eJECnk9IPGQTgkVnFAQ3GLnsBSzzJ+UAkG2NjZ88= +env: + global: + - secure: USjLDneiXlVvEjkUVqTt+LBi0XJ4QhkRcJzqVXA9gEau1NTjAkNTPmHjUbOygp0dkfoV0uWrZKCw6fL1g+HJgWl0vHeHzcNl4mUkA+OwkGFHgaeIhvUfnyyJA8P3Zm21XHC+ehzMpEFN5fVNNhREjnRj+CXMc0FgA6knwBRobu4= + - secure: HFTL8UN1GkY6/GqygpgzWpYdWvhXRN8uTO//AgGzSg0FkHuDFcxRJigwCNI3PRImFkkRRVmUJDw18dolkx2h60w0wqD9K09DFpPTYfwmF9Bql+O3hjG9Ep9iu+CREwQTm2u66f36Q+pjaDiHWmr0kgi0zUTD7w0UvlN7gdFCMxk= + - secure: L3nD/BEOAqGTmd8Va9bQ8MZL5gbiNWuy9TvIxCmQ5bfdlAAdq371oqXpuy90JftaloapktjjmuSrsiszWgMMB/TGDO45h1LrYQEMEmA/4JN/uly3lUk8th9Rig+blKTG0q6X6GsX8UWA0xVeLtXzkddpNAOBgpeqb33pWmFkdcM= + - secure: OdVG7LVqQ13RQxxEUPEMHZb0seoZUNzq+oy/K1qe4ubcupqiMh47pxnDcei6vkVpMn8QIvbG9lcV0oOREqJ+m+g3wUA5JX95liFqLNsmLMMVgP3yjYDf7KoNHckA5H5BwIYO/AFCBdwyNN0h439kwSciCbIr70UModVkvdWoFLI= - SCREEN_NAME=__twython__ - PROTECTED_TWITTER_1=TwythonSecure1 - PROTECTED_TWITTER_2=TwythonSecure2 diff --git a/requirements.txt b/requirements.txt index b7897fb..cab175e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage==3.6.0 -requests==1.2.2 +requests==2.0.0 requests_oauthlib==0.3.2 python-coveralls==2.1.0 nose-cov==1.6 diff --git a/setup.py b/setup.py index e0501a9..710b097 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import sys from setuptools import setup __author__ = 'Ryan McGrath ' -__version__ = '3.0.0' +__version__ = '3.1.0' packages = [ 'twython', @@ -20,7 +20,7 @@ if sys.argv[-1] == 'publish': setup( name='twython', version=__version__, - install_requires=['requests==1.2.3', 'requests_oauthlib==0.3.2'], + install_requires=['requests==2.0.0', 'requests_oauthlib==0.3.2'], author='Ryan McGrath', author_email='ryan@venodesigns.net', license=open('LICENSE').read(), -- 2.39.5 From 85a327163de628cd721789d7d2c3ab6bc6a84998 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 24 Sep 2013 17:50:50 -0400 Subject: [PATCH 043/204] Special encrypt for access token, this probably won't work --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 692131b..fc0db42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ env: - secure: HFTL8UN1GkY6/GqygpgzWpYdWvhXRN8uTO//AgGzSg0FkHuDFcxRJigwCNI3PRImFkkRRVmUJDw18dolkx2h60w0wqD9K09DFpPTYfwmF9Bql+O3hjG9Ep9iu+CREwQTm2u66f36Q+pjaDiHWmr0kgi0zUTD7w0UvlN7gdFCMxk= - secure: L3nD/BEOAqGTmd8Va9bQ8MZL5gbiNWuy9TvIxCmQ5bfdlAAdq371oqXpuy90JftaloapktjjmuSrsiszWgMMB/TGDO45h1LrYQEMEmA/4JN/uly3lUk8th9Rig+blKTG0q6X6GsX8UWA0xVeLtXzkddpNAOBgpeqb33pWmFkdcM= - secure: OdVG7LVqQ13RQxxEUPEMHZb0seoZUNzq+oy/K1qe4ubcupqiMh47pxnDcei6vkVpMn8QIvbG9lcV0oOREqJ+m+g3wUA5JX95liFqLNsmLMMVgP3yjYDf7KoNHckA5H5BwIYO/AFCBdwyNN0h439kwSciCbIr70UModVkvdWoFLI= + - secure: RJgKNXCxQZxjkxqinv6x96hGZvfJpjOIaRpH9YUJrCZeqLbS1FUOvTWkze5nZQw4DL14zBmpGZXYC9twSYvscEGNnaEhxDKygiFfLosERRa65N57kphD0AsGNe9YAhWUGDbCxnZ0PYHc48DYxX+jPxqsQ/2bxZbXVoJg4pZFP3o= - SCREEN_NAME=__twython__ - PROTECTED_TWITTER_1=TwythonSecure1 - PROTECTED_TWITTER_2=TwythonSecure2 @@ -24,3 +25,9 @@ branches: - master after_success: - coveralls +before_script: + - |- + ACCESS_TOKEN_B64=U2FsdGVkX1+OsKZdDqnhWy1jwIUKKqjp2eeTeIS2S2Z9xfwzg2XPavn01lV9Ml7j + LULxl/l4021YZ3DdYpudm7vQHr+lJGMM1TNPdf0QIusxWYZp7pGayl6uourm8iEY + WIEpMtCNL3l6Ntluw077oVP9TW0kiGQ8JROemIHLTOY= + - ACCESS_TOKEN=`openssl enc -d -aes-256-cbc -a $ACCESS_TOKEN_B64 -pass env:$ACCESS_TOKEN_SECRET` -- 2.39.5 From 58ff3de7a198b582725fe2760a271533e7426892 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 24 Sep 2013 17:58:59 -0400 Subject: [PATCH 044/204] Trying this travis var stuff again! --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc0db42..07b2744 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,10 @@ env: - TEST_TWEET_ID=332992304010899457 - TEST_LIST_SLUG=team - TEST_LIST_OWNER_SCREEN_NAME=twitterapi + - |- + ACCESS_TOKEN_B64=U2FsdGVkX1+OsKZdDqnhWy1jwIUKKqjp2eeTeIS2S2Z9xfwzg2XPavn01lV9Ml7j + LULxl/l4021YZ3DdYpudm7vQHr+lJGMM1TNPdf0QIusxWYZp7pGayl6uourm8iEY + WIEpMtCNL3l6Ntluw077oVP9TW0kiGQ8JROemIHLTOY= install: pip install -r requirements.txt script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing notifications: @@ -26,8 +30,4 @@ branches: after_success: - coveralls before_script: - - |- - ACCESS_TOKEN_B64=U2FsdGVkX1+OsKZdDqnhWy1jwIUKKqjp2eeTeIS2S2Z9xfwzg2XPavn01lV9Ml7j - LULxl/l4021YZ3DdYpudm7vQHr+lJGMM1TNPdf0QIusxWYZp7pGayl6uourm8iEY - WIEpMtCNL3l6Ntluw077oVP9TW0kiGQ8JROemIHLTOY= - - ACCESS_TOKEN=`openssl enc -d -aes-256-cbc -a $ACCESS_TOKEN_B64 -pass env:$ACCESS_TOKEN_SECRET` + - export ACCESS_TOKEN=$(echo $ACCESS_TOKEN_B64 | openssl enc -d -aes-256-cbc -a -pass env:ACCESS_TOKEN_SECRET) -- 2.39.5 From bc373c3436d8407d9ce821be5e2da4fe1f476521 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 24 Sep 2013 18:12:17 -0400 Subject: [PATCH 045/204] Gotsta see what the access token is --- tests/test_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_core.py b/tests/test_core.py index 710ba1b..3a38ec8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -29,6 +29,7 @@ class TwythonAPITestCase(unittest.TestCase): oauth_token, oauth_token_secret, client_args=client_args) + print 'access token is', access_token self.oauth2_api = Twython(app_key, access_token=access_token, client_args=oauth2_client_args) -- 2.39.5 From 082528490eb1674c2b0737b8d832d171bdd6d723 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 10:45:24 -0400 Subject: [PATCH 046/204] Update docs [ci skip] --- docs/usage/advanced_usage.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index 74246f3..db36301 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -102,8 +102,8 @@ Here is an example of sending the request through proxies: client_args = { 'proxies': { - 'http': '10.0.10.1:8000', - 'https': '10.0.10.1:8001', + 'http': 'http://10.0.10.1:8000', + 'https': 'https://10.0.10.1:8001', } } @@ -122,8 +122,8 @@ or both (and set a timeout variable): 'User-Agent': 'My App Name' }, 'proxies': { - 'http': '10.0.10.1:8000', - 'https': '10.0.10.1:8001', + 'http': 'http://10.0.10.1:8000', + 'https': 'https://10.0.10.1:8001', } 'timeout': 300, } -- 2.39.5 From 21257d0475a15625d0a2a2c3bcb899428fd0bfd2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 10:59:22 -0400 Subject: [PATCH 047/204] One line b64 string for ACCESS_TOKEN --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 07b2744..a4ebb21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,7 @@ env: - TEST_TWEET_ID=332992304010899457 - TEST_LIST_SLUG=team - TEST_LIST_OWNER_SCREEN_NAME=twitterapi - - |- - ACCESS_TOKEN_B64=U2FsdGVkX1+OsKZdDqnhWy1jwIUKKqjp2eeTeIS2S2Z9xfwzg2XPavn01lV9Ml7j - LULxl/l4021YZ3DdYpudm7vQHr+lJGMM1TNPdf0QIusxWYZp7pGayl6uourm8iEY - WIEpMtCNL3l6Ntluw077oVP9TW0kiGQ8JROemIHLTOY= + - ACCESS_TOKEN_B64=U2FsdGVkX18QdBhvMNshM4PGy04tU3HLwKP+nNSoNZHKsvGLjELcWEXN2LIu/T+yngX1vGONf9lo14ElnfS4k7sfhiru8phR4+rZuBVP3bDvC2A6fXJuhuLqNhBrWqg32WQewvxLWDWBoKmnvRHg5b74GHh+IN/12tU0cBF2HK8= install: pip install -r requirements.txt script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing notifications: -- 2.39.5 From 636e52a8c53e96a047b906cc38b2232a9d94ad86 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 11:14:27 -0400 Subject: [PATCH 048/204] Fixing decrypt, remove print --- .travis.yml | 2 +- tests/test_core.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a4ebb21..9e4e7fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,4 +27,4 @@ branches: after_success: - coveralls before_script: - - export ACCESS_TOKEN=$(echo $ACCESS_TOKEN_B64 | openssl enc -d -aes-256-cbc -a -pass env:ACCESS_TOKEN_SECRET) + - export ACCESS_TOKEN=$(echo $ACCESS_TOKEN_B64 | openssl enc -d -aes-256-cbc -A -a -pass env:ACCESS_TOKEN_SECRET) diff --git a/tests/test_core.py b/tests/test_core.py index 3a38ec8..710ba1b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -29,7 +29,6 @@ class TwythonAPITestCase(unittest.TestCase): oauth_token, oauth_token_secret, client_args=client_args) - print 'access token is', access_token self.oauth2_api = Twython(app_key, access_token=access_token, client_args=oauth2_client_args) -- 2.39.5 From a6399778734d9fea1528a21d4609a0a7500870b8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 11:24:13 -0400 Subject: [PATCH 049/204] Should have left the print in --- tests/test_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_core.py b/tests/test_core.py index 710ba1b..3a38ec8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -29,6 +29,7 @@ class TwythonAPITestCase(unittest.TestCase): oauth_token, oauth_token_secret, client_args=client_args) + print 'access token is', access_token self.oauth2_api = Twython(app_key, access_token=access_token, client_args=oauth2_client_args) -- 2.39.5 From 6da8805545fdfb40a62d6fd04aeb78400731bdda Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 11:59:55 -0400 Subject: [PATCH 050/204] =?UTF-8?q?Fix=20decrypt,=20remove=20print?= =?UTF-8?q?=E2=80=A6.=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- tests/test_core.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9e4e7fc..8411c02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,4 +27,4 @@ branches: after_success: - coveralls before_script: - - export ACCESS_TOKEN=$(echo $ACCESS_TOKEN_B64 | openssl enc -d -aes-256-cbc -A -a -pass env:ACCESS_TOKEN_SECRET) + - export ACCESS_TOKEN=$(echo $ACCESS_TOKEN_B64 | openssl enc -d -aes-256-cbc -A -a -pass env:ACCESS_TOKEN_PASS) diff --git a/tests/test_core.py b/tests/test_core.py index 3a38ec8..710ba1b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -29,7 +29,6 @@ class TwythonAPITestCase(unittest.TestCase): oauth_token, oauth_token_secret, client_args=client_args) - print 'access token is', access_token self.oauth2_api = Twython(app_key, access_token=access_token, client_args=oauth2_client_args) -- 2.39.5 From f6d458e7587069852af1cb05aba4cfc5e382df4b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 12:58:40 -0400 Subject: [PATCH 051/204] Fixes #230 --- twython/streaming/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index ef8924a..a27e6cd 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -21,7 +21,7 @@ import time class TwythonStreamer(object): def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, timeout=300, retry_count=None, retry_in=10, client_args=None, - handlers=None): + handlers=None, chunk_size=1): """Streaming class for a friendly streaming user experience Authentication IS required to use the Twitter Streaming API @@ -42,6 +42,9 @@ class TwythonStreamer(object): [ex. headers, proxies, verify(SSL verification)] :param handlers: (optional) Array of message types for which corresponding handlers will be called + + :param chunk_size: (optional) Define the buffer size before data is + actually returned from the Streaming API. Default: 1 """ self.auth = OAuth1(app_key, app_secret, @@ -124,7 +127,7 @@ class TwythonStreamer(object): while self.connected: response = _send(retry_counter) - for line in response.iter_lines(): + for line in response.iter_lines(self.chunk_size): if not self.connected: break if line: -- 2.39.5 From 99067c3e6ff6c0738d514bdfff9eea2c939cd807 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 13:02:52 -0400 Subject: [PATCH 052/204] Fixes #254 and forgot to def chunk_size --- twython/streaming/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index a27e6cd..6fa247e 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -10,6 +10,7 @@ Twitter API calls. from .. import __version__ from ..compat import json, is_py3 +from ..helpers import _transparent_params from .types import TwythonStreamerTypes import requests @@ -88,6 +89,8 @@ class TwythonStreamer(object): self.handlers = handlers if handlers else ['delete', 'limit', 'disconnect'] + self.chunk_size = chunk_size + def _request(self, url, method='GET', params=None): """Internal stream request handling""" self.connected = True @@ -95,6 +98,7 @@ class TwythonStreamer(object): method = method.lower() func = getattr(self.client, method) + params, _ = _transparent_params(params) def _send(retry_counter): requests_args = {} -- 2.39.5 From 28b6d68b3b12ab0410d4201c269f900e0e44e298 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 25 Sep 2013 16:21:49 -0400 Subject: [PATCH 053/204] Update changelog, fix version number [ci skip] --- HISTORY.rst | 4 +++- twython/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3c35a77..2fb4734 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ History ------- -3.1.0 (2013-xx-xx) +3.1.0 (2013-09-25) ++++++++++++++++++ - Added ``html_for_tweet`` static method. This method accepts a tweet object returned from a Twitter API call and will return a string with urls, mentions and hashtags in the tweet replaced with HTML. @@ -14,6 +14,8 @@ History - ``Twython.search_gen`` has been deprecated. Please use ``twitter.cursor(twitter.search, q='your_query')`` instead, where ``twitter`` is your ``Twython`` instance. - Added methods ``get_list_memberships``, ``get_twitter_configuration``, ``get_supported_languages``, ``get_privacy_policy``, ``get_tos`` - Added ``auth_endpoint`` parameter to ``Twython.__init__`` for cases when the right parameters weren't being shown during the authentication step. +- Fixed streaming issue where results wouldn't be returned for streams that weren't so active (See https://github.com/ryanmcgrath/twython/issues/202#issuecomment-19915708) +- Streaming API now uses ``_transparent_params`` so when passed ``True`` or ``False`` or an array, etc. Twython formats it to meet Twitter parameter standards (i.e. ['ryanmcgrath', 'mikehelmick', 'twitterapi'] would convert to string 'ryanmcgrath,mikehelmick,twitterapi') 3.0.0 (2013-06-18) ++++++++++++++++++ diff --git a/twython/__init__.py b/twython/__init__.py index 52f4836..4a90afc 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.0.0' +__version__ = '3.1.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 9bf83fd933b61fbdc025636621ba1ed7c505d260 Mon Sep 17 00:00:00 2001 From: Remi Rampin Date: Wed, 9 Oct 2013 14:39:45 -0400 Subject: [PATCH 054/204] Don't mask TwythonStreamer exceptions Exceptions in handlers or on_success which subclass ValueError would previously be caught and reported as a JSON decoding problem, and on_error() would be called (with status_code=200). --- twython/streaming/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 6fa247e..c814acb 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -139,14 +139,15 @@ class TwythonStreamer(object): if is_py3: line = line.decode('utf-8') data = json.loads(line) + except ValueError: # pragma: no cover + self.on_error(response.status_code, 'Unable to decode response, not valid JSON.') + else: if self.on_success(data): # pragma: no cover for message_type in self.handlers: if message_type in data: handler = getattr(self, 'on_' + message_type, None) if handler and callable(handler) and not handler(data.get(message_type)): break - except ValueError: # pragma: no cover - self.on_error(response.status_code, 'Unable to decode response, not valid JSON.') response.close() -- 2.39.5 From 6f77addef86d4ab414bea1251913e44b946a1322 Mon Sep 17 00:00:00 2001 From: Chris Seymour Date: Thu, 10 Oct 2013 15:53:02 +0100 Subject: [PATCH 055/204] [Trivial] Added missing comma. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4727dc5..143a277 100644 --- a/README.rst +++ b/README.rst @@ -156,7 +156,7 @@ Create a Twython instance with your application keys and the users OAuth tokens .. code-block:: python from twython import Twython - twitter = Twython(APP_KEY, APP_SECRET + twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) Authenticated Users Home Timeline -- 2.39.5 From 5ff6db186e46e7331789eb10e859aba18adef2ac Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sun, 20 Oct 2013 14:51:39 +1100 Subject: [PATCH 056/204] Update setup.py Copied setup import from Python-Requests so it does not fail on some platforms (e.g. Nokia N9/MeeGo). --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 710b097..85c1e64 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,10 @@ import os import sys -from setuptools import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup __author__ = 'Ryan McGrath ' __version__ = '3.1.0' -- 2.39.5 From 36ef365d43ccb298829a6c8b74fe88c5e1901bfd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 23 Oct 2013 00:10:53 -0400 Subject: [PATCH 057/204] Delete shorten_url.py Fixes #277 [ci skip] --- examples/shorten_url.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 examples/shorten_url.py diff --git a/examples/shorten_url.py b/examples/shorten_url.py deleted file mode 100644 index 61eb105..0000000 --- a/examples/shorten_url.py +++ /dev/null @@ -1,6 +0,0 @@ -from twython import Twython - -# Shortening URLs requires no authentication, huzzah -shortURL = Twython.shorten_url('http://www.webs.com/') - -print shortURL -- 2.39.5 From 6c2cac76a56d61699af190d95bd22a88493199eb Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 26 Oct 2013 19:04:08 -0400 Subject: [PATCH 058/204] Update lib versions, update gitignore [ci skip] --- .gitignore | 2 ++ requirements.txt | 4 ++-- setup.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 7dadcf7..60d1cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ nosetests.xml docs/_build test.py + +.venv diff --git a/requirements.txt b/requirements.txt index cab175e..a103b0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage==3.6.0 -requests==2.0.0 -requests_oauthlib==0.3.2 +requests==2.0.1 +requests_oauthlib==0.4.0 python-coveralls==2.1.0 nose-cov==1.6 diff --git a/setup.py b/setup.py index 85c1e64..a7ff8df 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ if sys.argv[-1] == 'publish': setup( name='twython', version=__version__, - install_requires=['requests==2.0.0', 'requests_oauthlib==0.3.2'], + install_requires=['requests==2.0.1', 'requests_oauthlib==0.4.0'], author='Ryan McGrath', author_email='ryan@venodesigns.net', license=open('LICENSE').read(), -- 2.39.5 From 638f75b93d531993983a2f789c9af8579b6e63ed Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 26 Oct 2013 19:04:36 -0400 Subject: [PATCH 059/204] Fixes #266 --- tests/test_auth.py | 7 +++++++ twython/api.py | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 12f54b2..edefe07 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -9,6 +9,8 @@ class TwythonAuthTestCase(unittest.TestCase): def setUp(self): self.api = Twython(app_key, app_secret) self.bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET') + self.bad_api_invalid_tokens = Twython('BAD_APP_KEY', 'BAD_APP_SECRET', + 'BAD_OT', 'BAD_OTS') self.oauth2_api = Twython(app_key, app_secret, oauth_version=2) self.oauth2_bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET', @@ -31,6 +33,11 @@ class TwythonAuthTestCase(unittest.TestCase): self.assertRaises(TwythonError, self.bad_api.get_authorized_tokens, 'BAD_OAUTH_VERIFIER') + def test_get_authorized_tokens_invalid_or_expired_tokens(self): + """Test getting final token fails when invalid or expired tokens have been passed""" + self.assertRaises(TwythonError, self.bad_api_invalid_tokens.get_authorized_tokens, + 'BAD_OAUTH_VERIFIER') + def test_get_authentication_tokens_raises_error_when_oauth2(self): """Test when API is set for OAuth 2, get_authentication_tokens raises a TwythonError""" diff --git a/twython/api.py b/twython/api.py index 19efb96..39b8f28 100644 --- a/twython/api.py +++ b/twython/api.py @@ -307,7 +307,21 @@ class Twython(EndpointsMixin, object): if self.oauth_version != 1: raise TwythonError('This method can only be called when your OAuth version is 1.0.') - response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}) + response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}, headers={'Content-Type': 'application/json'}) + + if response.status_code == 401: + try: + try: + # try to get json + content = response.json() + except AttributeError: # pragma: no cover + # if unicode detected + content = json.loads(response.content) + except ValueError: + content = {} + + raise TwythonError(content.get('error', 'Invalid / expired Token'), error_code=response.status_code) + authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') -- 2.39.5 From cf766311f0ea4030c8981894182ca49b425682a2 Mon Sep 17 00:00:00 2001 From: drevicko Date: Fri, 8 Nov 2013 12:23:22 +1100 Subject: [PATCH 060/204] added option to cursor() to yeild "pages" - ie: iterators of items for each page returned by twitter modified: api.py --- twython/api.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/twython/api.py b/twython/api.py index 39b8f28..6e9acac 100644 --- a/twython/api.py +++ b/twython/api.py @@ -388,7 +388,7 @@ class Twython(EndpointsMixin, object): ) return self.cursor(self.search, q=search_query, **params) - def cursor(self, function, **params): + def cursor(self, function, **params, returnPages = False): """Returns a generator for results that match a specified query. :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) @@ -418,8 +418,11 @@ class Twython(EndpointsMixin, object): else: results = content - for result in results: - yield result + if returnPages: + yield results + else: + for result in results: + yield result if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': raise StopIteration @@ -438,8 +441,11 @@ class Twython(EndpointsMixin, object): except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - for result in self.cursor(function, **params): - yield result + if returnPages: + yield self.cursor(function, **params) + else: + for result in self.cursor(function, **params): + yield result @staticmethod def unicode2utf8(text): -- 2.39.5 From 252ded6e00cfb751a27a84bfd8b2c57d2575d69e Mon Sep 17 00:00:00 2001 From: drevicko Date: Fri, 8 Nov 2013 12:43:29 +1100 Subject: [PATCH 061/204] fixed recursive part for returning cursor() pages modified: api.py --- twython/api.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/twython/api.py b/twython/api.py index 6e9acac..9fc673b 100644 --- a/twython/api.py +++ b/twython/api.py @@ -388,7 +388,7 @@ class Twython(EndpointsMixin, object): ) return self.cursor(self.search, q=search_query, **params) - def cursor(self, function, **params, returnPages = False): + def cursor(self, function, returnPages = False, **params): """Returns a generator for results that match a specified query. :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) @@ -441,11 +441,8 @@ class Twython(EndpointsMixin, object): except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - if returnPages: - yield self.cursor(function, **params) - else: - for result in self.cursor(function, **params): - yield result + for result in self.cursor(function, returnPages = returnPages, **params): + yield result @staticmethod def unicode2utf8(text): -- 2.39.5 From 32337fd036df907a21e9f5b1758abeb58649cb4e Mon Sep 17 00:00:00 2001 From: drevicko Date: Fri, 8 Nov 2013 13:03:23 +1100 Subject: [PATCH 062/204] changed cursor() from recursion to while loop modified: api.py --- twython/api.py | 52 ++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/twython/api.py b/twython/api.py index 39b8f28..2d5d065 100644 --- a/twython/api.py +++ b/twython/api.py @@ -408,38 +408,36 @@ class Twython(EndpointsMixin, object): if not hasattr(function, 'iter_mode'): raise TwythonError('Unable to create generator for Twython method "%s"' % function.__name__) - content = function(**params) + while True: + content = function(**params) - if not content: - raise StopIteration + if not content: + raise StopIteration - if hasattr(function, 'iter_key'): - results = content.get(function.iter_key) - else: - results = content + if hasattr(function, 'iter_key'): + results = content.get(function.iter_key) + else: + results = content - for result in results: - yield result + for result in results: + yield result - if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': - raise StopIteration + if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': + raise StopIteration - try: - if function.iter_mode == 'id': - if not 'max_id' in params: - # Add 1 to the id because since_id and max_id are inclusive - if hasattr(function, 'iter_metadata'): - since_id = content[function.iter_metadata].get('since_id_str') - else: - since_id = content[0]['id_str'] - params['since_id'] = (int(since_id) - 1) - elif function.iter_mode == 'cursor': - params['cursor'] = content['next_cursor_str'] - except (TypeError, ValueError): # pragma: no cover - raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - - for result in self.cursor(function, **params): - yield result + try: + if function.iter_mode == 'id': + if not 'max_id' in params: + # Add 1 to the id because since_id and max_id are inclusive + if hasattr(function, 'iter_metadata'): + since_id = content[function.iter_metadata].get('since_id_str') + else: + since_id = content[0]['id_str'] + params['since_id'] = (int(since_id) - 1) + elif function.iter_mode == 'cursor': + params['cursor'] = content['next_cursor_str'] + except (TypeError, ValueError): # pragma: no cover + raise TwythonError('Unable to generate next page of search results, `page` is not a number.') @staticmethod def unicode2utf8(text): -- 2.39.5 From c2068466af154f6915160b6aa86b8326b18661f2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 5 Dec 2013 18:15:30 -0500 Subject: [PATCH 063/204] Update requests version, HISTORY.rst, prepare for 3.1.1 release --- HISTORY.rst | 10 ++++++++++ requirements.txt | 2 +- setup.py | 4 ++-- twython/__init__.py | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2fb4734..73a3cd7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,16 @@ History ------- +3.1.1 (2013-12-05) +++++++++++++++++++ + +- Update `requests` version to 2.1.0. +- Fixed: Streaming issue where `Exceptions` in handlers or `on_success` which subclass `ValueError` would +previously be caught and reported as a JSON decoding problem, and +`on_error()` would be called (with status_code=200) +- Fixed issue where XML was returned when bad tokens were passed to `get_authorized_tokens` +- Fixed import for `setup` causing installation to fail on some devices (eg. Nokia N9/MeeGo) + 3.1.0 (2013-09-25) ++++++++++++++++++ diff --git a/requirements.txt b/requirements.txt index a103b0e..9168274 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage==3.6.0 -requests==2.0.1 +requests==2.1.0 requests_oauthlib==0.4.0 python-coveralls==2.1.0 nose-cov==1.6 diff --git a/setup.py b/setup.py index a7ff8df..7c8df69 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.1.0' +__version__ = '3.1.1' packages = [ 'twython', @@ -23,7 +23,7 @@ if sys.argv[-1] == 'publish': setup( name='twython', version=__version__, - install_requires=['requests==2.0.1', 'requests_oauthlib==0.4.0'], + install_requires=['requests==2.1.0', 'requests_oauthlib==0.4.0'], author='Ryan McGrath', author_email='ryan@venodesigns.net', license=open('LICENSE').read(), diff --git a/twython/__init__.py b/twython/__init__.py index 4a90afc..bd4bc0b 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.1.0' +__version__ = '3.1.1' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 92479cd75baf20a3f57441d3e3b388d32fa27447 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 5 Dec 2013 18:22:59 -0500 Subject: [PATCH 064/204] Update conf.py [ci skip] --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1f5311d..6367fe2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.1.0' +version = '3.1.1' # The full version, including alpha/beta/rc tags. -release = '3.1.0' +release = '3.1.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -- 2.39.5 From 74b2f97f2c5512361c910dee5673ed51b5fb675c Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 6 Dec 2013 11:12:29 -0500 Subject: [PATCH 065/204] Update HISTORY.rst [ci skip] --- HISTORY.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 73a3cd7..e56d657 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,9 +7,7 @@ History ++++++++++++++++++ - Update `requests` version to 2.1.0. -- Fixed: Streaming issue where `Exceptions` in handlers or `on_success` which subclass `ValueError` would -previously be caught and reported as a JSON decoding problem, and -`on_error()` would be called (with status_code=200) +- Fixed: Streaming issue where `Exceptions` in handlers or `on_success` which subclass `ValueError` would previously be caught and reported as a JSON decoding problem, and `on_error()` would be called (with status_code=200) - Fixed issue where XML was returned when bad tokens were passed to `get_authorized_tokens` - Fixed import for `setup` causing installation to fail on some devices (eg. Nokia N9/MeeGo) -- 2.39.5 From 0937fbe92950c17e2e7701312ea839b562e66415 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 6 Dec 2013 11:14:39 -0500 Subject: [PATCH 066/204] Fix Changelog, update version [ci skip] --- HISTORY.rst | 5 +++++ docs/conf.py | 4 ++-- setup.py | 2 +- twython/__init__.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index e56d657..1259227 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ History ------- +3.1.2 (2013-12-05) +++++++++++++++++++ + +- Fixed Changelog (HISTORY.rst) + 3.1.1 (2013-12-05) ++++++++++++++++++ diff --git a/docs/conf.py b/docs/conf.py index 6367fe2..b0b98eb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.1.1' +version = '3.1.2' # The full version, including alpha/beta/rc tags. -release = '3.1.1' +release = '3.1.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 7c8df69..d2d3570 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.1.1' +__version__ = '3.1.2' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index bd4bc0b..ab4df37 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.1.1' +__version__ = '3.1.2' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From d5dfcb7e8626ba2149049040573ac151205f143b Mon Sep 17 00:00:00 2001 From: Natan L Date: Sun, 15 Dec 2013 20:28:43 -0800 Subject: [PATCH 067/204] Fix typo 'SECERT' --> 'SECRET' --- docs/usage/starting_out.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/starting_out.rst b/docs/usage/starting_out.rst index b96e452..0a681ac 100644 --- a/docs/usage/starting_out.rst +++ b/docs/usage/starting_out.rst @@ -98,7 +98,7 @@ Once you have the final user tokens, store them in a database for later use! .. code-block:: python OAUTH_TOKEN = final_step['oauth_token'] - OAUTH_TOKEN_SECERT = final_step['oauth_token_secret'] + OAUTH_TOKEN_SECRET = final_step['oauth_token_secret'] .. _oauth2: -- 2.39.5 From 25eca18da3435ca97212fc9f7e1b304911e26b54 Mon Sep 17 00:00:00 2001 From: Cash Costello Date: Sun, 5 Jan 2014 14:34:37 -0500 Subject: [PATCH 068/204] fixed typo in documentation of cursor() --- twython/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index 39b8f28..ef1aac6 100644 --- a/twython/api.py +++ b/twython/api.py @@ -392,7 +392,7 @@ class Twython(EndpointsMixin, object): """Returns a generator for results that match a specified query. :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) - :param \*\*params: Extra parameters to send with your request (usually parameters excepted by the Twitter API endpoint) + :param \*\*params: Extra parameters to send with your request (usually parameters accepted by the Twitter API endpoint) :rtype: generator Usage:: -- 2.39.5 From 9cbd3d6fbc23a878970e298e1675ae3b2eb525c0 Mon Sep 17 00:00:00 2001 From: cash Date: Thu, 9 Jan 2014 22:59:23 -0500 Subject: [PATCH 069/204] added some basic tests of the core request() method using dropbox/responses --- requirements.txt | 1 + tests/test_core.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/requirements.txt b/requirements.txt index 9168274..917c31a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ requests==2.1.0 requests_oauthlib==0.4.0 python-coveralls==2.1.0 nose-cov==1.6 +responses==0.2.0 diff --git a/tests/test_core.py b/tests/test_core.py index 710ba1b..0d701f2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,6 +9,7 @@ from .config import ( import time import unittest +import responses class TwythonAPITestCase(unittest.TestCase): @@ -32,6 +33,94 @@ class TwythonAPITestCase(unittest.TestCase): self.oauth2_api = Twython(app_key, access_token=access_token, client_args=oauth2_client_args) + @responses.activate + def test_request_should_handle_full_endpoint(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + responses.add(responses.GET, url) + + self.api.request(url) + + self.assertEqual(1, len(responses.calls)) + self.assertEqual(url, responses.calls[0].request.url) + + @responses.activate + def test_request_should_handle_relative_endpoint(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + responses.add(responses.GET, url) + + self.api.request('search/tweets', version='1.1') + + self.assertEqual(1, len(responses.calls)) + self.assertEqual(url, responses.calls[0].request.url) + + @responses.activate + def test_request_should_post_request_regardless_of_case(self): + url = 'https://api.twitter.com/1.1/statuses/update.json' + responses.add(responses.POST, url) + + self.api.request(url, method='POST') + self.api.request(url, method='post') + + self.assertEqual(2, len(responses.calls)) + self.assertEqual(url, responses.calls[0].request.url) + self.assertEqual(url, responses.calls[1].request.url) + + @responses.activate + def test_request_should_throw_exception_with_invalid_http_method(self): + #TODO(cash): should Twython catch the AttributeError and throw a TwythonError + self.assertRaises(AttributeError, self.api.request, endpoint='search/tweets', method='INVALID') + + @responses.activate + def test_request_should_encode_boolean_as_lowercase_string(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + responses.add(responses.GET, url) + + self.api.request('search/tweets', params={'include_entities': True}) + self.api.request('search/tweets', params={'include_entities': False}) + + self.assertEqual(url + '?include_entities=true', responses.calls[0].request.url) + self.assertEqual(url + '?include_entities=false', responses.calls[1].request.url) + + @responses.activate + def test_request_should_handle_string_or_number_parameter(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + responses.add(responses.GET, url) + + self.api.request('search/tweets', params={'lang': 'es'}) + self.api.request('search/tweets', params={'count': 50}) + + self.assertEqual(url + '?lang=es', responses.calls[0].request.url) + self.assertEqual(url + '?count=50', responses.calls[1].request.url) + + @responses.activate + def test_request_should_encode_string_list_as_string(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + location = ['37.781157', '-122.39872', '1mi'] + responses.add(responses.GET, url) + + self.api.request('search/tweets', params={'geocode': location}) + + self.assertEqual(url + '?geocode=37.781157%2C-122.39872%2C1mi', responses.calls[0].request.url) + + @responses.activate + def test_request_should_encode_number_list_as_string(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + location = [37.781157, -122.39872, '1mi'] + responses.add(responses.GET, url) + + self.api.request('search/tweets', params={'geocode': location}) + + self.assertEqual(url + '?geocode=37.781157%2C-122.39872%2C1mi', responses.calls[0].request.url) + + @responses.activate + def test_request_should_ignore_bad_parameter(self): + url = 'https://api.twitter.com/1.1/search/tweets.json' + responses.add(responses.GET, url) + + self.api.request('search/tweets', params={'geocode': self}) + + self.assertEqual(url, responses.calls[0].request.url) + def test_construct_api_url(self): """Test constructing a Twitter API url works as we expect""" url = 'https://api.twitter.com/1.1/search/tweets.json' -- 2.39.5 From f075586fcd8364c8d448dc8a594d00d389ba729c Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 11 Jan 2014 10:06:30 -0500 Subject: [PATCH 070/204] moved integration tests to new file --- tests/test_core.py | 396 ------------------------------------- tests/test_endpoints.py | 429 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+), 396 deletions(-) create mode 100644 tests/test_endpoints.py diff --git a/tests/test_core.py b/tests/test_core.py index 0d701f2..d63d40d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -194,399 +194,3 @@ class TwythonAPITestCase(unittest.TestCase): def test_raise_error_on_bad_ssl_cert(self): """Test TwythonError is raised by a RequestException when an actual HTTP happens""" self.assertRaises(TwythonError, self.api.get, 'https://example.com') - - # Timelines - def test_get_mentions_timeline(self): - """Test returning mentions timeline for authenticated user succeeds""" - self.api.get_mentions_timeline() - - def test_get_user_timeline(self): - """Test returning timeline for authenticated user and random user - succeeds""" - self.api.get_user_timeline() # Authenticated User Timeline - self.api.get_user_timeline(screen_name='twitter') # Random User Timeline - - def test_get_protected_user_timeline_following(self): - """Test returning a protected user timeline who you are following - succeeds""" - self.api.get_user_timeline(screen_name=protected_twitter_1) - - def test_get_protected_user_timeline_not_following(self): - """Test returning a protected user timeline who you are not following - fails and raise a TwythonAuthError""" - self.assertRaises(TwythonAuthError, self.api.get_user_timeline, - screen_name=protected_twitter_2) - - def test_retweeted_of_me(self): - """Test that getting recent tweets by authenticated user that have - been retweeted by others succeeds""" - self.api.retweeted_of_me() - - def test_get_home_timeline(self): - """Test returning home timeline for authenticated user succeeds""" - self.api.get_home_timeline() - - # Tweets - def test_get_retweets(self): - """Test getting retweets of a specific tweet succeeds""" - self.api.get_retweets(id=test_tweet_id) - - def test_show_status(self): - """Test returning a single status details succeeds""" - self.api.show_status(id=test_tweet_id) - - def test_update_and_destroy_status(self): - """Test updating and deleting a status succeeds""" - status = self.api.update_status(status='Test post just to get deleted :( %s' % int(time.time())) - self.api.destroy_status(id=status['id_str']) - - def test_get_oembed_tweet(self): - """Test getting info to embed tweet on Third Party site succeeds""" - self.api.get_oembed_tweet(id='99530515043983360') - - def test_get_retweeters_ids(self): - """Test getting ids for people who retweeted a tweet succeeds""" - self.api.get_retweeters_ids(id='99530515043983360') - - # Search - def test_search(self): - """Test searching tweets succeeds""" - self.api.search(q='twitter') - - # Direct Messages - def test_get_direct_messages(self): - """Test getting the authenticated users direct messages succeeds""" - self.api.get_direct_messages() - - def test_get_sent_messages(self): - """Test getting the authenticated users direct messages they've - sent succeeds""" - self.api.get_sent_messages() - - def test_send_get_and_destroy_direct_message(self): - """Test sending, getting, then destory a direct message succeeds""" - message = self.api.send_direct_message(screen_name=protected_twitter_1, - text='Hey d00d! %s' % int(time.time())) - - self.api.get_direct_message(id=message['id_str']) - self.api.destroy_direct_message(id=message['id_str']) - - def test_send_direct_message_to_non_follower(self): - """Test sending a direct message to someone who doesn't follow you - fails""" - self.assertRaises(TwythonError, self.api.send_direct_message, - screen_name=protected_twitter_2, text='Yo, man! %s' % int(time.time())) - - # Friends & Followers - def test_get_user_ids_of_blocked_retweets(self): - """Test that collection of user_ids that the authenticated user does - not want to receive retweets from succeeds""" - self.api.get_user_ids_of_blocked_retweets(stringify_ids=True) - - def test_get_friends_ids(self): - """Test returning ids of users the authenticated user and then a random - user is following succeeds""" - self.api.get_friends_ids() - self.api.get_friends_ids(screen_name='twitter') - - def test_get_followers_ids(self): - """Test returning ids of users the authenticated user and then a random - user are followed by succeeds""" - self.api.get_followers_ids() - self.api.get_followers_ids(screen_name='twitter') - - def test_lookup_friendships(self): - """Test returning relationships of the authenticating user to the - comma-separated list of up to 100 screen_names or user_ids provided - succeeds""" - self.api.lookup_friendships(screen_name='twitter,ryanmcgrath') - - def test_get_incoming_friendship_ids(self): - """Test returning incoming friendship ids succeeds""" - self.api.get_incoming_friendship_ids() - - def test_get_outgoing_friendship_ids(self): - """Test returning outgoing friendship ids succeeds""" - self.api.get_outgoing_friendship_ids() - - def test_create_friendship(self): - """Test creating a friendship succeeds""" - self.api.create_friendship(screen_name='justinbieber') - - def test_destroy_friendship(self): - """Test destroying a friendship succeeds""" - self.api.destroy_friendship(screen_name='justinbieber') - - def test_update_friendship(self): - """Test updating friendships succeeds""" - self.api.update_friendship(screen_name=protected_twitter_1, - retweets='true') - - self.api.update_friendship(screen_name=protected_twitter_1, - retweets=False) - - def test_show_friendships(self): - """Test showing specific friendship succeeds""" - self.api.show_friendship(target_screen_name=protected_twitter_1) - - def test_get_friends_list(self): - """Test getting list of users authenticated user then random user is - following succeeds""" - self.api.get_friends_list() - self.api.get_friends_list(screen_name='twitter') - - def test_get_followers_list(self): - """Test getting list of users authenticated user then random user are - followed by succeeds""" - self.api.get_followers_list() - self.api.get_followers_list(screen_name='twitter') - - # Users - def test_get_account_settings(self): - """Test getting the authenticated user account settings succeeds""" - self.api.get_account_settings() - - def test_verify_credentials(self): - """Test representation of the authenticated user call succeeds""" - self.api.verify_credentials() - - def test_update_account_settings(self): - """Test updating a user account settings succeeds""" - self.api.update_account_settings(lang='en') - - def test_update_delivery_service(self): - """Test updating delivery settings fails because we don't have - a mobile number on the account""" - self.assertRaises(TwythonError, self.api.update_delivery_service, - device='none') - - def test_update_profile(self): - """Test updating profile succeeds""" - self.api.update_profile(include_entities='true') - - def test_update_profile_colors(self): - """Test updating profile colors succeeds""" - self.api.update_profile_colors(profile_background_color='3D3D3D') - - def test_list_blocks(self): - """Test listing users who are blocked by the authenticated user - succeeds""" - self.api.list_blocks() - - def test_list_block_ids(self): - """Test listing user ids who are blocked by the authenticated user - succeeds""" - self.api.list_block_ids() - - def test_create_block(self): - """Test blocking a user succeeds""" - self.api.create_block(screen_name='justinbieber') - - def test_destroy_block(self): - """Test unblocking a user succeeds""" - self.api.destroy_block(screen_name='justinbieber') - - def test_lookup_user(self): - """Test listing a number of user objects succeeds""" - self.api.lookup_user(screen_name='twitter,justinbieber') - - def test_show_user(self): - """Test showing one user works""" - self.api.show_user(screen_name='twitter') - - def test_search_users(self): - """Test that searching for users succeeds""" - self.api.search_users(q='Twitter API') - - def test_get_contributees(self): - """Test returning list of accounts the specified user can - contribute to succeeds""" - self.api.get_contributees(screen_name='TechCrunch') - - def test_get_contributors(self): - """Test returning list of accounts that contribute to the - authenticated user fails because we are not a Contributor account""" - self.assertRaises(TwythonError, self.api.get_contributors, - screen_name=screen_name) - - def test_remove_profile_banner(self): - """Test removing profile banner succeeds""" - self.api.remove_profile_banner() - - def test_get_profile_banner_sizes(self): - """Test getting list of profile banner sizes fails because - we have not uploaded a profile banner""" - self.assertRaises(TwythonError, self.api.get_profile_banner_sizes) - - # Suggested Users - def test_get_user_suggestions_by_slug(self): - """Test getting user suggestions by slug succeeds""" - self.api.get_user_suggestions_by_slug(slug='twitter') - - def test_get_user_suggestions(self): - """Test getting user suggestions succeeds""" - self.api.get_user_suggestions() - - def test_get_user_suggestions_statuses_by_slug(self): - """Test getting status of suggested users succeeds""" - self.api.get_user_suggestions_statuses_by_slug(slug='funny') - - # Favorites - def test_get_favorites(self): - """Test getting list of favorites for the authenticated - user succeeds""" - self.api.get_favorites() - - def test_create_and_destroy_favorite(self): - """Test creating and destroying a favorite on a tweet succeeds""" - self.api.create_favorite(id=test_tweet_id) - self.api.destroy_favorite(id=test_tweet_id) - - # Lists - def test_show_lists(self): - """Test show lists for specified user""" - self.api.show_lists(screen_name='twitter') - - def test_get_list_statuses(self): - """Test timeline of tweets authored by members of the - specified list succeeds""" - self.api.get_list_statuses(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name) - - def test_create_update_destroy_list_add_remove_list_members(self): - """Test create a list, adding and removing members then - deleting the list succeeds""" - the_list = self.api.create_list(name='Stuff %s' % int(time.time())) - list_id = the_list['id_str'] - - self.api.update_list(list_id=list_id, name='Stuff Renamed %s' % int(time.time())) - - screen_names = ['johncena', 'xbox'] - # Multi add/delete members - self.api.create_list_members(list_id=list_id, - screen_name=screen_names) - self.api.delete_list_members(list_id=list_id, - screen_name=screen_names) - - # Single add/delete member - self.api.add_list_member(list_id=list_id, screen_name='justinbieber') - self.api.delete_list_member(list_id=list_id, screen_name='justinbieber') - - self.api.delete_list(list_id=list_id) - - def test_get_list_memberships(self): - """Test list of memberhips the authenticated user succeeds""" - self.api.get_list_memberships() - - def test_get_list_subscribers(self): - """Test list of subscribers of a specific list succeeds""" - self.api.get_list_subscribers(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name) - - def test_subscribe_is_subbed_and_unsubscribe_to_list(self): - """Test subscribing, is a list sub and unsubbing to list succeeds""" - self.api.subscribe_to_list(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name) - # Returns 404 if user is not a subscriber - self.api.is_list_subscriber(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name, - screen_name=screen_name) - self.api.unsubscribe_from_list(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name) - - def test_is_list_member(self): - """Test returning if specified user is member of a list succeeds""" - # Returns 404 if not list member - self.api.is_list_member(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name, - screen_name='themattharris') - - def test_get_list_members(self): - """Test listing members of the specified list succeeds""" - self.api.get_list_members(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name) - - def test_get_specific_list(self): - """Test getting specific list succeeds""" - self.api.get_specific_list(slug=test_list_slug, - owner_screen_name=test_list_owner_screen_name) - - def test_get_list_subscriptions(self): - """Test collection of the lists the specified user is - subscribed to succeeds""" - self.api.get_list_subscriptions(screen_name='twitter') - - def test_show_owned_lists(self): - """Test collection of lists the specified user owns succeeds""" - self.api.show_owned_lists(screen_name='twitter') - - # Saved Searches - def test_get_saved_searches(self): - """Test getting list of saved searches for authenticated - user succeeds""" - self.api.get_saved_searches() - - def test_create_get_destroy_saved_search(self): - """Test getting list of saved searches for authenticated - user succeeds""" - saved_search = self.api.create_saved_search(query='#Twitter') - saved_search_id = saved_search['id_str'] - - self.api.show_saved_search(id=saved_search_id) - self.api.destroy_saved_search(id=saved_search_id) - - # Places & Geo - def test_get_geo_info(self): - """Test getting info about a geo location succeeds""" - self.api.get_geo_info(place_id='df51dec6f4ee2b2c') - - def test_reverse_geo_code(self): - """Test reversing geocode succeeds""" - self.api.reverse_geocode(lat='37.76893497', long='-122.42284884') - - def test_search_geo(self): - """Test search for places that can be attached - to a statuses/update succeeds""" - self.api.search_geo(query='Toronto') - - def test_get_similar_places(self): - """Test locates places near the given coordinates which - are similar in name succeeds""" - self.api.get_similar_places(lat='37', long='-122', name='Twitter HQ') - - # Trends - def test_get_place_trends(self): - """Test getting the top 10 trending topics for a specific - WOEID succeeds""" - self.api.get_place_trends(id=1) - - def test_get_available_trends(self): - """Test returning locations that Twitter has trending - topic information for succeeds""" - self.api.get_available_trends() - - def test_get_closest_trends(self): - """Test getting the locations that Twitter has trending topic - information for, closest to a specified location succeeds""" - self.api.get_closest_trends(lat='37', long='-122') - - # Help - def test_get_twitter_configuration(self): - """Test getting Twitter's configuration succeeds""" - self.api.get_twitter_configuration() - - def test_get_supported_languages(self): - """Test getting languages supported by Twitter succeeds""" - self.api.get_supported_languages() - - def test_privacy_policy(self): - """Test getting Twitter's Privacy Policy succeeds""" - self.api.get_privacy_policy() - - def test_get_tos(self): - """Test getting the Twitter Terms of Service succeeds""" - self.api.get_tos() - - def test_get_application_rate_limit_status(self): - """Test getting application rate limit status succeeds""" - self.oauth2_api.get_application_rate_limit_status() diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py new file mode 100644 index 0000000..f5ea56a --- /dev/null +++ b/tests/test_endpoints.py @@ -0,0 +1,429 @@ +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 +) + +import time +import unittest + + +class TwythonEndpointsTestCase(unittest.TestCase): + def setUp(self): + + client_args = { + 'headers': { + 'User-Agent': '__twython__ Test' + }, + 'allow_redirects': False + } + + oauth2_client_args = { + 'headers': {} # This is so we can hit coverage that Twython sets User-Agent for us if none is supplied + } + + 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) + + # Timelines + def test_get_mentions_timeline(self): + """Test returning mentions timeline for authenticated user succeeds""" + self.api.get_mentions_timeline() + + def test_get_user_timeline(self): + """Test returning timeline for authenticated user and random user + succeeds""" + self.api.get_user_timeline() # Authenticated User Timeline + self.api.get_user_timeline(screen_name='twitter') # Random User Timeline + + def test_get_protected_user_timeline_following(self): + """Test returning a protected user timeline who you are following + succeeds""" + self.api.get_user_timeline(screen_name=protected_twitter_1) + + def test_get_protected_user_timeline_not_following(self): + """Test returning a protected user timeline who you are not following + fails and raise a TwythonAuthError""" + self.assertRaises(TwythonAuthError, self.api.get_user_timeline, + screen_name=protected_twitter_2) + + def test_retweeted_of_me(self): + """Test that getting recent tweets by authenticated user that have + been retweeted by others succeeds""" + self.api.retweeted_of_me() + + def test_get_home_timeline(self): + """Test returning home timeline for authenticated user succeeds""" + self.api.get_home_timeline() + + # Tweets + def test_get_retweets(self): + """Test getting retweets of a specific tweet succeeds""" + self.api.get_retweets(id=test_tweet_id) + + def test_show_status(self): + """Test returning a single status details succeeds""" + self.api.show_status(id=test_tweet_id) + + def test_update_and_destroy_status(self): + """Test updating and deleting a status succeeds""" + status = self.api.update_status(status='Test post just to get deleted :( %s' % int(time.time())) + self.api.destroy_status(id=status['id_str']) + + def test_get_oembed_tweet(self): + """Test getting info to embed tweet on Third Party site succeeds""" + self.api.get_oembed_tweet(id='99530515043983360') + + def test_get_retweeters_ids(self): + """Test getting ids for people who retweeted a tweet succeeds""" + self.api.get_retweeters_ids(id='99530515043983360') + + # Search + def test_search(self): + """Test searching tweets succeeds""" + self.api.search(q='twitter') + + # Direct Messages + def test_get_direct_messages(self): + """Test getting the authenticated users direct messages succeeds""" + self.api.get_direct_messages() + + def test_get_sent_messages(self): + """Test getting the authenticated users direct messages they've + sent succeeds""" + self.api.get_sent_messages() + + def test_send_get_and_destroy_direct_message(self): + """Test sending, getting, then destory a direct message succeeds""" + message = self.api.send_direct_message(screen_name=protected_twitter_1, + text='Hey d00d! %s' % int(time.time())) + + self.api.get_direct_message(id=message['id_str']) + self.api.destroy_direct_message(id=message['id_str']) + + def test_send_direct_message_to_non_follower(self): + """Test sending a direct message to someone who doesn't follow you + fails""" + self.assertRaises(TwythonError, self.api.send_direct_message, + screen_name=protected_twitter_2, text='Yo, man! %s' % int(time.time())) + + # Friends & Followers + def test_get_user_ids_of_blocked_retweets(self): + """Test that collection of user_ids that the authenticated user does + not want to receive retweets from succeeds""" + self.api.get_user_ids_of_blocked_retweets(stringify_ids=True) + + def test_get_friends_ids(self): + """Test returning ids of users the authenticated user and then a random + user is following succeeds""" + self.api.get_friends_ids() + self.api.get_friends_ids(screen_name='twitter') + + def test_get_followers_ids(self): + """Test returning ids of users the authenticated user and then a random + user are followed by succeeds""" + self.api.get_followers_ids() + self.api.get_followers_ids(screen_name='twitter') + + def test_lookup_friendships(self): + """Test returning relationships of the authenticating user to the + comma-separated list of up to 100 screen_names or user_ids provided + succeeds""" + self.api.lookup_friendships(screen_name='twitter,ryanmcgrath') + + def test_get_incoming_friendship_ids(self): + """Test returning incoming friendship ids succeeds""" + self.api.get_incoming_friendship_ids() + + def test_get_outgoing_friendship_ids(self): + """Test returning outgoing friendship ids succeeds""" + self.api.get_outgoing_friendship_ids() + + def test_create_friendship(self): + """Test creating a friendship succeeds""" + self.api.create_friendship(screen_name='justinbieber') + + def test_destroy_friendship(self): + """Test destroying a friendship succeeds""" + self.api.destroy_friendship(screen_name='justinbieber') + + def test_update_friendship(self): + """Test updating friendships succeeds""" + self.api.update_friendship(screen_name=protected_twitter_1, + retweets='true') + + self.api.update_friendship(screen_name=protected_twitter_1, + retweets=False) + + def test_show_friendships(self): + """Test showing specific friendship succeeds""" + self.api.show_friendship(target_screen_name=protected_twitter_1) + + def test_get_friends_list(self): + """Test getting list of users authenticated user then random user is + following succeeds""" + self.api.get_friends_list() + self.api.get_friends_list(screen_name='twitter') + + def test_get_followers_list(self): + """Test getting list of users authenticated user then random user are + followed by succeeds""" + self.api.get_followers_list() + self.api.get_followers_list(screen_name='twitter') + + # Users + def test_get_account_settings(self): + """Test getting the authenticated user account settings succeeds""" + self.api.get_account_settings() + + def test_verify_credentials(self): + """Test representation of the authenticated user call succeeds""" + self.api.verify_credentials() + + def test_update_account_settings(self): + """Test updating a user account settings succeeds""" + self.api.update_account_settings(lang='en') + + def test_update_delivery_service(self): + """Test updating delivery settings fails because we don't have + a mobile number on the account""" + self.assertRaises(TwythonError, self.api.update_delivery_service, + device='none') + + def test_update_profile(self): + """Test updating profile succeeds""" + self.api.update_profile(include_entities='true') + + def test_update_profile_colors(self): + """Test updating profile colors succeeds""" + self.api.update_profile_colors(profile_background_color='3D3D3D') + + def test_list_blocks(self): + """Test listing users who are blocked by the authenticated user + succeeds""" + self.api.list_blocks() + + def test_list_block_ids(self): + """Test listing user ids who are blocked by the authenticated user + succeeds""" + self.api.list_block_ids() + + def test_create_block(self): + """Test blocking a user succeeds""" + self.api.create_block(screen_name='justinbieber') + + def test_destroy_block(self): + """Test unblocking a user succeeds""" + self.api.destroy_block(screen_name='justinbieber') + + def test_lookup_user(self): + """Test listing a number of user objects succeeds""" + self.api.lookup_user(screen_name='twitter,justinbieber') + + def test_show_user(self): + """Test showing one user works""" + self.api.show_user(screen_name='twitter') + + def test_search_users(self): + """Test that searching for users succeeds""" + self.api.search_users(q='Twitter API') + + def test_get_contributees(self): + """Test returning list of accounts the specified user can + contribute to succeeds""" + self.api.get_contributees(screen_name='TechCrunch') + + def test_get_contributors(self): + """Test returning list of accounts that contribute to the + authenticated user fails because we are not a Contributor account""" + self.assertRaises(TwythonError, self.api.get_contributors, + screen_name=screen_name) + + def test_remove_profile_banner(self): + """Test removing profile banner succeeds""" + self.api.remove_profile_banner() + + def test_get_profile_banner_sizes(self): + """Test getting list of profile banner sizes fails because + we have not uploaded a profile banner""" + self.assertRaises(TwythonError, self.api.get_profile_banner_sizes) + + # Suggested Users + def test_get_user_suggestions_by_slug(self): + """Test getting user suggestions by slug succeeds""" + self.api.get_user_suggestions_by_slug(slug='twitter') + + def test_get_user_suggestions(self): + """Test getting user suggestions succeeds""" + self.api.get_user_suggestions() + + def test_get_user_suggestions_statuses_by_slug(self): + """Test getting status of suggested users succeeds""" + self.api.get_user_suggestions_statuses_by_slug(slug='funny') + + # Favorites + def test_get_favorites(self): + """Test getting list of favorites for the authenticated + user succeeds""" + self.api.get_favorites() + + def test_create_and_destroy_favorite(self): + """Test creating and destroying a favorite on a tweet succeeds""" + self.api.create_favorite(id=test_tweet_id) + self.api.destroy_favorite(id=test_tweet_id) + + # Lists + def test_show_lists(self): + """Test show lists for specified user""" + self.api.show_lists(screen_name='twitter') + + def test_get_list_statuses(self): + """Test timeline of tweets authored by members of the + specified list succeeds""" + self.api.get_list_statuses(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) + + def test_create_update_destroy_list_add_remove_list_members(self): + """Test create a list, adding and removing members then + deleting the list succeeds""" + the_list = self.api.create_list(name='Stuff %s' % int(time.time())) + list_id = the_list['id_str'] + + self.api.update_list(list_id=list_id, name='Stuff Renamed %s' % int(time.time())) + + screen_names = ['johncena', 'xbox'] + # Multi add/delete members + self.api.create_list_members(list_id=list_id, + screen_name=screen_names) + self.api.delete_list_members(list_id=list_id, + screen_name=screen_names) + + # Single add/delete member + self.api.add_list_member(list_id=list_id, screen_name='justinbieber') + self.api.delete_list_member(list_id=list_id, screen_name='justinbieber') + + self.api.delete_list(list_id=list_id) + + def test_get_list_memberships(self): + """Test list of memberhips the authenticated user succeeds""" + self.api.get_list_memberships() + + def test_get_list_subscribers(self): + """Test list of subscribers of a specific list succeeds""" + self.api.get_list_subscribers(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) + + def test_subscribe_is_subbed_and_unsubscribe_to_list(self): + """Test subscribing, is a list sub and unsubbing to list succeeds""" + self.api.subscribe_to_list(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) + # Returns 404 if user is not a subscriber + self.api.is_list_subscriber(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name, + screen_name=screen_name) + self.api.unsubscribe_from_list(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) + + def test_is_list_member(self): + """Test returning if specified user is member of a list succeeds""" + # Returns 404 if not list member + self.api.is_list_member(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name, + screen_name='themattharris') + + def test_get_list_members(self): + """Test listing members of the specified list succeeds""" + self.api.get_list_members(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) + + def test_get_specific_list(self): + """Test getting specific list succeeds""" + self.api.get_specific_list(slug=test_list_slug, + owner_screen_name=test_list_owner_screen_name) + + def test_get_list_subscriptions(self): + """Test collection of the lists the specified user is + subscribed to succeeds""" + self.api.get_list_subscriptions(screen_name='twitter') + + def test_show_owned_lists(self): + """Test collection of lists the specified user owns succeeds""" + self.api.show_owned_lists(screen_name='twitter') + + # Saved Searches + def test_get_saved_searches(self): + """Test getting list of saved searches for authenticated + user succeeds""" + self.api.get_saved_searches() + + def test_create_get_destroy_saved_search(self): + """Test getting list of saved searches for authenticated + user succeeds""" + saved_search = self.api.create_saved_search(query='#Twitter') + saved_search_id = saved_search['id_str'] + + self.api.show_saved_search(id=saved_search_id) + self.api.destroy_saved_search(id=saved_search_id) + + # Places & Geo + def test_get_geo_info(self): + """Test getting info about a geo location succeeds""" + self.api.get_geo_info(place_id='df51dec6f4ee2b2c') + + def test_reverse_geo_code(self): + """Test reversing geocode succeeds""" + self.api.reverse_geocode(lat='37.76893497', long='-122.42284884') + + def test_search_geo(self): + """Test search for places that can be attached + to a statuses/update succeeds""" + self.api.search_geo(query='Toronto') + + def test_get_similar_places(self): + """Test locates places near the given coordinates which + are similar in name succeeds""" + self.api.get_similar_places(lat='37', long='-122', name='Twitter HQ') + + # Trends + def test_get_place_trends(self): + """Test getting the top 10 trending topics for a specific + WOEID succeeds""" + self.api.get_place_trends(id=1) + + def test_get_available_trends(self): + """Test returning locations that Twitter has trending + topic information for succeeds""" + self.api.get_available_trends() + + def test_get_closest_trends(self): + """Test getting the locations that Twitter has trending topic + information for, closest to a specified location succeeds""" + self.api.get_closest_trends(lat='37', long='-122') + + # Help + def test_get_twitter_configuration(self): + """Test getting Twitter's configuration succeeds""" + self.api.get_twitter_configuration() + + def test_get_supported_languages(self): + """Test getting languages supported by Twitter succeeds""" + self.api.get_supported_languages() + + def test_privacy_policy(self): + """Test getting Twitter's Privacy Policy succeeds""" + self.api.get_privacy_policy() + + def test_get_tos(self): + """Test getting the Twitter Terms of Service succeeds""" + self.api.get_tos() + + def test_get_application_rate_limit_status(self): + """Test getting application rate limit status succeeds""" + self.oauth2_api.get_application_rate_limit_status() -- 2.39.5 From 5304803f0955b1d0f4ef8457334f500402b60dee Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 11 Jan 2014 11:13:55 -0500 Subject: [PATCH 071/204] added docstrings for new tests and finished testing input arguments of request() --- tests/test_core.py | 95 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index d63d40d..b0cdc8d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -11,6 +11,11 @@ import time import unittest import responses +try: + import io.StringIO as StringIO +except ImportError: + import StringIO + class TwythonAPITestCase(unittest.TestCase): def setUp(self): @@ -33,8 +38,13 @@ class TwythonAPITestCase(unittest.TestCase): self.oauth2_api = Twython(app_key, access_token=access_token, client_args=oauth2_client_args) + def get_url(self, endpoint): + """Convenience function for mapping from endpoint to URL""" + return '%s/%s.json' % (self.api.api_url % self.api.api_version, endpoint) + @responses.activate def test_request_should_handle_full_endpoint(self): + """Test that request() accepts a full URL for the endpoint argument""" url = 'https://api.twitter.com/1.1/search/tweets.json' responses.add(responses.GET, url) @@ -45,16 +55,18 @@ class TwythonAPITestCase(unittest.TestCase): @responses.activate def test_request_should_handle_relative_endpoint(self): - url = 'https://api.twitter.com/1.1/search/tweets.json' + """Test that request() accepts a twitter endpoint name for the endpoint argument""" + url = 'https://api.twitter.com/2.0/search/tweets.json' responses.add(responses.GET, url) - self.api.request('search/tweets', version='1.1') + self.api.request('search/tweets', version='2.0') self.assertEqual(1, len(responses.calls)) self.assertEqual(url, responses.calls[0].request.url) @responses.activate def test_request_should_post_request_regardless_of_case(self): + """Test that request() accepts the HTTP method name regardless of case""" url = 'https://api.twitter.com/1.1/statuses/update.json' responses.add(responses.POST, url) @@ -62,70 +74,101 @@ class TwythonAPITestCase(unittest.TestCase): self.api.request(url, method='post') self.assertEqual(2, len(responses.calls)) - self.assertEqual(url, responses.calls[0].request.url) - self.assertEqual(url, responses.calls[1].request.url) + self.assertEqual('POST', responses.calls[0].request.method) + self.assertEqual('POST', responses.calls[1].request.method) @responses.activate def test_request_should_throw_exception_with_invalid_http_method(self): + """Test that request() throws an exception when an invalid HTTP method is passed""" #TODO(cash): should Twython catch the AttributeError and throw a TwythonError self.assertRaises(AttributeError, self.api.request, endpoint='search/tweets', method='INVALID') @responses.activate def test_request_should_encode_boolean_as_lowercase_string(self): - url = 'https://api.twitter.com/1.1/search/tweets.json' + """Test that request() encodes a boolean parameter as a lowercase string""" + endpoint = 'search/tweets' + url = self.get_url(endpoint) responses.add(responses.GET, url) - self.api.request('search/tweets', params={'include_entities': True}) - self.api.request('search/tweets', params={'include_entities': False}) + self.api.request(endpoint, params={'include_entities': True}) + self.api.request(endpoint, params={'include_entities': False}) self.assertEqual(url + '?include_entities=true', responses.calls[0].request.url) self.assertEqual(url + '?include_entities=false', responses.calls[1].request.url) @responses.activate def test_request_should_handle_string_or_number_parameter(self): - url = 'https://api.twitter.com/1.1/search/tweets.json' + """Test that request() encodes a numeric or string parameter correctly""" + endpoint = 'search/tweets' + url = self.get_url(endpoint) responses.add(responses.GET, url) - self.api.request('search/tweets', params={'lang': 'es'}) - self.api.request('search/tweets', params={'count': 50}) + self.api.request(endpoint, params={'lang': 'es'}) + self.api.request(endpoint, params={'count': 50}) self.assertEqual(url + '?lang=es', responses.calls[0].request.url) self.assertEqual(url + '?count=50', responses.calls[1].request.url) @responses.activate - def test_request_should_encode_string_list_as_string(self): - url = 'https://api.twitter.com/1.1/search/tweets.json' + def test_request_should_encode_list_of_strings_as_string(self): + """Test that request() encodes a list of strings as a comma-separated string""" + endpoint = 'search/tweets' + url = self.get_url(endpoint) location = ['37.781157', '-122.39872', '1mi'] responses.add(responses.GET, url) - self.api.request('search/tweets', params={'geocode': location}) + self.api.request(endpoint, params={'geocode': location}) + # requests url encodes the parameters so , is %2C self.assertEqual(url + '?geocode=37.781157%2C-122.39872%2C1mi', responses.calls[0].request.url) @responses.activate - def test_request_should_encode_number_list_as_string(self): - url = 'https://api.twitter.com/1.1/search/tweets.json' + def test_request_should_encode_numeric_list_as_string(self): + """Test that request() encodes a list of numbers as a comma-separated string""" + endpoint = 'search/tweets' + url = self.get_url(endpoint) location = [37.781157, -122.39872, '1mi'] responses.add(responses.GET, url) - self.api.request('search/tweets', params={'geocode': location}) + self.api.request(endpoint, params={'geocode': location}) self.assertEqual(url + '?geocode=37.781157%2C-122.39872%2C1mi', responses.calls[0].request.url) @responses.activate def test_request_should_ignore_bad_parameter(self): - url = 'https://api.twitter.com/1.1/search/tweets.json' + """Test that request() ignores unexpected parameter types""" + endpoint = 'search/tweets' + url = self.get_url(endpoint) responses.add(responses.GET, url) - self.api.request('search/tweets', params={'geocode': self}) + self.api.request(endpoint, params={'geocode': self}) self.assertEqual(url, responses.calls[0].request.url) - def test_construct_api_url(self): - """Test constructing a Twitter API url works as we expect""" - url = 'https://api.twitter.com/1.1/search/tweets.json' - constructed_url = self.api.construct_api_url(url, q='#twitter') - self.assertEqual(constructed_url, 'https://api.twitter.com/1.1/search/tweets.json?q=%23twitter') + @responses.activate + def test_request_should_handle_file_as_parameter(self): + """Test that request() pulls a file out of params for requests lib""" + endpoint = 'account/update_profile_image' + url = self.get_url(endpoint) + responses.add(responses.POST, url) + + mock_file = StringIO.StringIO("Twython test image") + self.api.request(endpoint, method='POST', params={'image': mock_file}) + + self.assertIn('filename="image"', responses.calls[0].request.body) + self.assertIn("Twython test image", responses.calls[0].request.body) + + @responses.activate + def test_request_should_put_params_in_body_when_post(self): + """Test that request() passes params as data when the request is a POST""" + endpoint = 'statuses/update' + url = self.get_url(endpoint) + responses.add(responses.POST, url) + + self.api.request(endpoint, method='POST', params={'status': 'this is a test'}) + + self.assertIn('status=this+is+a+test', responses.calls[0].request.body) + self.assertNotIn('status=this+is+a+test', responses.calls[0].request.url) def test_get(self): """Test Twython generic GET request works""" @@ -138,6 +181,12 @@ class TwythonAPITestCase(unittest.TestCase): status = self.api.post(update_url, params={'status': 'I love Twython! %s' % int(time.time())}) self.api.post('statuses/destroy/%s' % status['id_str']) + def test_construct_api_url(self): + """Test constructing a Twitter API url works as we expect""" + url = 'https://api.twitter.com/1.1/search/tweets.json' + constructed_url = self.api.construct_api_url(url, q='#twitter') + self.assertEqual(constructed_url, 'https://api.twitter.com/1.1/search/tweets.json?q=%23twitter') + def test_get_lastfunction_header(self): """Test getting last specific header of the last API call works""" self.api.get('statuses/home_timeline') -- 2.39.5 From fc55791cbfb6d3c5f190d5c985afcd14e7daaad8 Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 11 Jan 2014 15:37:47 -0500 Subject: [PATCH 072/204] updated remaining tests in test_core.py and removed cursor test (which will be replaced) --- tests/test_core.py | 124 +++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 66 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index b0cdc8d..e09556d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,42 +1,27 @@ 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 + test_tweet_object, test_tweet_html ) -import time import unittest import responses +import requests try: import io.StringIO as StringIO except ImportError: import StringIO +try: + import unittest.mock as mock +except ImportError: + import mock + class TwythonAPITestCase(unittest.TestCase): def setUp(self): - - client_args = { - 'headers': { - 'User-Agent': '__twython__ Test' - }, - 'allow_redirects': False - } - - oauth2_client_args = { - 'headers': {} # This is so we can hit coverage that Twython sets User-Agent for us if none is supplied - } - - 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) + self.api = Twython('', '', '', '') def get_url(self, endpoint): """Convenience function for mapping from endpoint to URL""" @@ -170,53 +155,64 @@ class TwythonAPITestCase(unittest.TestCase): self.assertIn('status=this+is+a+test', responses.calls[0].request.body) self.assertNotIn('status=this+is+a+test', responses.calls[0].request.url) - def test_get(self): + @responses.activate + def test_get_uses_get_method(self): """Test Twython generic GET request works""" - self.api.get('account/verify_credentials') + endpoint = 'account/verify_credentials' + url = self.get_url(endpoint) + responses.add(responses.GET, url) - def test_post(self): - """Test Twython generic POST request works, with a full url and - with just an endpoint""" - update_url = 'https://api.twitter.com/1.1/statuses/update.json' - status = self.api.post(update_url, params={'status': 'I love Twython! %s' % int(time.time())}) - self.api.post('statuses/destroy/%s' % status['id_str']) + self.api.get(endpoint) + self.assertEqual(1, len(responses.calls)) + self.assertEqual(url, responses.calls[0].request.url) + + @responses.activate + def test_post_uses_post_method(self): + """Test Twython generic POST request works""" + endpoint = 'statuses/update' + url = self.get_url(endpoint) + responses.add(responses.POST, url) + + self.api.post(endpoint, params={'status': 'I love Twython!'}) + + self.assertEqual(1, len(responses.calls)) + self.assertEqual(url, responses.calls[0].request.url) + + def test_raise_twython_error_on_request_exception(self): + """Test if TwythonError is raised by a RequestException""" + with mock.patch.object(requests.Session, 'get') as get_mock: + # mocking an ssl cert error + get_mock.side_effect = requests.RequestException("hostname 'example.com' doesn't match ...") + self.assertRaises(TwythonError, self.api.get, 'https://example.com') + + @responses.activate + def test_get_lastfunction_header_should_return_header(self): + """Test getting last specific header of the last API call works""" + endpoint = 'statuses/home_timeline' + url = self.get_url(endpoint) + responses.add(responses.GET, url, adding_headers={'x-rate-limit-remaining': 37}) + + self.api.get(endpoint) + + value = self.api.get_lastfunction_header('x-rate-limit-remaining') + self.assertEqual(37, value) + value2 = self.api.get_lastfunction_header('does-not-exist') + self.assertIsNone(value2) + value3 = self.api.get_lastfunction_header('not-there-either', 96) + self.assertEqual(96, value3) + + def test_get_lastfunction_header_should_raise_error_when_no_previous_call(self): + """Test attempting to get a header when no API call was made raises a TwythonError""" + self.assertRaises(TwythonError, self.api.get_lastfunction_header, 'no-api-call-was-made') + + # Static methods def test_construct_api_url(self): """Test constructing a Twitter API url works as we expect""" url = 'https://api.twitter.com/1.1/search/tweets.json' constructed_url = self.api.construct_api_url(url, q='#twitter') self.assertEqual(constructed_url, 'https://api.twitter.com/1.1/search/tweets.json?q=%23twitter') - def test_get_lastfunction_header(self): - """Test getting last specific header of the last API call works""" - self.api.get('statuses/home_timeline') - self.api.get_lastfunction_header('x-rate-limit-remaining') - - def test_get_lastfunction_header_not_present(self): - """Test getting specific header that does not exist from the last call returns None""" - self.api.get('statuses/home_timeline') - header = self.api.get_lastfunction_header('does-not-exist') - self.assertEqual(header, None) - - def test_get_lastfunction_header_no_last_api_call(self): - """Test attempting to get a header when no API call was made raises a TwythonError""" - self.assertRaises(TwythonError, self.api.get_lastfunction_header, - 'no-api-call-was-made') - - def test_cursor(self): - """Test looping through the generator results works, at least once that is""" - search = self.api.cursor(self.api.search, q='twitter', count=1) - counter = 0 - while counter < 2: - counter += 1 - result = next(search) - new_id_str = int(result['id_str']) - if counter == 1: - prev_id_str = new_id_str - time.sleep(1) # Give time for another tweet to come into search - if counter == 2: - self.assertTrue(new_id_str > prev_id_str) - def test_encode(self): """Test encoding UTF-8 works""" self.api.encode('Twython is awesome!') @@ -236,10 +232,6 @@ class TwythonAPITestCase(unittest.TestCase): def test_html_for_tweet_short_url(self): """Test using expanded url in HTML for Tweet displays full urls""" tweet_text = self.api.html_for_tweet(test_tweet_object, False) - # Make sure HTML doesn't contain the display OR exapanded url + # Make sure HTML doesn't contain the display OR expanded url self.assertTrue(not 'http://google.com' in tweet_text) self.assertTrue(not 'google.com' in tweet_text) - - def test_raise_error_on_bad_ssl_cert(self): - """Test TwythonError is raised by a RequestException when an actual HTTP happens""" - self.assertRaises(TwythonError, self.api.get, 'https://example.com') -- 2.39.5 From c449e3f8e185be383f07c652561433f22b073838 Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 11 Jan 2014 15:45:45 -0500 Subject: [PATCH 073/204] skipping tests that were failing because of external dependency on Twitter --- tests/test_auth.py | 9 +++++ tests/test_core.py | 6 +++- tests/test_endpoints.py | 75 +++++++++++++++++++++++++++++++++++++++++ tests/test_streaming.py | 5 +++ 4 files changed, 94 insertions(+), 1 deletion(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index edefe07..3b2a713 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -16,48 +16,57 @@ class TwythonAuthTestCase(unittest.TestCase): self.oauth2_bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET', oauth_version=2) + @unittest.skip('skipping non-updated test') def test_get_authentication_tokens(self): """Test getting authentication tokens works""" self.api.get_authentication_tokens(callback_url='http://google.com/', force_login=True, screen_name=screen_name) + @unittest.skip('skipping non-updated test') def test_get_authentication_tokens_bad_tokens(self): """Test getting authentication tokens with bad tokens raises TwythonAuthError""" self.assertRaises(TwythonAuthError, self.bad_api.get_authentication_tokens, callback_url='http://google.com/') + @unittest.skip('skipping non-updated test') def test_get_authorized_tokens_bad_tokens(self): """Test getting final tokens fails with wrong tokens""" self.assertRaises(TwythonError, self.bad_api.get_authorized_tokens, 'BAD_OAUTH_VERIFIER') + @unittest.skip('skipping non-updated test') def test_get_authorized_tokens_invalid_or_expired_tokens(self): """Test getting final token fails when invalid or expired tokens have been passed""" self.assertRaises(TwythonError, self.bad_api_invalid_tokens.get_authorized_tokens, 'BAD_OAUTH_VERIFIER') + @unittest.skip('skipping non-updated test') def test_get_authentication_tokens_raises_error_when_oauth2(self): """Test when API is set for OAuth 2, get_authentication_tokens raises a TwythonError""" self.assertRaises(TwythonError, self.oauth2_api.get_authentication_tokens) + @unittest.skip('skipping non-updated test') def test_get_authorization_tokens_raises_error_when_oauth2(self): """Test when API is set for OAuth 2, get_authorized_tokens raises a TwythonError""" self.assertRaises(TwythonError, self.oauth2_api.get_authorized_tokens, 'BAD_OAUTH_VERIFIER') + @unittest.skip('skipping non-updated test') def test_obtain_access_token(self): """Test obtaining an Application Only OAuth 2 access token succeeds""" self.oauth2_api.obtain_access_token() + @unittest.skip('skipping non-updated test') def test_obtain_access_token_bad_tokens(self): """Test obtaining an Application Only OAuth 2 access token using bad app tokens fails""" self.assertRaises(TwythonAuthError, self.oauth2_bad_api.obtain_access_token) + @unittest.skip('skipping non-updated test') def test_obtain_access_token_raises_error_when_oauth1(self): """Test when API is set for OAuth 1, obtain_access_token raises a TwythonError""" diff --git a/tests/test_core.py b/tests/test_core.py index e09556d..17d1fce 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4,7 +4,11 @@ from .config import ( test_tweet_object, test_tweet_html ) -import unittest +try: + import unittest2 as unittest +except ImportError: + import unittest + import responses import requests diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index f5ea56a..edeacfa 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -33,73 +33,88 @@ class TwythonEndpointsTestCase(unittest.TestCase): client_args=oauth2_client_args) # Timelines + @unittest.skip('skipping non-updated test') def test_get_mentions_timeline(self): """Test returning mentions timeline for authenticated user succeeds""" self.api.get_mentions_timeline() + @unittest.skip('skipping non-updated test') def test_get_user_timeline(self): """Test returning timeline for authenticated user and random user succeeds""" self.api.get_user_timeline() # Authenticated User Timeline self.api.get_user_timeline(screen_name='twitter') # Random User Timeline + @unittest.skip('skipping non-updated test') def test_get_protected_user_timeline_following(self): """Test returning a protected user timeline who you are following succeeds""" self.api.get_user_timeline(screen_name=protected_twitter_1) + @unittest.skip('skipping non-updated test') def test_get_protected_user_timeline_not_following(self): """Test returning a protected user timeline who you are not following fails and raise a TwythonAuthError""" self.assertRaises(TwythonAuthError, self.api.get_user_timeline, screen_name=protected_twitter_2) + @unittest.skip('skipping non-updated test') def test_retweeted_of_me(self): """Test that getting recent tweets by authenticated user that have been retweeted by others succeeds""" self.api.retweeted_of_me() + @unittest.skip('skipping non-updated test') def test_get_home_timeline(self): """Test returning home timeline for authenticated user succeeds""" self.api.get_home_timeline() # Tweets + @unittest.skip('skipping non-updated test') def test_get_retweets(self): """Test getting retweets of a specific tweet succeeds""" self.api.get_retweets(id=test_tweet_id) + @unittest.skip('skipping non-updated test') def test_show_status(self): """Test returning a single status details succeeds""" self.api.show_status(id=test_tweet_id) + @unittest.skip('skipping non-updated test') def test_update_and_destroy_status(self): """Test updating and deleting a status succeeds""" status = self.api.update_status(status='Test post just to get deleted :( %s' % int(time.time())) self.api.destroy_status(id=status['id_str']) + @unittest.skip('skipping non-updated test') def test_get_oembed_tweet(self): """Test getting info to embed tweet on Third Party site succeeds""" self.api.get_oembed_tweet(id='99530515043983360') + @unittest.skip('skipping non-updated test') def test_get_retweeters_ids(self): """Test getting ids for people who retweeted a tweet succeeds""" self.api.get_retweeters_ids(id='99530515043983360') # Search + @unittest.skip('skipping non-updated test') def test_search(self): """Test searching tweets succeeds""" self.api.search(q='twitter') # Direct Messages + @unittest.skip('skipping non-updated test') def test_get_direct_messages(self): """Test getting the authenticated users direct messages succeeds""" self.api.get_direct_messages() + @unittest.skip('skipping non-updated test') def test_get_sent_messages(self): """Test getting the authenticated users direct messages they've sent succeeds""" self.api.get_sent_messages() + @unittest.skip('skipping non-updated test') def test_send_get_and_destroy_direct_message(self): """Test sending, getting, then destory a direct message succeeds""" message = self.api.send_direct_message(screen_name=protected_twitter_1, @@ -108,6 +123,7 @@ class TwythonEndpointsTestCase(unittest.TestCase): self.api.get_direct_message(id=message['id_str']) self.api.destroy_direct_message(id=message['id_str']) + @unittest.skip('skipping non-updated test') def test_send_direct_message_to_non_follower(self): """Test sending a direct message to someone who doesn't follow you fails""" @@ -115,45 +131,54 @@ class TwythonEndpointsTestCase(unittest.TestCase): screen_name=protected_twitter_2, text='Yo, man! %s' % int(time.time())) # Friends & Followers + @unittest.skip('skipping non-updated test') def test_get_user_ids_of_blocked_retweets(self): """Test that collection of user_ids that the authenticated user does not want to receive retweets from succeeds""" self.api.get_user_ids_of_blocked_retweets(stringify_ids=True) + @unittest.skip('skipping non-updated test') def test_get_friends_ids(self): """Test returning ids of users the authenticated user and then a random user is following succeeds""" self.api.get_friends_ids() self.api.get_friends_ids(screen_name='twitter') + @unittest.skip('skipping non-updated test') def test_get_followers_ids(self): """Test returning ids of users the authenticated user and then a random user are followed by succeeds""" self.api.get_followers_ids() self.api.get_followers_ids(screen_name='twitter') + @unittest.skip('skipping non-updated test') def test_lookup_friendships(self): """Test returning relationships of the authenticating user to the comma-separated list of up to 100 screen_names or user_ids provided succeeds""" self.api.lookup_friendships(screen_name='twitter,ryanmcgrath') + @unittest.skip('skipping non-updated test') def test_get_incoming_friendship_ids(self): """Test returning incoming friendship ids succeeds""" self.api.get_incoming_friendship_ids() + @unittest.skip('skipping non-updated test') def test_get_outgoing_friendship_ids(self): """Test returning outgoing friendship ids succeeds""" self.api.get_outgoing_friendship_ids() + @unittest.skip('skipping non-updated test') def test_create_friendship(self): """Test creating a friendship succeeds""" self.api.create_friendship(screen_name='justinbieber') + @unittest.skip('skipping non-updated test') def test_destroy_friendship(self): """Test destroying a friendship succeeds""" self.api.destroy_friendship(screen_name='justinbieber') + @unittest.skip('skipping non-updated test') def test_update_friendship(self): """Test updating friendships succeeds""" self.api.update_friendship(screen_name=protected_twitter_1, @@ -162,16 +187,19 @@ class TwythonEndpointsTestCase(unittest.TestCase): self.api.update_friendship(screen_name=protected_twitter_1, retweets=False) + @unittest.skip('skipping non-updated test') def test_show_friendships(self): """Test showing specific friendship succeeds""" self.api.show_friendship(target_screen_name=protected_twitter_1) + @unittest.skip('skipping non-updated test') def test_get_friends_list(self): """Test getting list of users authenticated user then random user is following succeeds""" self.api.get_friends_list() self.api.get_friends_list(screen_name='twitter') + @unittest.skip('skipping non-updated test') def test_get_followers_list(self): """Test getting list of users authenticated user then random user are followed by succeeds""" @@ -179,117 +207,142 @@ class TwythonEndpointsTestCase(unittest.TestCase): self.api.get_followers_list(screen_name='twitter') # Users + @unittest.skip('skipping non-updated test') def test_get_account_settings(self): """Test getting the authenticated user account settings succeeds""" self.api.get_account_settings() + @unittest.skip('skipping non-updated test') def test_verify_credentials(self): """Test representation of the authenticated user call succeeds""" self.api.verify_credentials() + @unittest.skip('skipping non-updated test') def test_update_account_settings(self): """Test updating a user account settings succeeds""" self.api.update_account_settings(lang='en') + @unittest.skip('skipping non-updated test') def test_update_delivery_service(self): """Test updating delivery settings fails because we don't have a mobile number on the account""" self.assertRaises(TwythonError, self.api.update_delivery_service, device='none') + @unittest.skip('skipping non-updated test') def test_update_profile(self): """Test updating profile succeeds""" self.api.update_profile(include_entities='true') + @unittest.skip('skipping non-updated test') def test_update_profile_colors(self): """Test updating profile colors succeeds""" self.api.update_profile_colors(profile_background_color='3D3D3D') + @unittest.skip('skipping non-updated test') def test_list_blocks(self): """Test listing users who are blocked by the authenticated user succeeds""" self.api.list_blocks() + @unittest.skip('skipping non-updated test') def test_list_block_ids(self): """Test listing user ids who are blocked by the authenticated user succeeds""" self.api.list_block_ids() + @unittest.skip('skipping non-updated test') def test_create_block(self): """Test blocking a user succeeds""" self.api.create_block(screen_name='justinbieber') + @unittest.skip('skipping non-updated test') def test_destroy_block(self): """Test unblocking a user succeeds""" self.api.destroy_block(screen_name='justinbieber') + @unittest.skip('skipping non-updated test') def test_lookup_user(self): """Test listing a number of user objects succeeds""" self.api.lookup_user(screen_name='twitter,justinbieber') + @unittest.skip('skipping non-updated test') def test_show_user(self): """Test showing one user works""" self.api.show_user(screen_name='twitter') + @unittest.skip('skipping non-updated test') def test_search_users(self): """Test that searching for users succeeds""" self.api.search_users(q='Twitter API') + @unittest.skip('skipping non-updated test') def test_get_contributees(self): """Test returning list of accounts the specified user can contribute to succeeds""" self.api.get_contributees(screen_name='TechCrunch') + @unittest.skip('skipping non-updated test') def test_get_contributors(self): """Test returning list of accounts that contribute to the authenticated user fails because we are not a Contributor account""" self.assertRaises(TwythonError, self.api.get_contributors, screen_name=screen_name) + @unittest.skip('skipping non-updated test') def test_remove_profile_banner(self): """Test removing profile banner succeeds""" self.api.remove_profile_banner() + @unittest.skip('skipping non-updated test') def test_get_profile_banner_sizes(self): """Test getting list of profile banner sizes fails because we have not uploaded a profile banner""" self.assertRaises(TwythonError, self.api.get_profile_banner_sizes) # Suggested Users + @unittest.skip('skipping non-updated test') def test_get_user_suggestions_by_slug(self): """Test getting user suggestions by slug succeeds""" self.api.get_user_suggestions_by_slug(slug='twitter') + @unittest.skip('skipping non-updated test') def test_get_user_suggestions(self): """Test getting user suggestions succeeds""" self.api.get_user_suggestions() + @unittest.skip('skipping non-updated test') def test_get_user_suggestions_statuses_by_slug(self): """Test getting status of suggested users succeeds""" self.api.get_user_suggestions_statuses_by_slug(slug='funny') # Favorites + @unittest.skip('skipping non-updated test') def test_get_favorites(self): """Test getting list of favorites for the authenticated user succeeds""" self.api.get_favorites() + @unittest.skip('skipping non-updated test') def test_create_and_destroy_favorite(self): """Test creating and destroying a favorite on a tweet succeeds""" self.api.create_favorite(id=test_tweet_id) self.api.destroy_favorite(id=test_tweet_id) # Lists + @unittest.skip('skipping non-updated test') def test_show_lists(self): """Test show lists for specified user""" self.api.show_lists(screen_name='twitter') + @unittest.skip('skipping non-updated test') def test_get_list_statuses(self): """Test timeline of tweets authored by members of the specified list succeeds""" self.api.get_list_statuses(slug=test_list_slug, owner_screen_name=test_list_owner_screen_name) + @unittest.skip('skipping non-updated test') def test_create_update_destroy_list_add_remove_list_members(self): """Test create a list, adding and removing members then deleting the list succeeds""" @@ -311,15 +364,18 @@ class TwythonEndpointsTestCase(unittest.TestCase): self.api.delete_list(list_id=list_id) + @unittest.skip('skipping non-updated test') def test_get_list_memberships(self): """Test list of memberhips the authenticated user succeeds""" self.api.get_list_memberships() + @unittest.skip('skipping non-updated test') def test_get_list_subscribers(self): """Test list of subscribers of a specific list succeeds""" self.api.get_list_subscribers(slug=test_list_slug, owner_screen_name=test_list_owner_screen_name) + @unittest.skip('skipping non-updated test') def test_subscribe_is_subbed_and_unsubscribe_to_list(self): """Test subscribing, is a list sub and unsubbing to list succeeds""" self.api.subscribe_to_list(slug=test_list_slug, @@ -331,6 +387,7 @@ class TwythonEndpointsTestCase(unittest.TestCase): self.api.unsubscribe_from_list(slug=test_list_slug, owner_screen_name=test_list_owner_screen_name) + @unittest.skip('skipping non-updated test') def test_is_list_member(self): """Test returning if specified user is member of a list succeeds""" # Returns 404 if not list member @@ -338,31 +395,37 @@ class TwythonEndpointsTestCase(unittest.TestCase): owner_screen_name=test_list_owner_screen_name, screen_name='themattharris') + @unittest.skip('skipping non-updated test') def test_get_list_members(self): """Test listing members of the specified list succeeds""" self.api.get_list_members(slug=test_list_slug, owner_screen_name=test_list_owner_screen_name) + @unittest.skip('skipping non-updated test') def test_get_specific_list(self): """Test getting specific list succeeds""" self.api.get_specific_list(slug=test_list_slug, owner_screen_name=test_list_owner_screen_name) + @unittest.skip('skipping non-updated test') def test_get_list_subscriptions(self): """Test collection of the lists the specified user is subscribed to succeeds""" self.api.get_list_subscriptions(screen_name='twitter') + @unittest.skip('skipping non-updated test') def test_show_owned_lists(self): """Test collection of lists the specified user owns succeeds""" self.api.show_owned_lists(screen_name='twitter') # Saved Searches + @unittest.skip('skipping non-updated test') def test_get_saved_searches(self): """Test getting list of saved searches for authenticated user succeeds""" self.api.get_saved_searches() + @unittest.skip('skipping non-updated test') def test_create_get_destroy_saved_search(self): """Test getting list of saved searches for authenticated user succeeds""" @@ -373,57 +436,69 @@ class TwythonEndpointsTestCase(unittest.TestCase): self.api.destroy_saved_search(id=saved_search_id) # Places & Geo + @unittest.skip('skipping non-updated test') def test_get_geo_info(self): """Test getting info about a geo location succeeds""" self.api.get_geo_info(place_id='df51dec6f4ee2b2c') + @unittest.skip('skipping non-updated test') def test_reverse_geo_code(self): """Test reversing geocode succeeds""" self.api.reverse_geocode(lat='37.76893497', long='-122.42284884') + @unittest.skip('skipping non-updated test') def test_search_geo(self): """Test search for places that can be attached to a statuses/update succeeds""" self.api.search_geo(query='Toronto') + @unittest.skip('skipping non-updated test') def test_get_similar_places(self): """Test locates places near the given coordinates which are similar in name succeeds""" self.api.get_similar_places(lat='37', long='-122', name='Twitter HQ') # Trends + @unittest.skip('skipping non-updated test') def test_get_place_trends(self): """Test getting the top 10 trending topics for a specific WOEID succeeds""" self.api.get_place_trends(id=1) + @unittest.skip('skipping non-updated test') def test_get_available_trends(self): """Test returning locations that Twitter has trending topic information for succeeds""" self.api.get_available_trends() + @unittest.skip('skipping non-updated test') def test_get_closest_trends(self): """Test getting the locations that Twitter has trending topic information for, closest to a specified location succeeds""" self.api.get_closest_trends(lat='37', long='-122') # Help + @unittest.skip('skipping non-updated test') def test_get_twitter_configuration(self): """Test getting Twitter's configuration succeeds""" self.api.get_twitter_configuration() + @unittest.skip('skipping non-updated test') def test_get_supported_languages(self): """Test getting languages supported by Twitter succeeds""" self.api.get_supported_languages() + @unittest.skip('skipping non-updated test') def test_privacy_policy(self): """Test getting Twitter's Privacy Policy succeeds""" self.api.get_privacy_policy() + @unittest.skip('skipping non-updated test') def test_get_tos(self): """Test getting the Twitter Terms of Service succeeds""" self.api.get_tos() + @unittest.skip('skipping non-updated test') def test_get_application_rate_limit_status(self): """Test getting application rate limit status succeeds""" self.oauth2_api.get_application_rate_limit_status() diff --git a/tests/test_streaming.py b/tests/test_streaming.py index 1b8db6c..d0cf20a 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -29,19 +29,24 @@ class TwythonStreamTestCase(unittest.TestCase): oauth_token, oauth_token_secret, client_args=client_args) + @unittest.skip('skipping non-updated test') def test_stream_status_filter(self): self.api.statuses.filter(track='twitter') + @unittest.skip('skipping non-updated test') def test_stream_status_sample(self): self.api.statuses.sample() + @unittest.skip('skipping non-updated test') def test_stream_status_firehose(self): self.assertRaises(TwythonStreamError, self.api.statuses.firehose, track='twitter') + @unittest.skip('skipping non-updated test') def test_stream_site(self): self.assertRaises(TwythonStreamError, self.api.site, follow='twitter') + @unittest.skip('skipping non-updated test') def test_stream_user(self): self.api.user(track='twitter') -- 2.39.5 From 30ff4319e2325cc6aaeb8b9ad4e404e087b0cd23 Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 11 Jan 2014 15:52:46 -0500 Subject: [PATCH 074/204] updated unit tests and travis config for python 2.6 and 3 --- .travis.yml | 4 +++- tests/config.py | 6 ++++++ tests/test_auth.py | 4 +--- tests/test_core.py | 18 +++++++----------- tests/test_endpoints.py | 3 +-- tests/test_streaming.py | 4 +--- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8411c02..91ebf1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,9 @@ env: - TEST_LIST_SLUG=team - TEST_LIST_OWNER_SCREEN_NAME=twitterapi - ACCESS_TOKEN_B64=U2FsdGVkX18QdBhvMNshM4PGy04tU3HLwKP+nNSoNZHKsvGLjELcWEXN2LIu/T+yngX1vGONf9lo14ElnfS4k7sfhiru8phR4+rZuBVP3bDvC2A6fXJuhuLqNhBrWqg32WQewvxLWDWBoKmnvRHg5b74GHh+IN/12tU0cBF2HK8= -install: pip install -r requirements.txt +install: + - pip install -r requirements.txt + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing notifications: email: false diff --git a/tests/config.py b/tests/config.py index 4e8895e..d26b6e1 100644 --- a/tests/config.py +++ b/tests/config.py @@ -1,5 +1,11 @@ import os +import sys +if sys.version_info[0] == 2 and sys.version_info[1] == 6: + import unittest2 as unittest +else: + import unittest + app_key = os.environ.get('APP_KEY') app_secret = os.environ.get('APP_SECRET') oauth_token = os.environ.get('OAUTH_TOKEN') diff --git a/tests/test_auth.py b/tests/test_auth.py index 3b2a713..7de4160 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,8 +1,6 @@ from twython import Twython, TwythonError, TwythonAuthError -from .config import app_key, app_secret, screen_name - -import unittest +from .config import app_key, app_secret, screen_name, unittest class TwythonAuthTestCase(unittest.TestCase): diff --git a/tests/test_core.py b/tests/test_core.py index 17d1fce..be22925 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,21 +1,17 @@ from twython import Twython, TwythonError, TwythonAuthError from .config import ( - test_tweet_object, test_tweet_html + test_tweet_object, test_tweet_html, unittest ) -try: - import unittest2 as unittest -except ImportError: - import unittest - import responses import requests -try: - import io.StringIO as StringIO -except ImportError: - import StringIO +from twython.compat import is_py2 +if is_py2: + from StringIO import StringIO +else: + from io import StringIO try: import unittest.mock as mock @@ -141,7 +137,7 @@ class TwythonAPITestCase(unittest.TestCase): url = self.get_url(endpoint) responses.add(responses.POST, url) - mock_file = StringIO.StringIO("Twython test image") + mock_file = StringIO("Twython test image") self.api.request(endpoint, method='POST', params={'image': mock_file}) self.assertIn('filename="image"', responses.calls[0].request.body) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index edeacfa..6204a57 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -4,11 +4,10 @@ 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 + access_token, test_tweet_object, test_tweet_html, unittest ) import time -import unittest class TwythonEndpointsTestCase(unittest.TestCase): diff --git a/tests/test_streaming.py b/tests/test_streaming.py index d0cf20a..9db7059 100644 --- a/tests/test_streaming.py +++ b/tests/test_streaming.py @@ -1,11 +1,9 @@ from twython import TwythonStreamer, TwythonStreamError from .config import ( - app_key, app_secret, oauth_token, oauth_token_secret + app_key, app_secret, oauth_token, oauth_token_secret, unittest ) -import unittest - class TwythonStreamTestCase(unittest.TestCase): def setUp(self): -- 2.39.5 From 9f7d38181ed65b0bba05d205689877cd9d70f4d9 Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 11 Jan 2014 18:08:52 -0500 Subject: [PATCH 075/204] python 3.3 workaround for dropbox/responses issue --- tests/test_core.py | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index be22925..ddf5256 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -27,11 +27,22 @@ class TwythonAPITestCase(unittest.TestCase): """Convenience function for mapping from endpoint to URL""" return '%s/%s.json' % (self.api.api_url % self.api.api_version, endpoint) + def register_response(self, method, url, body='', match_querystring=False, + status=200, adding_headers=None, stream=False, + content_type='text/plain'): + """Temporary function to work around python 3.3 issue with responses""" + # responses uses BytesIO to hold the body so it needs to be in bytes + if not is_py2: + body = bytes(body, 'UTF-8') + + responses.add(method, url, body, match_querystring, + status, adding_headers, stream, content_type) + @responses.activate def test_request_should_handle_full_endpoint(self): """Test that request() accepts a full URL for the endpoint argument""" url = 'https://api.twitter.com/1.1/search/tweets.json' - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request(url) @@ -42,7 +53,7 @@ class TwythonAPITestCase(unittest.TestCase): def test_request_should_handle_relative_endpoint(self): """Test that request() accepts a twitter endpoint name for the endpoint argument""" url = 'https://api.twitter.com/2.0/search/tweets.json' - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request('search/tweets', version='2.0') @@ -53,7 +64,7 @@ class TwythonAPITestCase(unittest.TestCase): def test_request_should_post_request_regardless_of_case(self): """Test that request() accepts the HTTP method name regardless of case""" url = 'https://api.twitter.com/1.1/statuses/update.json' - responses.add(responses.POST, url) + self.register_response(responses.POST, url) self.api.request(url, method='POST') self.api.request(url, method='post') @@ -73,7 +84,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test that request() encodes a boolean parameter as a lowercase string""" endpoint = 'search/tweets' url = self.get_url(endpoint) - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request(endpoint, params={'include_entities': True}) self.api.request(endpoint, params={'include_entities': False}) @@ -86,7 +97,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test that request() encodes a numeric or string parameter correctly""" endpoint = 'search/tweets' url = self.get_url(endpoint) - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request(endpoint, params={'lang': 'es'}) self.api.request(endpoint, params={'count': 50}) @@ -100,7 +111,7 @@ class TwythonAPITestCase(unittest.TestCase): endpoint = 'search/tweets' url = self.get_url(endpoint) location = ['37.781157', '-122.39872', '1mi'] - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request(endpoint, params={'geocode': location}) @@ -113,7 +124,7 @@ class TwythonAPITestCase(unittest.TestCase): endpoint = 'search/tweets' url = self.get_url(endpoint) location = [37.781157, -122.39872, '1mi'] - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request(endpoint, params={'geocode': location}) @@ -124,7 +135,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test that request() ignores unexpected parameter types""" endpoint = 'search/tweets' url = self.get_url(endpoint) - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.request(endpoint, params={'geocode': self}) @@ -135,11 +146,14 @@ class TwythonAPITestCase(unittest.TestCase): """Test that request() pulls a file out of params for requests lib""" endpoint = 'account/update_profile_image' url = self.get_url(endpoint) - responses.add(responses.POST, url) + self.register_response(responses.POST, url) mock_file = StringIO("Twython test image") self.api.request(endpoint, method='POST', params={'image': mock_file}) + if not is_py2: + responses.calls[0].request.body = responses.calls[0].request.body.decode("utf-8") + self.assertIn('filename="image"', responses.calls[0].request.body) self.assertIn("Twython test image", responses.calls[0].request.body) @@ -148,10 +162,13 @@ class TwythonAPITestCase(unittest.TestCase): """Test that request() passes params as data when the request is a POST""" endpoint = 'statuses/update' url = self.get_url(endpoint) - responses.add(responses.POST, url) + self.register_response(responses.POST, url) self.api.request(endpoint, method='POST', params={'status': 'this is a test'}) + if not is_py2: + responses.calls[0].request.body = responses.calls[0].request.body.decode("utf-8") + self.assertIn('status=this+is+a+test', responses.calls[0].request.body) self.assertNotIn('status=this+is+a+test', responses.calls[0].request.url) @@ -160,7 +177,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test Twython generic GET request works""" endpoint = 'account/verify_credentials' url = self.get_url(endpoint) - responses.add(responses.GET, url) + self.register_response(responses.GET, url) self.api.get(endpoint) @@ -172,7 +189,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test Twython generic POST request works""" endpoint = 'statuses/update' url = self.get_url(endpoint) - responses.add(responses.POST, url) + self.register_response(responses.POST, url) self.api.post(endpoint, params={'status': 'I love Twython!'}) @@ -191,7 +208,7 @@ class TwythonAPITestCase(unittest.TestCase): """Test getting last specific header of the last API call works""" endpoint = 'statuses/home_timeline' url = self.get_url(endpoint) - responses.add(responses.GET, url, adding_headers={'x-rate-limit-remaining': 37}) + self.register_response(responses.GET, url, adding_headers={'x-rate-limit-remaining': 37}) self.api.get(endpoint) -- 2.39.5 From f8d47c13d36cf6b945b555ca0d7cd63dc6a090fb Mon Sep 17 00:00:00 2001 From: wcdolphin Date: Sun, 12 Jan 2014 15:21:27 -0800 Subject: [PATCH 076/204] Adds retry_after attribute to TwythonRateLimitError --- twython/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/twython/exceptions.py b/twython/exceptions.py index b9c9df5..4aa7dba 100644 --- a/twython/exceptions.py +++ b/twython/exceptions.py @@ -53,6 +53,8 @@ class TwythonRateLimitError(TwythonError): # pragma: no cover msg = '%s (Retry after %d seconds)' % (msg, retry_after) TwythonError.__init__(self, msg, error_code=error_code) + self.retry_after = retry_after + class TwythonStreamError(TwythonError): """Raised when an invalid response from the Stream API is received""" -- 2.39.5 From c83304edf27514994bb66cb2d1b2056d9d1893e5 Mon Sep 17 00:00:00 2001 From: Cash Costello Date: Tue, 14 Jan 2014 11:14:14 -0500 Subject: [PATCH 077/204] requests returns content as bytes so use bytes for test strings. Also changed api verison in test 1.1 to not confuse people --- tests/test_core.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ddf5256..caf07ae 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -52,10 +52,10 @@ class TwythonAPITestCase(unittest.TestCase): @responses.activate def test_request_should_handle_relative_endpoint(self): """Test that request() accepts a twitter endpoint name for the endpoint argument""" - url = 'https://api.twitter.com/2.0/search/tweets.json' + url = 'https://api.twitter.com/1.1/search/tweets.json' self.register_response(responses.GET, url) - self.api.request('search/tweets', version='2.0') + self.api.request('search/tweets', version='1.1') self.assertEqual(1, len(responses.calls)) self.assertEqual(url, responses.calls[0].request.url) @@ -151,11 +151,8 @@ class TwythonAPITestCase(unittest.TestCase): mock_file = StringIO("Twython test image") self.api.request(endpoint, method='POST', params={'image': mock_file}) - if not is_py2: - responses.calls[0].request.body = responses.calls[0].request.body.decode("utf-8") - - self.assertIn('filename="image"', responses.calls[0].request.body) - self.assertIn("Twython test image", responses.calls[0].request.body) + self.assertIn(b'filename="image"', responses.calls[0].request.body) + self.assertIn(b"Twython test image", responses.calls[0].request.body) @responses.activate def test_request_should_put_params_in_body_when_post(self): @@ -166,10 +163,7 @@ class TwythonAPITestCase(unittest.TestCase): self.api.request(endpoint, method='POST', params={'status': 'this is a test'}) - if not is_py2: - responses.calls[0].request.body = responses.calls[0].request.body.decode("utf-8") - - self.assertIn('status=this+is+a+test', responses.calls[0].request.body) + self.assertIn(b'status=this+is+a+test', responses.calls[0].request.body) self.assertNotIn('status=this+is+a+test', responses.calls[0].request.url) @responses.activate -- 2.39.5 From fa2122127cd84e0acd25189cb0713dfc1e4f0919 Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 25 Jan 2014 16:56:48 -0500 Subject: [PATCH 078/204] Fixes #302 simplifies json decoding and handling of errors. Adds tests. --- tests/test_core.py | 68 +++++++++++++++++++++++++++++++++++++++++++--- twython/api.py | 52 +++++++++++++++++------------------ 2 files changed, 89 insertions(+), 31 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index caf07ae..df20660 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,4 @@ -from twython import Twython, TwythonError, TwythonAuthError +from twython import Twython, TwythonError, TwythonAuthError, TwythonRateLimitError from .config import ( test_tweet_object, test_tweet_html, unittest @@ -27,10 +27,11 @@ class TwythonAPITestCase(unittest.TestCase): """Convenience function for mapping from endpoint to URL""" return '%s/%s.json' % (self.api.api_url % self.api.api_version, endpoint) - def register_response(self, method, url, body='', match_querystring=False, + def register_response(self, method, url, body='{}', match_querystring=False, status=200, adding_headers=None, stream=False, - content_type='text/plain'): - """Temporary function to work around python 3.3 issue with responses""" + content_type='application/json; charset=utf-8'): + """Wrapper function for responses for simpler unit tests""" + # responses uses BytesIO to hold the body so it needs to be in bytes if not is_py2: body = bytes(body, 'UTF-8') @@ -197,6 +198,65 @@ class TwythonAPITestCase(unittest.TestCase): get_mock.side_effect = requests.RequestException("hostname 'example.com' doesn't match ...") self.assertRaises(TwythonError, self.api.get, 'https://example.com') + @responses.activate + def test_request_should_get_convert_json_to_data(self): + """Test that Twython converts JSON data to a Python object""" + endpoint = 'statuses/show' + url = self.get_url(endpoint) + self.register_response(responses.GET, url, body='{"id": 210462857140252672}') + + data = self.api.request(endpoint, params={'id': 210462857140252672}) + + self.assertEqual({'id': 210462857140252672}, data) + + @responses.activate + def test_request_should_raise_exception_with_invalid_json(self): + """Test that Twython handles invalid JSON (though Twitter should not return it)""" + endpoint = 'statuses/show' + url = self.get_url(endpoint) + self.register_response(responses.GET, url, body='{"id: 210462857140252672}') + + self.assertRaises(TwythonError, self.api.request, endpoint, params={'id': 210462857140252672}) + + @responses.activate + def test_request_should_handle_401(self): + """Test that Twython raises an auth error on 401 error""" + endpoint = 'statuses/home_timeline' + url = self.get_url(endpoint) + self.register_response(responses.GET, url, body='{"errors":[{"message":"Error"}]}', status=401) + + self.assertRaises(TwythonAuthError, self.api.request, endpoint) + + @responses.activate + def test_request_should_handle_400_for_missing_auth_data(self): + """Test that Twython raises an auth error on 400 error when no oauth data sent""" + endpoint = 'statuses/home_timeline' + url = self.get_url(endpoint) + self.register_response(responses.GET, url, + body='{"errors":[{"message":"Bad Authentication data"}]}', status=400) + + self.assertRaises(TwythonAuthError, self.api.request, endpoint) + + @responses.activate + def test_request_should_handle_400_that_is_not_auth_related(self): + """Test that Twython raises a normal error on 400 error when unrelated to authorization""" + endpoint = 'statuses/home_timeline' + url = self.get_url(endpoint) + self.register_response(responses.GET, url, + body='{"errors":[{"message":"Bad request"}]}', status=400) + + self.assertRaises(TwythonError, self.api.request, endpoint) + + @responses.activate + def test_request_should_handle_rate_limit(self): + """Test that Twython raises an rate limit error on 429""" + endpoint = 'statuses/home_timeline' + url = self.get_url(endpoint) + self.register_response(responses.GET, url, + body='{"errors":[{"message":"Rate Limit"}]}', status=429) + + self.assertRaises(TwythonRateLimitError, self.api.request, endpoint) + @responses.activate def test_get_lastfunction_header_should_return_header(self): """Test getting last specific header of the last API call works""" diff --git a/twython/api.py b/twython/api.py index ef1aac6..c457fd7 100644 --- a/twython/api.py +++ b/twython/api.py @@ -143,7 +143,6 @@ class Twython(EndpointsMixin, object): response = func(url, **requests_args) except requests.RequestException as e: raise TwythonError(str(e)) - content = response.content.decode('utf-8') # create stash for last function intel self._last_call = { @@ -153,37 +152,18 @@ class Twython(EndpointsMixin, object): 'headers': response.headers, 'status_code': response.status_code, 'url': response.url, - 'content': content, + 'content': response.text, } - # Wrap the json loads in a try, and defer an error - # Twitter will return invalid json with an error code in the headers - json_error = False - try: - try: - # try to get json - content = content.json() - except AttributeError: - # if unicode detected - content = json.loads(content) - except ValueError: - json_error = True - content = {} - + # greater than 304 (not modified) is an error if response.status_code > 304: - # If there is no error message, use a default. - errors = content.get('errors', - [{'message': 'An error occurred processing your request.'}]) - if errors and isinstance(errors, list): - error_message = errors[0]['message'] - else: - error_message = errors # pragma: no cover + error_message = self._get_error_message(response) self._last_call['api_error'] = error_message ExceptionType = TwythonError if response.status_code == 429: # Twitter API 1.1, always return 429 when rate limit is exceeded - ExceptionType = TwythonRateLimitError # pragma: no cover + ExceptionType = TwythonRateLimitError elif response.status_code == 401 or 'Bad Authentication data' in error_message: # Twitter API 1.1, returns a 401 Unauthorized or # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens @@ -193,12 +173,30 @@ class Twython(EndpointsMixin, object): error_code=response.status_code, retry_after=response.headers.get('retry-after')) - # if we have a json error here, then it's not an official Twitter API error - if json_error and not response.status_code in (200, 201, 202): # pragma: no cover - raise TwythonError('Response was not valid JSON, unable to decode.') + try: + content = response.json() + except json.JSONDecodeError: + raise TwythonError('Response was not valid JSON. Unable to decode.') return content + def _get_error_message(self, response): + """Parse and return the first error message""" + + error_message = 'An error occurred processing your request.' + try: + content = response.json() + # {"errors":[{"code":34,"message":"Sorry, that page does not exist"}]} + error_message = content['errors'][0]['message'] + except json.JSONDecodeError: + # bad json data from Twitter for an error + pass + except (KeyError, IndexError): + # missing data so fallback to default message + pass + + return error_message + def request(self, endpoint, method='GET', params=None, version='1.1'): """Return dict of response received from Twitter's API -- 2.39.5 From 1e627f9fb1cd0bc06704739e4426f6d1218352ac Mon Sep 17 00:00:00 2001 From: cash Date: Sat, 25 Jan 2014 17:15:17 -0500 Subject: [PATCH 079/204] need to use ValueError to account for different json libraries throwing different exceptions --- twython/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/api.py b/twython/api.py index c457fd7..db79381 100644 --- a/twython/api.py +++ b/twython/api.py @@ -175,7 +175,7 @@ class Twython(EndpointsMixin, object): try: content = response.json() - except json.JSONDecodeError: + except ValueError: raise TwythonError('Response was not valid JSON. Unable to decode.') return content @@ -188,7 +188,7 @@ class Twython(EndpointsMixin, object): content = response.json() # {"errors":[{"code":34,"message":"Sorry, that page does not exist"}]} error_message = content['errors'][0]['message'] - except json.JSONDecodeError: + except ValueError: # bad json data from Twitter for an error pass except (KeyError, IndexError): -- 2.39.5 From e1ee67192e37df7ff44337c9aaf8981ae0d93549 Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 21 Feb 2014 10:29:10 +1100 Subject: [PATCH 080/204] fixed paramater naming as per PEP8 --- twython/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index 7eee2f4..bb272d8 100644 --- a/twython/api.py +++ b/twython/api.py @@ -386,7 +386,7 @@ class Twython(EndpointsMixin, object): ) return self.cursor(self.search, q=search_query, **params) - def cursor(self, function, returnPages = False, **params): + def cursor(self, function, return_pages=False, **params): """Returns a generator for results that match a specified query. :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) @@ -416,7 +416,7 @@ class Twython(EndpointsMixin, object): else: results = content - if returnPages: + if return_pages: yield results else: for result in results: @@ -439,7 +439,7 @@ class Twython(EndpointsMixin, object): except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - for result in self.cursor(function, returnPages = returnPages, **params): + for result in self.cursor(function, return_pages=return_pages, **params): yield result @staticmethod -- 2.39.5 From 673336ff8f8a9a647fc8e44cb05f6c8a0ec39c98 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Sun, 23 Feb 2014 14:57:03 +0000 Subject: [PATCH 081/204] Failing test for Accept-Encoding header --- tests/test_core.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index df20660..4870688 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -277,6 +277,17 @@ class TwythonAPITestCase(unittest.TestCase): """Test attempting to get a header when no API call was made raises a TwythonError""" self.assertRaises(TwythonError, self.api.get_lastfunction_header, 'no-api-call-was-made') + @responses.activate + def test_sends_correct_accept_encoding_header(self): + """Test that Twython accepts compressed data.""" + endpoint = 'statuses/home_timeline' + url = self.get_url(endpoint) + self.register_response(responses.GET, url) + + self.api.get(endpoint) + + self.assertEqual('gzip, deflate, compress', responses.calls[0].request.headers['Accept-Encoding']) + # Static methods def test_construct_api_url(self): """Test constructing a Twitter API url works as we expect""" -- 2.39.5 From 31ff35349cc6a79bd4385cce26b76578ac41239f Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Sun, 23 Feb 2014 15:06:44 +0000 Subject: [PATCH 082/204] Merge headers, don't overwrite. --- twython/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index db79381..88b6735 100644 --- a/twython/api.py +++ b/twython/api.py @@ -109,10 +109,14 @@ class Twython(EndpointsMixin, object): # Never be used again. client_args_copy = self.client_args.copy() for k, v in client_args_copy.items(): - if k in ('cert', 'headers', 'hooks', 'max_redirects', 'proxies'): + if k in ('cert', 'hooks', 'max_redirects', 'proxies'): setattr(self.client, k, v) self.client_args.pop(k) # Pop, pop! + # Headers are always present, so we unconditionally pop them and merge + # them into the session headers. + self.client.headers.update(self.client_args.pop('headers')) + self._last_call = None def __repr__(self): -- 2.39.5 From f514816c6f9905da9e76d598cdbeb8f32a8ca63b Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Sun, 23 Feb 2014 15:12:44 +0000 Subject: [PATCH 083/204] Fixup python3 test failure. --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index 4870688..5b8a374 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -286,7 +286,7 @@ class TwythonAPITestCase(unittest.TestCase): self.api.get(endpoint) - self.assertEqual('gzip, deflate, compress', responses.calls[0].request.headers['Accept-Encoding']) + self.assertEqual(b'gzip, deflate, compress', responses.calls[0].request.headers['Accept-Encoding']) # Static methods def test_construct_api_url(self): -- 2.39.5 From 8fd80a0ce8b312cefee5e252d4864336bc4a81e5 Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Fri, 16 May 2014 17:33:09 +1000 Subject: [PATCH 084/204] Create follow_user.py Basic CLI script with example for following user. --- examples/follow_user.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/follow_user.py diff --git a/examples/follow_user.py b/examples/follow_user.py new file mode 100644 index 0000000..b9e78a8 --- /dev/null +++ b/examples/follow_user.py @@ -0,0 +1,20 @@ +from twython import Twython, TwythonError + +# Optionally accept user data from the command line (or elsewhere). +# +# Usage: follow_user.py ryanmcgrath + +import sys + +if len(sys.argv) >= 2: + target = sys.argv[1] +else: + target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +try: + twitter.create_friendship(screen_name=target, follow="true") +except TwythonError as e: + print(e) -- 2.39.5 From 8af56ee790f50a1d4ccb11f055e6ffb1dea61d51 Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sat, 17 May 2014 00:26:35 +1000 Subject: [PATCH 085/204] Create unfollow_user.py CLI script to stop following a user as specified by command arguments. --- examples/unfollow_user.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/unfollow_user.py diff --git a/examples/unfollow_user.py b/examples/unfollow_user.py new file mode 100644 index 0000000..2417035 --- /dev/null +++ b/examples/unfollow_user.py @@ -0,0 +1,20 @@ +from twython import Twython, TwythonError + +# Optionally accept user data from the command line (or elsewhere). +# +# Usage: follow_user.py ryanmcgrath + +import sys + +if len(sys.argv) >= 2: + target = sys.argv[1] +else: + target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +try: + twitter.destroy_friendship(screen_name=target, follow="true") +except TwythonError as e: + print(e) -- 2.39.5 From 687ccefc54eb4f59525cdfc4f2224ee7ec3c4f5f Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sat, 17 May 2014 00:45:04 +1000 Subject: [PATCH 086/204] Create block_user.py CLI script to block a user as specified by command arguments. --- examples/block_user.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/block_user.py diff --git a/examples/block_user.py b/examples/block_user.py new file mode 100644 index 0000000..a06c5f0 --- /dev/null +++ b/examples/block_user.py @@ -0,0 +1,20 @@ +from twython import Twython, TwythonError + +# Optionally accept user data from the command line (or elsewhere). +# +# Usage: follow_user.py ryanmcgrath + +import sys + +if len(sys.argv) >= 2: + target = sys.argv[1] +else: + target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +try: + twitter.create_block(screen_name=target, follow="true") +except TwythonError as e: + print(e) -- 2.39.5 From eacecd4946ce079e178ec48b74865714b0f16aa1 Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sat, 17 May 2014 01:18:35 +1000 Subject: [PATCH 087/204] Create unblock_user.py CLI script to unblock a user as specified by command arguments. --- examples/unblock_user.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/unblock_user.py diff --git a/examples/unblock_user.py b/examples/unblock_user.py new file mode 100644 index 0000000..80ea19c --- /dev/null +++ b/examples/unblock_user.py @@ -0,0 +1,20 @@ +from twython import Twython, TwythonError + +# Optionally accept user data from the command line (or elsewhere). +# +# Usage: follow_user.py ryanmcgrath + +import sys + +if len(sys.argv) >= 2: + target = sys.argv[1] +else: + target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +try: + twitter.destroy_block(screen_name=target, follow="true") +except TwythonError as e: + print(e) -- 2.39.5 From ad6cbd0a893c3aa3aca588bc1909d514f8553b7e Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sat, 17 May 2014 01:41:53 +1000 Subject: [PATCH 088/204] Create block_spammer.py CLI script to block and report a user for spamming, as specified by command arguments. --- examples/block_spammer.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 examples/block_spammer.py diff --git a/examples/block_spammer.py b/examples/block_spammer.py new file mode 100644 index 0000000..c579904 --- /dev/null +++ b/examples/block_spammer.py @@ -0,0 +1,20 @@ +from twython import Twython, TwythonError + +# Optionally accept user data from the command line (or elsewhere). +# +# Usage: follow_user.py ryanmcgrath + +import sys + +if len(sys.argv) >= 2: + target = sys.argv[1] +else: + target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +try: + twitter.report_spam(screen_name=target, follow="true") +except TwythonError as e: + print(e) -- 2.39.5 From b29ce05bc84132e6fd8cc0fdd27387ef3c88db29 Mon Sep 17 00:00:00 2001 From: bsbkeven Date: Wed, 21 May 2014 11:18:59 -0700 Subject: [PATCH 089/204] Update endpoints.py Added endpoint for statuses/lookup (https://dev.twitter.com/docs/api/1.1/get/statuses/lookup). --- twython/endpoints.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/twython/endpoints.py b/twython/endpoints.py index 460b3e9..801ed5a 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -73,6 +73,16 @@ class EndpointsMixin(object): """ return self.get('statuses/show/%s' % params.get('id'), params=params) + def lookup_status(self, **params): + """Returns fully-hydrated tweet objects for up to 100 tweets per + request, as specified by comma-separated values passed to the id + parameter. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/lookup + + """ + return self.post('statuses/lookup', params=params) + def destroy_status(self, **params): """Destroys the status specified by the required ID parameter -- 2.39.5 From 1455d8769ab3a20ede947ae25c6f0cc4ea21f8c3 Mon Sep 17 00:00:00 2001 From: Joe Cabrera Date: Thu, 24 Jul 2014 04:24:51 +0000 Subject: [PATCH 090/204] first pep8 work --- twython/__init__.py | 5 +- twython/advisory.py | 5 +- twython/endpoints.py | 126 +++++++++++++++++++++++++++++-------------- twython/helpers.py | 2 +- 4 files changed, 92 insertions(+), 46 deletions(-) diff --git a/twython/__init__.py b/twython/__init__.py index ab4df37..e0920a6 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -11,8 +11,9 @@ Twython Twython is a library for Python that wraps the Twitter API. -It aims to abstract away all the API endpoints, so that additions to the library -and/or the Twitter API won't cause any overall problems. +It aims to abstract away all the API endpoints, so that +additions to the library and/or the Twitter API won't +cause any overall problems. Questions, comments? ryan@venodesigns.net """ diff --git a/twython/advisory.py b/twython/advisory.py index f553f31..31657ee 100644 --- a/twython/advisory.py +++ b/twython/advisory.py @@ -15,7 +15,8 @@ only TwythonDeprecationWarnings. class TwythonDeprecationWarning(DeprecationWarning): - """Custom DeprecationWarning to be raised when methods/variables are being deprecated in Twython. - Python 2.7 > ignores DeprecationWarning so we want to specifcally bubble up ONLY Twython Deprecation Warnings + """Custom DeprecationWarning to be raised when methods/variables + are being deprecated in Twython. Python 2.7 > ignores DeprecationWarning + so we want to specifcally bubble up ONLY Twython Deprecation Warnings """ pass diff --git a/twython/endpoints.py b/twython/endpoints.py index 801ed5a..d71fe2a 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -5,7 +5,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. +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) @@ -20,7 +21,8 @@ class EndpointsMixin(object): """Returns the 20 most recent mentions (tweets containing a users's @screen_name) for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline + Docs: + https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline """ return self.get('statuses/mentions_timeline', params=params) @@ -63,7 +65,8 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets/%3Aid """ - return self.get('statuses/retweets/%s' % params.get('id'), params=params) + return self.get('statuses/retweets/%s' % params.get('id'), + params=params) def show_status(self, **params): """Returns a single Tweet, specified by the id parameter @@ -111,7 +114,8 @@ class EndpointsMixin(object): """Updates the authenticating user's current status and attaches media for upload. In other words, it creates a Tweet with a picture attached. - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media + Docs: + https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media """ return self.post('statuses/update_with_media', params=params) @@ -184,7 +188,8 @@ class EndpointsMixin(object): return self.post('direct_messages/destroy', params=params) def send_direct_message(self, **params): - """Sends a new direct message to the specified user from the authenticating user. + """Sends a new direct message to the specified user from the + authenticating user. Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/new @@ -196,7 +201,8 @@ class EndpointsMixin(object): """Returns a collection of user_ids that the currently authenticated user does not want to receive retweets from. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/no_retweets/ids + Docs: + https://dev.twitter.com/docs/api/1.1/get/friendships/no_retweets/ids """ return self.get('friendships/no_retweets/ids', params=params) @@ -327,7 +333,8 @@ class EndpointsMixin(object): requesting user if authentication was successful; returns a 401 status code and an error message if not. - Docs: https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials + Docs: + https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials """ return self.get('account/verify_credentials', params=params) @@ -343,13 +350,15 @@ class EndpointsMixin(object): def update_delivery_service(self, **params): """Sets which device Twitter delivers updates to for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_delivery_device + Docs: + https://dev.twitter.com/docs/api/1.1/post/account/update_delivery_device """ return self.post('account/update_delivery_device', params=params) def update_profile(self, **params): - """Sets values that users are able to set under the "Account" tab of their settings page. + """Sets values that users are able to set under the "Account" tab of their + settings page. Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile @@ -359,7 +368,8 @@ class EndpointsMixin(object): def update_profile_banner_image(self, **params): # pragma: no cover """Updates the authenticating user's profile background image. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image + Docs: + https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image """ return self.post('account/update_profile_banner', params=params) @@ -368,7 +378,8 @@ class EndpointsMixin(object): """Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors + Docs: + https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors """ return self.post('account/update_profile_colors', params=params) @@ -376,13 +387,15 @@ class EndpointsMixin(object): def update_profile_image(self, **params): # pragma: no cover """Updates the authenticating user's profile image. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image + Docs: + https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image """ return self.post('account/update_profile_image', params=params) def list_blocks(self, **params): - """Returns a collection of user objects that the authenticating user is blocking. + """Returns a collection of user objects that the authenticating user + is blocking. Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/list @@ -410,7 +423,8 @@ class EndpointsMixin(object): return self.post('blocks/create', params=params) def destroy_block(self, **params): - """Un-blocks the user specified in the ID parameter for the authenticating user. + """Un-blocks the user specified in the ID parameter for the + authenticating user. Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/destroy @@ -419,7 +433,8 @@ class EndpointsMixin(object): def lookup_user(self, **params): """Returns fully-hydrated user objects for up to 100 users per request, - as specified by comma-separated values passed to the user_id and/or screen_name parameters. + as specified by comma-separated values passed to the user_id and/or + screen_name parameters. Docs: https://dev.twitter.com/docs/api/1.1/get/users/lookup @@ -436,7 +451,8 @@ class EndpointsMixin(object): return self.get('users/show', params=params) def search_users(self, **params): - """Provides a simple, relevance-based search interface to public user accounts on Twitter. + """Provides a simple, relevance-based search interface to public user + accounts on Twitter. Docs: https://dev.twitter.com/docs/api/1.1/get/users/search @@ -463,7 +479,8 @@ class EndpointsMixin(object): """Removes the uploaded profile banner for the authenticating user. Returns HTTP 200 upon success. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/remove_profile_banner + Docs: + https://dev.twitter.com/docs/api/1.1/post/account/remove_profile_banner """ return self.post('account/remove_profile_banner', params=params) @@ -471,13 +488,16 @@ class EndpointsMixin(object): def update_profile_background_image(self, **params): """Uploads a profile banner on behalf of the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner + Docs: + https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner """ - return self.post('account/update_profile_background_image', params=params) + return self.post('account/update_profile_background_image', + params=params) def get_profile_banner_sizes(self, **params): - """Returns a map of the available size variations of the specified user's profile banner. + """Returns a map of the available size variations of the specified + user's profile banner. Docs: https://dev.twitter.com/docs/api/1.1/get/users/profile_banner @@ -488,10 +508,12 @@ class EndpointsMixin(object): def get_user_suggestions_by_slug(self, **params): """Access the users in a given category of the Twitter suggested user list. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug + Docs: + https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug """ - return self.get('users/suggestions/%s' % params.get('slug'), params=params) + return self.get('users/suggestions/%s' % params.get('slug'), + params=params) def get_user_suggestions(self, **params): """Access to Twitter's suggested user list. @@ -503,16 +525,20 @@ class EndpointsMixin(object): def get_user_suggestions_statuses_by_slug(self, **params): """Access the users in a given category of the Twitter suggested user - list and return their most recent status if they are not a protected user. + list and return their most recent status if they are not a protected + user. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug/members + Docs: + https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug/members """ - return self.get('users/suggestions/%s/members' % params.get('slug'), params=params) + return self.get('users/suggestions/%s/members' % params.get('slug'), + params=params) # Favorites def get_favorites(self, **params): - """Returns the 20 most recent Tweets favorited by the authenticating or specified user. + """Returns the 20 most recent Tweets favorited by the authenticating + or specified user. Docs: https://dev.twitter.com/docs/api/1.1/get/favorites/list @@ -521,7 +547,8 @@ class EndpointsMixin(object): get_favorites.iter_mode = 'id' def destroy_favorite(self, **params): - """Un-favorites the status specified in the ID parameter as the authenticating user. + """Un-favorites the status specified in the ID parameter as the + authenticating user. Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/destroy @@ -529,7 +556,8 @@ class EndpointsMixin(object): return self.post('favorites/destroy', params=params) def create_favorite(self, **params): - """Favorites the status specified in the ID parameter as the authenticating user. + """Favorites the status specified in the ID parameter as the + authenticating user. Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/create @@ -538,7 +566,8 @@ class EndpointsMixin(object): # Lists def show_lists(self, **params): - """Returns all lists the authenticating or specified user subscribes to, including their own. + """Returns all lists the authenticating or specified user subscribes to, + including their own. Docs: https://dev.twitter.com/docs/api/1.1/get/lists/list @@ -585,7 +614,8 @@ class EndpointsMixin(object): def subscribe_to_list(self, **params): """Subscribes the authenticated user to the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create + Docs: + https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create """ return self.post('lists/subscribers/create', params=params) @@ -601,7 +631,8 @@ class EndpointsMixin(object): def unsubscribe_from_list(self, **params): """Unsubscribes the authenticated user from the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy + Docs: + https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy """ return self.post('lists/subscribers/destroy', params=params) @@ -610,7 +641,8 @@ class EndpointsMixin(object): """Adds multiple members to a list, by specifying a comma-separated list of member ids or screen names. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all + Docs: + https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all """ return self.post('lists/members/create_all', params=params) @@ -687,7 +719,8 @@ class EndpointsMixin(object): """Removes multiple members from a list, by specifying a comma-separated list of member ids or screen names. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all + Docs: + https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all """ return self.post('lists/members/destroy_all', params=params) @@ -714,10 +747,12 @@ class EndpointsMixin(object): def show_saved_search(self, **params): """Retrieve the information for the saved search represented by the given id. - Docs: https://dev.twitter.com/docs/api/1.1/get/saved_searches/show/%3Aid + Docs: + https://dev.twitter.com/docs/api/1.1/get/saved_searches/show/%3Aid """ - return self.get('saved_searches/show/%s' % params.get('id'), params=params) + return self.get('saved_searches/show/%s' % params.get('id'), + params=params) def create_saved_search(self, **params): """Create a new saved search for the authenticated user. @@ -730,10 +765,12 @@ class EndpointsMixin(object): def destroy_saved_search(self, **params): """Destroys a saved search for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/saved_searches/destroy/%3Aid + Docs: + https://dev.twitter.com/docs/api/1.1/post/saved_searches/destroy/%3Aid """ - return self.post('saved_searches/destroy/%s' % params.get('id'), params=params) + return self.post('saved_searches/destroy/%s' % params.get('id'), + params=params) # Places & Geo def get_geo_info(self, **params): @@ -861,7 +898,8 @@ class EndpointsMixin(object): """Returns the current rate limits for methods belonging to the specified resource families. - Docs: https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status + Docs: + https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status """ return self.get('application/rate_limit_status', params=params) @@ -871,9 +909,15 @@ class EndpointsMixin(object): TWITTER_HTTP_STATUS_CODE = { 200: ('OK', 'Success!'), 304: ('Not Modified', 'There was no new data to return.'), - 400: ('Bad Request', 'The request was invalid. An accompanying error message will explain why. This is the status code will be returned during rate limiting.'), - 401: ('Unauthorized', 'Authentication credentials were missing or incorrect.'), - 403: ('Forbidden', 'The request is understood, but it has been refused. An accompanying error message will explain why. This code is used when requests are being denied due to update limits.'), + 400: ('Bad Request', 'The request was invalid. An accompanying \ + error message will explain why. This is the status code \ + will be returned during rate limiting.'), + 401: ('Unauthorized', 'Authentication credentials were missing \ + or incorrect.'), + 403: ('Forbidden', 'The request is understood, but it has been \ + refused. An accompanying error message will explain why. \ + This code is used when requests are being denied due to \ + update limits.'), 404: ('Not Found', 'The URI requested is invalid or the resource requested, such as a user, does not exists.'), 406: ('Not Acceptable', 'Returned by the Search API when an invalid format is specified in the request.'), 410: ('Gone', 'This resource is gone. Used to indicate that an API endpoint has been turned off.'), diff --git a/twython/helpers.py b/twython/helpers.py index a482610..f44d0ce 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -28,7 +28,7 @@ def _transparent_params(_params): try: params[k] = ','.join(v) except TypeError: - params[k] = ','.join(map(str,v)) + params[k] = ','.join(map(str, v)) else: continue # pragma: no cover return params, files -- 2.39.5 From 6206c0b73f33f1a900b5a4e04c4eb0744beaea77 Mon Sep 17 00:00:00 2001 From: Joe Cabrera Date: Thu, 24 Jul 2014 13:50:36 +0000 Subject: [PATCH 091/204] more pep8 fixes --- twython/api.py | 69 ++++++++++++++++++++++++++++---------------- twython/endpoints.py | 26 ++++++++++++----- 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/twython/api.py b/twython/api.py index 1d23a35..9ae69a1 100644 --- a/twython/api.py +++ b/twython/api.py @@ -27,29 +27,41 @@ warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > class Twython(EndpointsMixin, 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'): - """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + oauth_token_secret=None, access_token=None, + token_type='bearer', oauth_version=1, api_version='1.1', + client_args=None, auth_endpoint='authenticate'): + """Instantiates an instance of Twython. Takes optional parameters for + authentication and such (see below). :param app_key: (optional) Your applications key :param app_secret: (optional) Your applications secret key - :param oauth_token: (optional) When using **OAuth 1**, combined with oauth_token_secret to make authenticated calls - :param oauth_token_secret: (optional) When using **OAuth 1** combined with oauth_token to make authenticated calls - :param access_token: (optional) When using **OAuth 2**, provide a valid access token if you have one - :param token_type: (optional) When using **OAuth 2**, provide your token type. Default: bearer - :param oauth_version: (optional) Choose which OAuth version to use. Default: 1 - :param api_version: (optional) Choose which Twitter API version to use. Default: 1.1 - - :param client_args: (optional) Accepts some requests Session parameters and some requests Request parameters. - See http://docs.python-requests.org/en/latest/api/#sessionapi and requests section below it for details. - [ex. headers, proxies, verify(SSL verification)] - :param auth_endpoint: (optional) Lets you select which authentication endpoint will use your application. - This will allow the application to have DM access if the endpoint is 'authorize'. - Default: authenticate. + :param oauth_token: (optional) When using **OAuth 1**, combined with + oauth_token_secret to make authenticated calls + :param oauth_token_secret: (optional) When using **OAuth 1** combined + with oauth_token to make authenticated calls + :param access_token: (optional) When using **OAuth 2**, provide a + valid access token if you have one + :param token_type: (optional) When using **OAuth 2**, provide your + token type. Default: bearer + :param oauth_version: (optional) Choose which OAuth version to use. + Default: 1 + :param api_version: (optional) Choose which Twitter API version to + use. Default: 1.1 + :param client_args: (optional) Accepts some requests Session parameters + and some requests Request parameters. + See http://docs.python-requests.org/en/latest/api/#sessionapi + and requests section below it for details. + [ex. headers, proxies, verify(SSL verification)] + :param auth_endpoint: (optional) Lets you select which authentication + endpoint will use your application. + This will allow the application to have DM access + if the endpoint is 'authorize'. + Default: authenticate. """ - # API urls, OAuth urls and API version; needed for hitting that there API. + # API urls, OAuth urls and API version; needed for hitting that there + # API. self.api_version = api_version self.api_url = 'https://api.twitter.com/%s' @@ -75,16 +87,18 @@ class Twython(EndpointsMixin, object): self.client_args = client_args or {} default_headers = {'User-Agent': 'Twython v' + __version__} - if not 'headers' in self.client_args: + if 'headers' not in self.client_args: # If they didn't set any headers, set our defaults for them self.client_args['headers'] = default_headers elif 'User-Agent' not in self.client_args['headers']: - # If they set headers, but didn't include User-Agent.. set it for them + # If they set headers, but didn't include User-Agent.. set + # it for them self.client_args['headers'].update(default_headers) # Generate OAuth authentication object for the request # If no keys/tokens are passed to __init__, auth=None allows for - # unauthenticated requests, although I think all v1.1 requests need auth + # unauthenticated requests, although I think all v1.1 requests + # need auth auth = None if oauth_version == 1: # User Authentication is through OAuth 1 @@ -93,12 +107,14 @@ class Twython(EndpointsMixin, object): auth = OAuth1(self.app_key, self.app_secret) if self.app_key is not None and self.app_secret is not None and \ - self.oauth_token is not None and self.oauth_token_secret is not None: + self.oauth_token is not None and self.oauth_token_secret is \ + not None: auth = OAuth1(self.app_key, self.app_secret, self.oauth_token, self.oauth_token_secret) elif oauth_version == 2 and self.access_token: # Application Authentication is through OAuth 2 - token = {'token_type': token_type, 'access_token': self.access_token} + token = {'token_type': token_type, + 'access_token': self.access_token} auth = OAuth2(self.app_key, token=token) self.client = requests.Session() @@ -166,11 +182,14 @@ class Twython(EndpointsMixin, object): ExceptionType = TwythonError if response.status_code == 429: - # Twitter API 1.1, always return 429 when rate limit is exceeded + # Twitter API 1.1, always return 429 when + # rate limit is exceeded ExceptionType = TwythonRateLimitError - elif response.status_code == 401 or 'Bad Authentication data' in error_message: + elif response.status_code == 401 or 'Bad Authentication data' \ + in error_message: # Twitter API 1.1, returns a 401 Unauthorized or - # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens + # a 400 "Bad Authentication data" for invalid/expired + # app keys/user tokens ExceptionType = TwythonAuthError raise ExceptionType(error_message, diff --git a/twython/endpoints.py b/twython/endpoints.py index d71fe2a..bafdd7f 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -918,13 +918,23 @@ TWITTER_HTTP_STATUS_CODE = { refused. An accompanying error message will explain why. \ This code is used when requests are being denied due to \ update limits.'), - 404: ('Not Found', 'The URI requested is invalid or the resource requested, such as a user, does not exists.'), - 406: ('Not Acceptable', 'Returned by the Search API when an invalid format is specified in the request.'), - 410: ('Gone', 'This resource is gone. Used to indicate that an API endpoint has been turned off.'), - 422: ('Unprocessable Entity', 'Returned when an image uploaded to POST account/update_profile_banner is unable to be processed.'), - 429: ('Too Many Requests', 'Returned in API v1.1 when a request cannot be served due to the application\'s rate limit having been exhausted for the resource.'), - 500: ('Internal Server Error', 'Something is broken. Please post to the group so the Twitter team can investigate.'), + 404: ('Not Found', 'The URI requested is invalid or the resource \ + requested, such as a user, does not exists.'), + 406: ('Not Acceptable', 'Returned by the Search API when an \ + invalid format is specified in the request.'), + 410: ('Gone', 'This resource is gone. Used to indicate that an \ + API endpoint has been turned off.'), + 422: ('Unprocessable Entity', 'Returned when an image uploaded to \ + POST account/update_profile_banner is unable to be processed.'), + 429: ('Too Many Requests', 'Returned in API v1.1 when a request cannot \ + be served due to the application\'s rate limit having been \ + exhausted for the resource.'), + 500: ('Internal Server Error', 'Something is broken. Please post to the \ + group so the Twitter team can investigate.'), 502: ('Bad Gateway', 'Twitter is down or being upgraded.'), - 503: ('Service Unavailable', 'The Twitter servers are up, but overloaded with requests. Try again later.'), - 504: ('Gateway Timeout', 'The Twitter servers are up, but the request couldn\'t be serviced due to some failure within our stack. Try again later.'), + 503: ('Service Unavailable', 'The Twitter servers are up, but overloaded \ + with requests. Try again later.'), + 504: ('Gateway Timeout', 'The Twitter servers are up, but the request \ + couldn\'t be serviced due to some failure within our stack. Try \ + again later.'), } -- 2.39.5 From a0fec2f004b0260dd85cd012d471e441403c4887 Mon Sep 17 00:00:00 2001 From: Joe Cabrera Date: Sat, 26 Jul 2014 11:48:36 -0400 Subject: [PATCH 092/204] pep8 finished --- examples/follow_user.py | 3 +- examples/search_results.py | 4 +- examples/stream.py | 6 +- setup.py | 5 +- tests/config.py | 3 +- tests/test_core.py | 12 ++-- tests/test_endpoints.py | 22 ++++-- twython/api.py | 143 +++++++++++++++++++++++++------------ twython/streaming/api.py | 33 ++++++--- 9 files changed, 155 insertions(+), 76 deletions(-) diff --git a/examples/follow_user.py b/examples/follow_user.py index b9e78a8..21db370 100644 --- a/examples/follow_user.py +++ b/examples/follow_user.py @@ -9,7 +9,8 @@ import sys if len(sys.argv) >= 2: target = sys.argv[1] else: - target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") + target = raw_input("User to follow: ") + # For Python 3.x use: target = input("User to follow: ") # Requires Authentication as of Twitter API v1.1 twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) diff --git a/examples/search_results.py b/examples/search_results.py index 9984139..3eff41d 100644 --- a/examples/search_results.py +++ b/examples/search_results.py @@ -8,5 +8,7 @@ except TwythonError as e: print e for tweet in search_results['statuses']: - print 'Tweet from @%s Date: %s' % (tweet['user']['screen_name'].encode('utf-8'), tweet['created_at']) + print 'Tweet from @%s Date: %s' % (tweet['user']['screen_nam\ + e'].encode('utf-8'), + tweet['created_at']) print tweet['text'].encode('utf-8'), '\n' diff --git a/examples/stream.py b/examples/stream.py index 0ff5c04..1d1ee1f 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -16,5 +16,7 @@ stream = MyStreamer(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) stream.statuses.filter(track='twitter') -#stream.user() # Read the authenticated users home timeline (what they see on Twitter) in real-time -#stream.site(follow='twitter') +# stream.user() +# Read the authenticated users home timeline +# (what they see on Twitter) in real-time +# stream.site(follow='twitter') diff --git a/setup.py b/setup.py index d2d3570..e6c8e94 100755 --- a/setup.py +++ b/setup.py @@ -29,9 +29,10 @@ setup( license=open('LICENSE').read(), url='https://github.com/ryanmcgrath/twython/tree/master', keywords='twitter search api tweet twython stream', - description='Actively maintained, pure Python wrapper for the Twitter API. Supports both normal and streaming Twitter APIs', + description='Actively maintained, pure Python wrapper for the \ + Twitter API. Supports both normal and streaming Twitter APIs', long_description=open('README.rst').read() + '\n\n' + - open('HISTORY.rst').read(), + open('HISTORY.rst').read(), include_package_data=True, packages=packages, classifiers=[ diff --git a/tests/config.py b/tests/config.py index d26b6e1..65cc2f7 100644 --- a/tests/config.py +++ b/tests/config.py @@ -23,7 +23,8 @@ protected_twitter_2 = os.environ.get('PROTECTED_TWITTER_2', 'TwythonSecure2') # Test Ids test_tweet_id = os.environ.get('TEST_TWEET_ID', '318577428610031617') test_list_slug = os.environ.get('TEST_LIST_SLUG', 'team') -test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', 'twitterapi') +test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', + 'twitterapi') test_tweet_object = {u'contributors': None, u'truncated': False, u'text': u'http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ https://t.co/67pwRvY6z9', 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': [104, 116], 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': [65, 76], u'text': u'checkitout'}, {u'indices': [90, 97], u'text': u'thanks'}], 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': [117, 140], u'expanded_url': u'https://github.com', u'display_url': u'github.com'}]}, 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 should #checkitout. If you can! #thanks Love, @__twython__ github.com' diff --git a/tests/test_core.py b/tests/test_core.py index 5b8a374..98d3a47 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -28,8 +28,8 @@ class TwythonAPITestCase(unittest.TestCase): return '%s/%s.json' % (self.api.api_url % self.api.api_version, endpoint) def register_response(self, method, url, body='{}', match_querystring=False, - status=200, adding_headers=None, stream=False, - content_type='application/json; charset=utf-8'): + status=200, adding_headers=None, stream=False, + content_type='application/json; charset=utf-8'): """Wrapper function for responses for simpler unit tests""" # responses uses BytesIO to hold the body so it needs to be in bytes @@ -37,7 +37,7 @@ class TwythonAPITestCase(unittest.TestCase): body = bytes(body, 'UTF-8') responses.add(method, url, body, match_querystring, - status, adding_headers, stream, content_type) + status, adding_headers, stream, content_type) @responses.activate def test_request_should_handle_full_endpoint(self): @@ -77,7 +77,7 @@ class TwythonAPITestCase(unittest.TestCase): @responses.activate def test_request_should_throw_exception_with_invalid_http_method(self): """Test that request() throws an exception when an invalid HTTP method is passed""" - #TODO(cash): should Twython catch the AttributeError and throw a TwythonError + # TODO(cash): should Twython catch the AttributeError and throw a TwythonError self.assertRaises(AttributeError, self.api.request, endpoint='search/tweets', method='INVALID') @responses.activate @@ -315,5 +315,5 @@ class TwythonAPITestCase(unittest.TestCase): """Test using expanded url in HTML for Tweet displays full urls""" tweet_text = self.api.html_for_tweet(test_tweet_object, False) # Make sure HTML doesn't contain the display OR expanded url - self.assertTrue(not 'http://google.com' in tweet_text) - self.assertTrue(not 'google.com' in tweet_text) + self.assertTrue('http://google.com' not in tweet_text) + self.assertTrue('google.com' not in tweet_text) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 6204a57..b7478d0 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -20,8 +20,10 @@ class TwythonEndpointsTestCase(unittest.TestCase): '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': {} # This is so we can hit coverage that Twython sets User-Agent for us if none is supplied + 'headers': {} } self.api = Twython(app_key, app_secret, @@ -42,7 +44,8 @@ class TwythonEndpointsTestCase(unittest.TestCase): """Test returning timeline for authenticated user and random user succeeds""" self.api.get_user_timeline() # Authenticated User Timeline - self.api.get_user_timeline(screen_name='twitter') # Random User Timeline + self.api.get_user_timeline(screen_name='twitter') + # Random User Timeline @unittest.skip('skipping non-updated test') def test_get_protected_user_timeline_following(self): @@ -82,7 +85,8 @@ class TwythonEndpointsTestCase(unittest.TestCase): @unittest.skip('skipping non-updated test') def test_update_and_destroy_status(self): """Test updating and deleting a status succeeds""" - status = self.api.update_status(status='Test post just to get deleted :( %s' % int(time.time())) + status = self.api.update_status(status='Test post just to get \ + deleted :( %s' % int(time.time())) self.api.destroy_status(id=status['id_str']) @unittest.skip('skipping non-updated test') @@ -117,7 +121,8 @@ class TwythonEndpointsTestCase(unittest.TestCase): def test_send_get_and_destroy_direct_message(self): """Test sending, getting, then destory a direct message succeeds""" message = self.api.send_direct_message(screen_name=protected_twitter_1, - text='Hey d00d! %s' % int(time.time())) + text='Hey d00d! %s\ + ' % int(time.time())) self.api.get_direct_message(id=message['id_str']) self.api.destroy_direct_message(id=message['id_str']) @@ -127,7 +132,8 @@ class TwythonEndpointsTestCase(unittest.TestCase): """Test sending a direct message to someone who doesn't follow you fails""" self.assertRaises(TwythonError, self.api.send_direct_message, - screen_name=protected_twitter_2, text='Yo, man! %s' % int(time.time())) + screen_name=protected_twitter_2, text='Yo, man! \ + %s' % int(time.time())) # Friends & Followers @unittest.skip('skipping non-updated test') @@ -348,7 +354,8 @@ class TwythonEndpointsTestCase(unittest.TestCase): the_list = self.api.create_list(name='Stuff %s' % int(time.time())) list_id = the_list['id_str'] - self.api.update_list(list_id=list_id, name='Stuff Renamed %s' % int(time.time())) + self.api.update_list(list_id=list_id, name='Stuff Renamed \ + %s' % int(time.time())) screen_names = ['johncena', 'xbox'] # Multi add/delete members @@ -359,7 +366,8 @@ class TwythonEndpointsTestCase(unittest.TestCase): # Single add/delete member self.api.add_list_member(list_id=list_id, screen_name='justinbieber') - self.api.delete_list_member(list_id=list_id, screen_name='justinbieber') + self.api.delete_list_member(list_id=list_id, + screen_name='justinbieber') self.api.delete_list(list_id=list_id) diff --git a/twython/api.py b/twython/api.py index 9ae69a1..3a37909 100644 --- a/twython/api.py +++ b/twython/api.py @@ -194,12 +194,14 @@ class Twython(EndpointsMixin, object): raise ExceptionType(error_message, error_code=response.status_code, - retry_after=response.headers.get('retry-after')) + retry_after=response.headers.get('retry-\ + after')) try: content = response.json() except ValueError: - raise TwythonError('Response was not valid JSON. Unable to decode.') + raise TwythonError('Response was not valid JSON. \ + Unable to decode.') return content @@ -209,7 +211,8 @@ class Twython(EndpointsMixin, object): error_message = 'An error occurred processing your request.' try: content = response.json() - # {"errors":[{"code":34,"message":"Sorry, that page does not exist"}]} + # {"errors":[{"code":34,"message":"Sorry, + # that page does not exist"}]} error_message = content['errors'][0]['message'] except ValueError: # bad json data from Twitter for an error @@ -223,13 +226,18 @@ class Twython(EndpointsMixin, object): def request(self, endpoint, 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 (e.g. search/tweets) + :param endpoint: (required) Full url or Twitter API endpoint + (e.g. search/tweets) :type endpoint: string - :param method: (optional) Method of accessing data, either GET or POST. (default GET) + :param method: (optional) Method of accessing data, either + GET or POST. (default GET) :type method: string - :param params: (optional) Dict of parameters (if any) accepted the by Twitter API endpoint you are trying to access (default None) + :param params: (optional) Dict of parameters (if any) accepted + the by Twitter API endpoint you are trying to + access (default None) :type params: dict or None - :param version: (optional) Twitter API version to access (default 1.1) + :param version: (optional) Twitter API version to access + (default 1.1) :type version: string :rtype: dict @@ -242,7 +250,8 @@ class Twython(EndpointsMixin, object): else: url = '%s/%s.json' % (self.api_url % version, endpoint) - content = self._request(url, method=method, params=params, api_call=url) + content = self._request(url, method=method, params=params, + api_call=url) return content @@ -258,7 +267,8 @@ class Twython(EndpointsMixin, object): """Returns a specific header from the last API call This will return None if the header is not present - :param header: (required) The name of the header you want to get the value of + :param header: (required) The name of the header you want to get + the value of Most useful for the following header information: x-rate-limit-limit, @@ -268,21 +278,31 @@ class Twython(EndpointsMixin, object): """ if self._last_call is None: - raise TwythonError('This function must be called after an API call. It delivers header information.') + raise TwythonError('This function must be called after an API call. \ + It delivers header information.') return self._last_call['headers'].get(header, default_return_value) - def get_authentication_tokens(self, callback_url=None, force_login=False, screen_name=''): - """Returns a dict including an authorization URL, ``auth_url``, to direct a user to + def get_authentication_tokens(self, callback_url=None, force_login=False, + screen_name=''): + """Returns a dict including an authorization URL, ``auth_url``, to + direct a user to - :param callback_url: (optional) Url the user is returned to after they authorize your app (web clients only) - :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. - :param screen_name: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value + :param callback_url: (optional) Url the user is returned to after + they authorize your app (web clients only) + :param force_login: (optional) Forces the user to enter their + credentials to ensure the correct users + account is authorized. + :param screen_name: (optional) If forced_login is set OR user is + not currently logged in, Prefills the username + input box of the OAuth login screen with the + given value :rtype: dict """ if self.oauth_version != 1: - raise TwythonError('This method can only be called when your OAuth version is 1.0.') + raise TwythonError('This method can only be called when your \ + OAuth version is 1.0.') request_args = {} if callback_url: @@ -290,15 +310,18 @@ class Twython(EndpointsMixin, object): response = self.client.get(self.request_token_url, params=request_args) if response.status_code == 401: - raise TwythonAuthError(response.content, error_code=response.status_code) + raise TwythonAuthError(response.content, + error_code=response.status_code) elif response.status_code != 200: - raise TwythonError(response.content, error_code=response.status_code) + raise TwythonError(response.content, + error_code=response.status_code) request_tokens = dict(parse_qsl(response.content.decode('utf-8'))) if not request_tokens: raise TwythonError('Unable to decode request tokens.') - oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true' + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') \ + == 'true' auth_url_params = { 'oauth_token': request_tokens['oauth_token'], @@ -314,21 +337,28 @@ class Twython(EndpointsMixin, object): if callback_url and not oauth_callback_confirmed: auth_url_params['oauth_callback'] = self.callback_url - request_tokens['auth_url'] = self.authenticate_url + '?' + urlencode(auth_url_params) + request_tokens['auth_url'] = self.authenticate_url + \ + '?' + urlencode(auth_url_params) return request_tokens def get_authorized_tokens(self, oauth_verifier): - """Returns a dict of authorized tokens after they go through the :class:`get_authentication_tokens` phase. + """Returns a dict of authorized tokens after they go through the + :class:`get_authentication_tokens` phase. - :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN for non web apps) retrieved from the callback url querystring + :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN + for non web apps) retrieved from the callback url querystring :rtype: dict """ if self.oauth_version != 1: - raise TwythonError('This method can only be called when your OAuth version is 1.0.') + raise TwythonError('This method can only be called when your \ + OAuth version is 1.0.') - response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}, headers={'Content-Type': 'application/json'}) + response = self.client.get(self.access_token_url, + params={'oauth_verifier': oauth_verifier}, + headers={'Content-Type': 'application/\ + json'}) if response.status_code == 401: try: @@ -341,7 +371,8 @@ class Twython(EndpointsMixin, object): except ValueError: content = {} - raise TwythonError(content.get('error', 'Invalid / expired Token'), error_code=response.status_code) + raise TwythonError(content.get('error', 'Invalid / expired To \ + ken'), error_code=response.status_code) authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) if not authorized_tokens: @@ -350,12 +381,14 @@ class Twython(EndpointsMixin, object): return authorized_tokens # pragma: no cover def obtain_access_token(self): - """Returns an OAuth 2 access token to make OAuth 2 authenticated read-only calls. + """Returns an OAuth 2 access token to make OAuth 2 authenticated + read-only calls. :rtype: string """ if self.oauth_version != 2: - raise TwythonError('This method can only be called when your OAuth version is 2.0.') + raise TwythonError('This method can only be called when your \ + OAuth version is 2.0.') data = {'grant_type': 'client_credentials'} basic_auth = HTTPBasicAuth(self.app_key, self.app_secret) @@ -377,8 +410,10 @@ class Twython(EndpointsMixin, object): def construct_api_url(api_url, **params): """Construct a Twitter API url, encoded, with parameters - :param api_url: URL of the Twitter API endpoint you are attempting to construct - :param \*\*params: Parameters that are accepted by Twitter for the endpoint you're requesting + :param api_url: URL of the Twitter API endpoint you are attempting + to construct + :param \*\*params: Parameters that are accepted by Twitter for the + endpoint you're requesting :rtype: string Usage:: @@ -387,7 +422,8 @@ class Twython(EndpointsMixin, object): >>> twitter = Twython() >>> api_url = 'https://api.twitter.com/1.1/search/tweets.json' - >>> constructed_url = twitter.construct_api_url(api_url, q='python', result_type='popular') + >>> constructed_url = twitter.construct_api_url(api_url, q='python', + result_type='popular') >>> print constructed_url https://api.twitter.com/1.1/search/tweets.json?q=python&result_type=popular @@ -403,7 +439,8 @@ class Twython(EndpointsMixin, object): def search_gen(self, search_query, **params): # pragma: no cover warnings.warn( - 'This method is deprecated. You should use Twython.cursor instead. [eg. Twython.cursor(Twython.search, q=\'your_query\')]', + 'This method is deprecated. You should use Twython.cursor instead. \ + [eg. Twython.cursor(Twython.search, q=\'your_query\')]', TwythonDeprecationWarning, stacklevel=2 ) @@ -412,14 +449,17 @@ class Twython(EndpointsMixin, object): def cursor(self, function, return_pages=False, **params): """Returns a generator for results that match a specified query. - :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) - :param \*\*params: Extra parameters to send with your request (usually parameters accepted by the Twitter API endpoint) + :param function: Instance of a Twython function + (Twython.get_home_timeline, Twython.search) + :param \*\*params: Extra parameters to send with your request + (usually parameters accepted by the Twitter API endpoint) :rtype: generator Usage:: >>> from twython import Twython - >>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + >>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, + OAUTH_TOKEN_SECRET) >>> results = twitter.cursor(twitter.search, q='python') >>> for result in results: @@ -427,7 +467,8 @@ class Twython(EndpointsMixin, object): """ if not hasattr(function, 'iter_mode'): - raise TwythonError('Unable to create generator for Twython method "%s"' % function.__name__) + raise TwythonError('Unable to create generator for Twython \ + method "%s"' % function.__name__) while True: content = function(**params) @@ -446,22 +487,26 @@ class Twython(EndpointsMixin, object): for result in results: yield result - if function.iter_mode == 'cursor' and content['next_cursor_str'] == '0': + if function.iter_mode == 'cursor' and \ + content['next_cursor_str'] == '0': raise StopIteration try: if function.iter_mode == 'id': - if not 'max_id' in params: - # Add 1 to the id because since_id and max_id are inclusive + if 'max_id' not in params: + # Add 1 to the id because since_id and + # max_id are inclusive if hasattr(function, 'iter_metadata'): - since_id = content[function.iter_metadata].get('since_id_str') + since_id = content[function.iter_metadata]\ + .get('since_id_str') else: since_id = content[0]['id_str'] params['since_id'] = (int(since_id) - 1) elif function.iter_mode == 'cursor': params['cursor'] = content['next_cursor_str'] except (TypeError, ValueError): # pragma: no cover - raise TwythonError('Unable to generate next page of search results, `page` is not a number.') + raise TwythonError('Unable to generate next page of search \ + results, `page` is not a number.') @staticmethod def unicode2utf8(text): @@ -483,11 +528,14 @@ class Twython(EndpointsMixin, object): """Return HTML for a tweet (urls, mentions, hashtags replaced with links) :param tweet: Tweet object from received from Twitter API - :param use_display_url: Use display URL to represent link (ex. google.com, github.com). Default: True - :param use_expanded_url: Use expanded URL to represent link (e.g. http://google.com). Default False + :param use_display_url: Use display URL to represent link + (ex. google.com, github.com). Default: True + :param use_expanded_url: Use expanded URL to represent link + (e.g. http://google.com). Default False If use_expanded_url is True, it overrides use_display_url. - If use_display_url and use_expanded_url is False, short url will be used (t.co/xxxxx) + If use_display_url and use_expanded_url is False, short url will + be used (t.co/xxxxx) """ if 'retweeted_status' in tweet: @@ -502,7 +550,8 @@ class Twython(EndpointsMixin, object): start, end = entity['indices'][0], entity['indices'][1] mention_html = '@%(screen_name)s' - text = text.replace(tweet['text'][start:end], mention_html % {'screen_name': entity['screen_name']}) + text = text.replace(tweet['text'][start:end], + mention_html % {'screen_name': entity['screen_name']}) # Hashtags for entity in entities['hashtags']: @@ -514,7 +563,8 @@ class Twython(EndpointsMixin, object): # Urls for entity in entities['urls']: start, end = entity['indices'][0], entity['indices'][1] - if use_display_url and entity.get('display_url') and not use_expanded_url: + if use_display_url and entity.get('display_url') \ + and not use_expanded_url: shown_url = entity['display_url'] elif use_expanded_url and entity.get('expanded_url'): shown_url = entity['expanded_url'] @@ -522,6 +572,7 @@ class Twython(EndpointsMixin, object): shown_url = entity['url'] url_html = '%s' - text = text.replace(tweet['text'][start:end], url_html % (entity['url'], shown_url)) + text = text.replace(tweet['text'][start:end], + url_html % (entity['url'], shown_url)) return text diff --git a/twython/streaming/api.py b/twython/streaming/api.py index c814acb..47678e4 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -38,8 +38,11 @@ class TwythonStreamer(object): retired :param retry_in: (optional) Amount of time (in secs) the previous API call should be tried again - :param client_args: (optional) Accepts some requests Session parameters and some requests Request parameters. - See http://docs.python-requests.org/en/latest/api/#sessionapi and requests section below it for details. + :param client_args: (optional) Accepts some requests Session + parameters and some requests Request parameters. + See + http://docs.python-requests.org/en/latest/api/#sessionapi + and requests section below it for details. [ex. headers, proxies, verify(SSL verification)] :param handlers: (optional) Array of message types for which corresponding handlers will be called @@ -53,11 +56,12 @@ class TwythonStreamer(object): self.client_args = client_args or {} default_headers = {'User-Agent': 'Twython Streaming v' + __version__} - if not 'headers' in self.client_args: + if 'headers' not in self.client_args: # If they didn't set any headers, set our defaults for them self.client_args['headers'] = default_headers elif 'User-Agent' not in self.client_args['headers']: - # If they set headers, but didn't include User-Agent.. set it for them + # If they set headers, but didn't include User-Agent.. + # set it for them self.client_args['headers'].update(default_headers) self.client_args['timeout'] = timeout @@ -87,7 +91,8 @@ class TwythonStreamer(object): self.connected = False - self.handlers = handlers if handlers else ['delete', 'limit', 'disconnect'] + self.handlers = handlers if handlers else \ + ['delete', 'limit', 'disconnect'] self.chunk_size = chunk_size @@ -103,7 +108,8 @@ class TwythonStreamer(object): def _send(retry_counter): requests_args = {} for k, v in self.client_args.items(): - # Maybe this should be set as a class variable and only done once? + # Maybe this should be set as a class + # variable and only done once? if k in ('timeout', 'allow_redirects', 'verify'): requests_args[k] = v @@ -121,7 +127,8 @@ class TwythonStreamer(object): if response.status_code != 200: self.on_error(response.status_code, response.content) - if self.retry_count and (self.retry_count - retry_counter) > 0: + if self.retry_count and \ + (self.retry_count - retry_counter) > 0: time.sleep(self.retry_in) retry_counter += 1 _send(retry_counter) @@ -140,13 +147,19 @@ class TwythonStreamer(object): line = line.decode('utf-8') data = json.loads(line) except ValueError: # pragma: no cover - self.on_error(response.status_code, 'Unable to decode response, not valid JSON.') + self.on_error(response.status_code, + 'Unable to decode response, \ + not valid JSON.') else: if self.on_success(data): # pragma: no cover for message_type in self.handlers: if message_type in data: - handler = getattr(self, 'on_' + message_type, None) - if handler and callable(handler) and not handler(data.get(message_type)): + handler = getattr(self, + 'on_' + message_type, + None) + if handler \ + and callable(handler) \ + and not handler(data.get(message_type)): break response.close() -- 2.39.5 From 54647cf0a940afedb048d0c47c67bb6562e25c9c Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 5 Aug 2014 06:15:59 -0400 Subject: [PATCH 093/204] Revert 1aa2d45..3bef603 This rolls back to commit 1aa2d451d9edc063122bfd7a88e5c104bed08017. --- examples/block_spammer.py | 20 -------------------- examples/block_user.py | 20 -------------------- examples/unblock_user.py | 20 -------------------- examples/unfollow_user.py | 20 -------------------- 4 files changed, 80 deletions(-) delete mode 100644 examples/block_spammer.py delete mode 100644 examples/block_user.py delete mode 100644 examples/unblock_user.py delete mode 100644 examples/unfollow_user.py diff --git a/examples/block_spammer.py b/examples/block_spammer.py deleted file mode 100644 index c579904..0000000 --- a/examples/block_spammer.py +++ /dev/null @@ -1,20 +0,0 @@ -from twython import Twython, TwythonError - -# Optionally accept user data from the command line (or elsewhere). -# -# Usage: follow_user.py ryanmcgrath - -import sys - -if len(sys.argv) >= 2: - target = sys.argv[1] -else: - target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") - -# Requires Authentication as of Twitter API v1.1 -twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - -try: - twitter.report_spam(screen_name=target, follow="true") -except TwythonError as e: - print(e) diff --git a/examples/block_user.py b/examples/block_user.py deleted file mode 100644 index a06c5f0..0000000 --- a/examples/block_user.py +++ /dev/null @@ -1,20 +0,0 @@ -from twython import Twython, TwythonError - -# Optionally accept user data from the command line (or elsewhere). -# -# Usage: follow_user.py ryanmcgrath - -import sys - -if len(sys.argv) >= 2: - target = sys.argv[1] -else: - target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") - -# Requires Authentication as of Twitter API v1.1 -twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - -try: - twitter.create_block(screen_name=target, follow="true") -except TwythonError as e: - print(e) diff --git a/examples/unblock_user.py b/examples/unblock_user.py deleted file mode 100644 index 80ea19c..0000000 --- a/examples/unblock_user.py +++ /dev/null @@ -1,20 +0,0 @@ -from twython import Twython, TwythonError - -# Optionally accept user data from the command line (or elsewhere). -# -# Usage: follow_user.py ryanmcgrath - -import sys - -if len(sys.argv) >= 2: - target = sys.argv[1] -else: - target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") - -# Requires Authentication as of Twitter API v1.1 -twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - -try: - twitter.destroy_block(screen_name=target, follow="true") -except TwythonError as e: - print(e) diff --git a/examples/unfollow_user.py b/examples/unfollow_user.py deleted file mode 100644 index 2417035..0000000 --- a/examples/unfollow_user.py +++ /dev/null @@ -1,20 +0,0 @@ -from twython import Twython, TwythonError - -# Optionally accept user data from the command line (or elsewhere). -# -# Usage: follow_user.py ryanmcgrath - -import sys - -if len(sys.argv) >= 2: - target = sys.argv[1] -else: - target = raw_input("User to follow: ") # For Python 3.x use: target = input("User to follow: ") - -# Requires Authentication as of Twitter API v1.1 -twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - -try: - twitter.destroy_friendship(screen_name=target, follow="true") -except TwythonError as e: - print(e) -- 2.39.5 From 14e9349f48cdc6f779b430c5518b59c9406389b0 Mon Sep 17 00:00:00 2001 From: Diego Allen Date: Thu, 21 Aug 2014 21:07:59 -0400 Subject: [PATCH 094/204] Add missing comma in documentation code snippet --- docs/usage/basic_usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/basic_usage.rst b/docs/usage/basic_usage.rst index 4ed2a3f..84ffb08 100644 --- a/docs/usage/basic_usage.rst +++ b/docs/usage/basic_usage.rst @@ -22,7 +22,7 @@ Create a Twython instance with your application keys and the users OAuth tokens .. code-block:: python from twython import Twython - twitter = Twython(APP_KEY, APP_SECRET + twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) User Information -- 2.39.5 From 27617181b1d41ee0af3746b8f90c4159f0dcd2bd Mon Sep 17 00:00:00 2001 From: Diego Allen Date: Sun, 31 Aug 2014 23:14:49 -0400 Subject: [PATCH 095/204] Fix issue #317 TwythonRateLimitError.retry_after is always None. --- twython/api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/twython/api.py b/twython/api.py index 3a37909..499b83b 100644 --- a/twython/api.py +++ b/twython/api.py @@ -194,8 +194,7 @@ class Twython(EndpointsMixin, object): raise ExceptionType(error_message, error_code=response.status_code, - retry_after=response.headers.get('retry-\ - after')) + retry_after=response.headers.get('X-Rate-Limit-Reset')) try: content = response.json() @@ -497,8 +496,7 @@ class Twython(EndpointsMixin, object): # Add 1 to the id because since_id and # max_id are inclusive if hasattr(function, 'iter_metadata'): - since_id = content[function.iter_metadata]\ - .get('since_id_str') + since_id = content[function.iter_metadata].get('since_id_str') else: since_id = content[0]['id_str'] params['since_id'] = (int(since_id) - 1) -- 2.39.5 From f2a535b2508b9bb1f622b34d74994838af810bfe Mon Sep 17 00:00:00 2001 From: Diego Allen Date: Mon, 1 Sep 2014 09:06:07 -0400 Subject: [PATCH 096/204] Formatting fixes --- twython/api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index 499b83b..6e33904 100644 --- a/twython/api.py +++ b/twython/api.py @@ -192,9 +192,10 @@ class Twython(EndpointsMixin, object): # app keys/user tokens ExceptionType = TwythonAuthError - raise ExceptionType(error_message, - error_code=response.status_code, - retry_after=response.headers.get('X-Rate-Limit-Reset')) + raise ExceptionType( + error_message, + error_code=response.status_code, + retry_after=response.headers.get('X-Rate-Limit-Reset')) try: content = response.json() -- 2.39.5 From f22dcebbbbb319aaa484e7dcf0511f367e60a7fb Mon Sep 17 00:00:00 2001 From: Renan Cakirerk Date: Fri, 19 Sep 2014 01:48:37 -0700 Subject: [PATCH 097/204] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 143a277..e15e73c 100644 --- a/README.rst +++ b/README.rst @@ -137,7 +137,7 @@ Now that you have the ``oauth_verifier`` stored to a variable, you'll want to cr Once you have the final user tokens, store them in a database for later use!:: OAUTH_TOKEN = final_step['oauth_token'] - OAUTH_TOKEN_SECERT = final_step['oauth_token_secret'] + OAUTH_TOKEN_SECRET = final_step['oauth_token_secret'] For OAuth 2 (Application Only, read-only) authentication, see `our documentation `_ -- 2.39.5 From bcb54c43fe371e96a6445f16f0dd4e756dd6c633 Mon Sep 17 00:00:00 2001 From: Donne Martin Date: Fri, 19 Sep 2014 17:58:27 -0400 Subject: [PATCH 098/204] Fix for Typos in README.rst #344 --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 143a277..80bfa75 100644 --- a/README.rst +++ b/README.rst @@ -115,7 +115,7 @@ Handling the Callback If your application is a Desktop or Mobile Application *oauth_verifier* will be the PIN code -After they authorize your application to access some of their account details, they'll be redirected to the callback url you specified in ``get_autentication_tokens`` +After they authorize your application to access some of their account details, they'll be redirected to the callback url you specified in ``get_authentication_tokens`` You'll want to extract the ``oauth_verifier`` from the url. @@ -134,10 +134,10 @@ Now that you have the ``oauth_verifier`` stored to a variable, you'll want to cr final_step = twitter.get_authorized_tokens(oauth_verifier) -Once you have the final user tokens, store them in a database for later use!:: +Once you have the final user tokens, store them in a database for later use:: OAUTH_TOKEN = final_step['oauth_token'] - OAUTH_TOKEN_SECERT = final_step['oauth_token_secret'] + OAUTH_TOKEN_SECRET = final_step['oauth_token_secret'] For OAuth 2 (Application Only, read-only) authentication, see `our documentation `_ -- 2.39.5 From 83379b42e6ba5249265a26e23cd2345a521fb21b Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Thu, 23 Oct 2014 12:03:33 -0300 Subject: [PATCH 099/204] adding upload media endpoint --- twython/endpoints.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/twython/endpoints.py b/twython/endpoints.py index bafdd7f..9b6fca1 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -120,6 +120,16 @@ class EndpointsMixin(object): """ return self.post('statuses/update_with_media', params=params) + def upload_media(self, **params): + """Uploads media file to Twitter servers. The file will be available to be attached + to a status for 60 minutes. To attach to a update, pass a list of returned media ids + to the 'update_status' method using the 'media_ids' param. + + Docs: + https://dev.twitter.com/rest/public/uploading-media-multiple-photos + """ + return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) + def get_oembed_tweet(self, **params): """Returns information allowing the creation of an embedded representation of a Tweet on third party sites. -- 2.39.5 From 28290cd7c3b1f7f0d2b63cf912869313e9c95e29 Mon Sep 17 00:00:00 2001 From: Mertcan Mermerkaya Date: Wed, 29 Oct 2014 01:26:03 +0200 Subject: [PATCH 100/204] Update advanced_usage.rst Fixed wrong variable name in function arguments. --- docs/usage/advanced_usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index db36301..b95cdb3 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -55,7 +55,7 @@ Posting a Status with an Editing Image # unable to be read image_io.seek(0) - twitter.update_status_with_media(media=photo, status='Check out my edited image!') + twitter.update_status_with_media(media=image_io, status='Check out my edited image!') Search Generator -- 2.39.5 From 8548a312389703d02722bdde6fbfff587133a771 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 12 Aug 2014 13:38:20 -0400 Subject: [PATCH 101/204] Changing versions, adding AUTHORS & HISTORY --- AUTHORS.rst | 11 +++++++++-- HISTORY.rst | 12 ++++++++++++ docs/conf.py | 4 ++-- setup.py | 4 ++-- twython/__init__.py | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6cddac6..ab86784 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,7 +27,7 @@ Patches and Suggestions - `Jonathan Elsas `_, Fix for original Streaming API stub causing import errors. - `LuqueDaniel `_, Extended example code where necessary. - `Mesar Hameed `_, Commit to swap ``__getattr__`` trick for a more debuggable solution. -- `Remy DeCausemaker `_, PEP-8 contributions. +- `Remy DeCausemaker `_, PEP 8 contributions. - `mckellister `_ Twitter Spring 2012 Clean Up fixes to ``Exception`` raised by Twython (Rate Limits, etc). - `Tatz Tsuchiya `_, Fix for ``lambda`` scoping in key injection phase. - `Mohammed ALDOUB `_, Fixes for ``http/https`` access endpoints. @@ -35,7 +35,7 @@ Patches and Suggestions - `Terry Jones `_, Error cleanup and Exception processing in 2.3.0. - `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. - `Chris Brown `_, Updated to use v1.1 endpoints over v1 -- `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. +- `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. - `Paul Solbach `_, fixed requirement for oauth_verifier - `Greg Nofi `_, fixed using built-in Exception attributes for storing & retrieving error message - `Jonathan Vanasco `_, Debugging support, error_code tracking, Twitter error API tracking, other fixes @@ -43,3 +43,10 @@ Patches and Suggestions - `Ruben Varela Rosa `_, Fixed search example - `Oleg Anashkin `_, streaming ``handlers`` functionality - `Luis Alberto Santana `_, Fixed issue where Twython was unnecessarily disabling compression +- `Cory Dolphin `_, Added retry_after attribute to TwythonRateLimitError +- `Natan L `_, Fixed typo in documentation +- `Cash Costello `_, Moved tests to use `responsoes`, fixed typos in documentation +- `Joe Cabrera `_, PEP 8 contributions +- `bsbkeven `_, Added `lookup_status` function to `endpoints.py` +- `drevicko `_, Added option to yield full page vs individual results in `cursor` diff --git a/HISTORY.rst b/HISTORY.rst index 1259227..4be8b5a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,18 @@ History ------- +3.2.0 (2014-08-xx) +++++++++++++++++++ +- PEP8'd some code +- Added `lookup_status` function to `endpoints.py` +- Added keyword argument to `cursor` to return full pages rather than individual results +- `cursor` now uses while loop rather than recursion +- Fixed issue where Twython was unnecessarily disabling compression +- Using `responses` to mock API calls in tests +- Fixed some typos in documentation +- Added retry_after attribute to TwythonRateLimitError + + 3.1.2 (2013-12-05) ++++++++++++++++++ diff --git a/docs/conf.py b/docs/conf.py index b0b98eb..a0f9ef4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.1.2' +version = '3.2.0' # The full version, including alpha/beta/rc tags. -release = '3.1.2' +release = '3.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index e6c8e94..09ea8ec 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.1.2' +__version__ = '3.2.0' packages = [ 'twython', @@ -23,7 +23,7 @@ if sys.argv[-1] == 'publish': setup( name='twython', version=__version__, - install_requires=['requests==2.1.0', 'requests_oauthlib==0.4.0'], + install_requires=['requests2.1.0', 'requests_oauthlib==0.4.0'], author='Ryan McGrath', author_email='ryan@venodesigns.net', license=open('LICENSE').read(), diff --git a/twython/__init__.py b/twython/__init__.py index e0920a6..a79667d 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.1.2' +__version__ = '3.2.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 23a0b62f2c2464d446444d98dadf466288facbcb Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 Oct 2014 11:07:18 -0400 Subject: [PATCH 102/204] Update AUTHORS, HISTORY, DeprecationWarning on update_status_with_media --- AUTHORS.rst | 4 ++++ HISTORY.rst | 6 ++++-- twython/endpoints.py | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index ab86784..24bd911 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -50,3 +50,7 @@ Patches and Suggestions - `Joe Cabrera `_, PEP 8 contributions - `bsbkeven `_, Added `lookup_status` function to `endpoints.py` - `drevicko `_, Added option to yield full page vs individual results in `cursor` +- `Filipe A Ximenes `_, Added `upload_media` function to `endpoints.py` +- `Mertcan Mermerkaya `_, Fixed code example in documentation +- `Donne Martin `_, Fixed typos in `README.rst` +- `Diego Allen `_, Add missing comma in documentation code snippet diff --git a/HISTORY.rst b/HISTORY.rst index 4be8b5a..03567f4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,8 +11,10 @@ History - `cursor` now uses while loop rather than recursion - Fixed issue where Twython was unnecessarily disabling compression - Using `responses` to mock API calls in tests -- Fixed some typos in documentation -- Added retry_after attribute to TwythonRateLimitError +- Fixed some typos in documentation +- Added `retry_after` attribute to `TwythonRateLimitError` +- Added `upload_media` method to `Twython` in favor of `update_with_media` +- Deprecating `update_with_media` per Twitter API 1.1 (https://dev.twitter.com/rest/reference/post/statuses/update_with_media) 3.1.2 (2013-12-05) diff --git a/twython/endpoints.py b/twython/endpoints.py index 9b6fca1..444a920 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -14,6 +14,10 @@ This map is organized the order functions are documented at: https://dev.twitter.com/docs/api/1.1 """ +import warnings + +from .advisory import TwythonDeprecationWarning + class EndpointsMixin(object): # Timelines @@ -118,6 +122,11 @@ class EndpointsMixin(object): https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media """ + warnings.warn( + 'This method is deprecated. You should use Twython.upload_media instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) return self.post('statuses/update_with_media', params=params) def upload_media(self, **params): -- 2.39.5 From e42728166095b1ad1b9fa07dfb00ce0bcf245a67 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 Oct 2014 11:07:23 -0400 Subject: [PATCH 103/204] Fixes #305 --- twython/api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index 6e33904..0b421c3 100644 --- a/twython/api.py +++ b/twython/api.py @@ -9,6 +9,8 @@ Twitter Authentication, and miscellaneous methods that are useful when dealing with the Twitter API """ +import warnings + import requests from requests.auth import HTTPBasicAuth from requests_oauthlib import OAuth1, OAuth2 @@ -20,8 +22,6 @@ from .endpoints import EndpointsMixin from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .helpers import _transparent_params -import warnings - warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > @@ -243,9 +243,12 @@ class Twython(EndpointsMixin, object): :rtype: dict """ + if endpoint.startswith('http://'): + raise TwythonError('api.twitter.com is restricted to SSL/TLS traffic.') + # In case they want to pass a full Twitter URL # i.e. https://api.twitter.com/1.1/search/tweets.json - if endpoint.startswith('http://') or endpoint.startswith('https://'): + if endpoint.startswith('https://'): url = endpoint else: url = '%s/%s.json' % (self.api_url % version, endpoint) -- 2.39.5 From 02d1a946c19b52229b7522d1c4acf227b5aaf284 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 Oct 2014 16:49:03 -0400 Subject: [PATCH 104/204] Unpin requests --- requirements.txt | 6 +++--- setup.py | 2 +- tests/test_core.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 917c31a..126cbe4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ coverage==3.6.0 -requests==2.1.0 -requests_oauthlib==0.4.0 +requests>=2.1.0 +requests_oauthlib>=0.4.0 python-coveralls==2.1.0 nose-cov==1.6 -responses==0.2.0 +responses==0.3.0 diff --git a/setup.py b/setup.py index 09ea8ec..e203cc9 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ if sys.argv[-1] == 'publish': setup( name='twython', version=__version__, - install_requires=['requests2.1.0', 'requests_oauthlib==0.4.0'], + install_requires=['requests>=2.1.0', 'requests_oauthlib>=0.4.0'], author='Ryan McGrath', author_email='ryan@venodesigns.net', license=open('LICENSE').read(), diff --git a/tests/test_core.py b/tests/test_core.py index 98d3a47..1aa0eea 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -262,16 +262,16 @@ class TwythonAPITestCase(unittest.TestCase): """Test getting last specific header of the last API call works""" endpoint = 'statuses/home_timeline' url = self.get_url(endpoint) - self.register_response(responses.GET, url, adding_headers={'x-rate-limit-remaining': 37}) + self.register_response(responses.GET, url, adding_headers={'x-rate-limit-remaining': '37'}) self.api.get(endpoint) value = self.api.get_lastfunction_header('x-rate-limit-remaining') - self.assertEqual(37, value) + self.assertEqual('37', value) value2 = self.api.get_lastfunction_header('does-not-exist') self.assertIsNone(value2) - value3 = self.api.get_lastfunction_header('not-there-either', 96) - self.assertEqual(96, value3) + value3 = self.api.get_lastfunction_header('not-there-either', '96') + self.assertEqual('96', value3) def test_get_lastfunction_header_should_raise_error_when_no_previous_call(self): """Test attempting to get a header when no API call was made raises a TwythonError""" @@ -286,7 +286,7 @@ class TwythonAPITestCase(unittest.TestCase): self.api.get(endpoint) - self.assertEqual(b'gzip, deflate, compress', responses.calls[0].request.headers['Accept-Encoding']) + self.assertEqual(b'gzip, deflate', responses.calls[0].request.headers['Accept-Encoding']) # Static methods def test_construct_api_url(self): -- 2.39.5 From 1d383d93033976056c7afb5ea2ba101ba78770ea Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 Oct 2014 16:49:58 -0400 Subject: [PATCH 105/204] Update HISTORY.rst --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 03567f4..4454d4c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,6 +15,7 @@ History - Added `retry_after` attribute to `TwythonRateLimitError` - Added `upload_media` method to `Twython` in favor of `update_with_media` - Deprecating `update_with_media` per Twitter API 1.1 (https://dev.twitter.com/rest/reference/post/statuses/update_with_media) +- Unpin `requests` and `requests-oauthlib` in `requirements.txt` 3.1.2 (2013-12-05) -- 2.39.5 From 134414c87a3aa7b26b1800c188de412aca162b87 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 Oct 2014 17:05:14 -0400 Subject: [PATCH 106/204] Update HISTORY.rst --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4454d4c..f9d129c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ History ------- -3.2.0 (2014-08-xx) +3.2.0 (2014-10-30) ++++++++++++++++++ - PEP8'd some code - Added `lookup_status` function to `endpoints.py` -- 2.39.5 From 123b8f4f74a7e1322bc602f66e6e962dc5ff767a Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sun, 2 Nov 2014 10:41:56 +1100 Subject: [PATCH 107/204] Added the create, destroy, list and list IDs mute(s) endpoints. Roughly modelled on the similar-ish block user functions (note: the "mutes/users" instead of just "mutes" appears to be intentional by Twitter). Also added myself to AUTHORS.rst because I reckon adding API bits makes the grade. ;) --- AUTHORS.rst | 1 + twython/endpoints.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 24bd911..8a16050 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -54,3 +54,4 @@ Patches and Suggestions - `Mertcan Mermerkaya `_, Fixed code example in documentation - `Donne Martin `_, Fixed typos in `README.rst` - `Diego Allen `_, Add missing comma in documentation code snippet +- `Ben McGinnes `_, Added mute API endpoints, a couple of examples, random bits. diff --git a/twython/endpoints.py b/twython/endpoints.py index 444a920..7ac9961 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -523,6 +523,46 @@ class EndpointsMixin(object): """ return self.get('users/profile_banner', params=params) + def list_mutes(self, **params): + """Returns a collection of user objects that the authenticating user + is muting. + + Docs: https://dev.twitter.com/docs/api/1.1/get/mutes/users/list + + """ + return self.get('mutes/users/list', params=params) + list_mutes.iter_mode = 'cursor' + list_mutes.iter_key = 'users' + + def list_mute_ids(self, **params): + """Returns an array of numeric user ids the authenticating user + is muting. + + Docs: https://dev.twitter.com/docs/api/1.1/get/mutes/users/ids + + """ + return self.get('mutes/users/ids', params=params) + list_mutes_ids.iter_mode = 'cursor' + list_mutes_ids.iter_key = 'ids' + + def create_mute(self, **params): + """Mutes the specified user, preventing their tweets appearing + in the authenticating user's timeline. + + Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/create + + """ + return self.post('mutes/users/create', params=params) + + def destroy_mute(self, **params): + """Un-mutes the user specified in the user or ID parameter for + the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/destroy + + """ + return self.post('mutes/users/destroy', params=params) + # Suggested Users def get_user_suggestions_by_slug(self, **params): """Access the users in a given category of the Twitter suggested user list. -- 2.39.5 From b5847e3e84f76a9659305580e9042d33af0fa870 Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sun, 2 Nov 2014 14:15:37 +1100 Subject: [PATCH 108/204] Added muting to test suite. I should have checked for this earlier, ah well, 'tis here now. Again, basically a copy of the blocking code updated for muting. Excellent target to test it on too. --- tests/test_endpoints.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index b7478d0..aa79998 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -305,6 +305,28 @@ class TwythonEndpointsTestCase(unittest.TestCase): we have not uploaded a profile banner""" self.assertRaises(TwythonError, self.api.get_profile_banner_sizes) + @unittest.skip('skipping non-updated test') + def test_list_mutes(self): + """Test listing users who are muted by the authenticated user + succeeds""" + self.api.list_mutes() + + @unittest.skip('skipping non-updated test') + def test_list_mute_ids(self): + """Test listing user ids who are muted by the authenticated user + succeeds""" + self.api.list_mute_ids() + + @unittest.skip('skipping non-updated test') + def test_create_mute(self): + """Test muting a user succeeds""" + self.api.create_mute(screen_name='justinbieber') + + @unittest.skip('skipping non-updated test') + def test_destroy_mute(self): + """Test muting a user succeeds""" + self.api.destroy_mute(screen_name='justinbieber') + # Suggested Users @unittest.skip('skipping non-updated test') def test_get_user_suggestions_by_slug(self): -- 2.39.5 From 036760bfd4a3d06b534be06bc3a56843ccb84868 Mon Sep 17 00:00:00 2001 From: Ben McGinnes Date: Sun, 2 Nov 2014 16:04:39 +1100 Subject: [PATCH 109/204] Fixed typo with list_mute_ids. --- twython/endpoints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 7ac9961..81aa582 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -542,8 +542,8 @@ class EndpointsMixin(object): """ return self.get('mutes/users/ids', params=params) - list_mutes_ids.iter_mode = 'cursor' - list_mutes_ids.iter_key = 'ids' + list_mute_ids.iter_mode = 'cursor' + list_mute_ids.iter_key = 'ids' def create_mute(self, **params): """Mutes the specified user, preventing their tweets appearing -- 2.39.5 From 245873b92f1b14328d93615f13864023d2df423c Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 3 Nov 2014 17:24:29 -0500 Subject: [PATCH 110/204] Update HISTORY.rst --- HISTORY.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index f9d129c..a7c961d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ History ------- +3.3.0 (2014-xx-xx) +++++++++++++++++++ +- Added support for muting users + 3.2.0 (2014-10-30) ++++++++++++++++++ - PEP8'd some code -- 2.39.5 From 8eb8c2dd556bb0efed9cd8f0e77ccfa59e7c0d82 Mon Sep 17 00:00:00 2001 From: Derek Hu Date: Wed, 19 Nov 2014 01:09:02 -0500 Subject: [PATCH 111/204] Remove redundant checking for oauth_token & oauth_token_secret --- twython/api.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/twython/api.py b/twython/api.py index 0b421c3..e7e0046 100644 --- a/twython/api.py +++ b/twython/api.py @@ -102,15 +102,10 @@ class Twython(EndpointsMixin, object): auth = None if oauth_version == 1: # User Authentication is through OAuth 1 - if self.app_key is not None and self.app_secret is not None and \ - self.oauth_token is None and self.oauth_token_secret is None: - auth = OAuth1(self.app_key, self.app_secret) - - if self.app_key is not None and self.app_secret is not None and \ - self.oauth_token is not None and self.oauth_token_secret is \ - not None: + if self.app_key is not None and self.app_secret is not None: auth = OAuth1(self.app_key, self.app_secret, - self.oauth_token, self.oauth_token_secret) + self.oauth_token, self.oauth_token_secret) + elif oauth_version == 2 and self.access_token: # Application Authentication is through OAuth 2 token = {'token_type': token_type, -- 2.39.5 From 3cd88a09e691b630e1bb99e739ec72a1e3e1838f Mon Sep 17 00:00:00 2001 From: ping Date: Tue, 2 Dec 2014 17:54:52 +0800 Subject: [PATCH 112/204] Format media entities in Twython.html_for_tweet() --- docs/usage/special_functions.rst | 5 +++-- tests/config.py | 4 ++-- twython/api.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index f0762c0..bd51f78 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -72,15 +72,16 @@ The above code takes all the tweets from a specific users timeline, loops over t So: - http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ https://t.co/67pwRvY6z9 + http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ https://t.co/67pwRvY6z9 http://t.co/N6InAO4B71 will be replaced with: - google.com is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ github.com + google.com is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ github.com pic.twitter.com/N6InAO4B71 .. note:: When converting the string to HTML we add a class to each HTML tag so that you can maninpulate the DOM later on. - For urls that are replaced we add ``class="twython-url"`` to the anchor tag +- For media urls that are replaced we add ``class="twython-media"`` to the anchor tag - For user mentions that are replaced we add ``class="twython-mention"`` to the anchor tag - For hashtags that are replaced we add ``class="twython-hashtag"`` to the anchor tag diff --git a/tests/config.py b/tests/config.py index 65cc2f7..21baa69 100644 --- a/tests/config.py +++ b/tests/config.py @@ -26,5 +26,5 @@ test_list_slug = os.environ.get('TEST_LIST_SLUG', 'team') test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', 'twitterapi') -test_tweet_object = {u'contributors': None, u'truncated': False, u'text': u'http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick should #checkitout. If you can! #thanks Love, @__twython__ https://t.co/67pwRvY6z9', 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': [104, 116], 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': [65, 76], u'text': u'checkitout'}, {u'indices': [90, 97], u'text': u'thanks'}], 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': [117, 140], u'expanded_url': u'https://github.com', u'display_url': u'github.com'}]}, 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 should #checkitout. If you can! #thanks Love, @__twython__ github.com' +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' diff --git a/twython/api.py b/twython/api.py index 0b421c3..5150568 100644 --- a/twython/api.py +++ b/twython/api.py @@ -577,4 +577,20 @@ class Twython(EndpointsMixin, object): text = text.replace(tweet['text'][start:end], url_html % (entity['url'], shown_url)) + # Media + if 'media' in entities: + for entity in entities['media']: + start, end = entity['indices'][0], entity['indices'][1] + if use_display_url and entity.get('display_url') \ + and not use_expanded_url: + shown_url = entity['display_url'] + elif use_expanded_url and entity.get('expanded_url'): + shown_url = entity['expanded_url'] + else: + shown_url = entity['url'] + + url_html = '%s' + text = text.replace(tweet['text'][start:end], + url_html % (entity['url'], shown_url)) + return text -- 2.39.5 From 14526cb3df40f334428a27d91985a4a7357a8108 Mon Sep 17 00:00:00 2001 From: Mirat Can Bayrak Date: Tue, 16 Dec 2014 13:40:36 +0000 Subject: [PATCH 113/204] Fixed type errror on api.py --- twython/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/twython/api.py b/twython/api.py index 5150568..d2d5c03 100644 --- a/twython/api.py +++ b/twython/api.py @@ -214,6 +214,8 @@ class Twython(EndpointsMixin, object): # {"errors":[{"code":34,"message":"Sorry, # that page does not exist"}]} error_message = content['errors'][0]['message'] + except TypeError: + error_message = content['errors'] except ValueError: # bad json data from Twitter for an error pass -- 2.39.5 From db738cf41e0d2d1a087393930b48f086d4ebc2fe Mon Sep 17 00:00:00 2001 From: Jeremy Keen Date: Fri, 13 Feb 2015 12:05:28 -0800 Subject: [PATCH 114/204] search_gen in Advanced usage deprecated Updated advanced usage documentation to use cursor instead of search_gen --- docs/usage/advanced_usage.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index b95cdb3..ff61166 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -67,9 +67,13 @@ That being said, Twython offers a generator for search results and can be access .. code-block:: python - search = twitter.search_gen('python') - for result in search: - print result + from twython import Twython + twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, + OAUTH_TOKEN_SECRET) + + results = twitter.cursor(twitter.search, q='python') + for result in results: + print result Manipulate the Request (headers, proxies, etc.) ----------------------------------------------- -- 2.39.5 From d70fa54375fd9172d4ce3b3110a2639c47476ad3 Mon Sep 17 00:00:00 2001 From: shuuji3 Date: Sat, 14 Feb 2015 17:44:45 +0900 Subject: [PATCH 115/204] Fix a broken link to the document of media_upload. --- twython/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 81aa582..d0add16 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -135,7 +135,7 @@ class EndpointsMixin(object): to the 'update_status' method using the 'media_ids' param. Docs: - https://dev.twitter.com/rest/public/uploading-media-multiple-photos + https://dev.twitter.com/rest/public/uploading-media """ return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) -- 2.39.5 From c0dae9a63ae4babad83b7e1c5c33a4adf2716a95 Mon Sep 17 00:00:00 2001 From: floydsoft Date: Mon, 23 Feb 2015 23:13:21 +0800 Subject: [PATCH 116/204] Update special_functions.rst --- docs/usage/special_functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index bd51f78..b52a245 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -37,7 +37,7 @@ The New Way twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - results = twitter.cursor(t.search, q='twitter') + results = twitter.cursor(twitter.search, q='twitter') for result in results: print result['id_str'] -- 2.39.5 From e0a2dfbbb0d53e816ea39220182a5ba6be5d7a54 Mon Sep 17 00:00:00 2001 From: Ben Bertka Date: Tue, 24 Mar 2015 11:00:49 -0700 Subject: [PATCH 117/204] Added dynamic filtering --- twython/streaming/types.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/twython/streaming/types.py b/twython/streaming/types.py index 39a9ccb..c69baa9 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -51,6 +51,7 @@ class TwythonStreamerTypesStatuses(object): """ def __init__(self, streamer): self.streamer = streamer + self.params = None def filter(self, **params): """Stream statuses/filter @@ -87,3 +88,20 @@ class TwythonStreamerTypesStatuses(object): url = 'https://stream.twitter.com/%s/statuses/firehose.json' \ % self.streamer.api_version self.streamer._request(url, params=params) + + def set_dynamic_filter(self, **params): + """Set/update statuses/filter + + :param \*\*params: Parameters to send with your stream request + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/post/statuses/filter + """ + self.params = params + + def dynamic_filter(self): + """Stream statuses/filter with dynamic parameters""" + + url = 'https://stream.twitter.com/%s/statuses/filter.json' \ + % self.streamer.api_version + self.streamer._request(url, 'POST', params=self.params) -- 2.39.5 From 6d1c82b594256992cabff4bbc671c54b92d8ca4e Mon Sep 17 00:00:00 2001 From: Ben Bertka Date: Tue, 24 Mar 2015 11:36:08 -0700 Subject: [PATCH 118/204] Fixed indents for dynamic filtering in types.py --- twython/streaming/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/streaming/types.py b/twython/streaming/types.py index c69baa9..aa6b9ad 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -51,7 +51,7 @@ class TwythonStreamerTypesStatuses(object): """ def __init__(self, streamer): self.streamer = streamer - self.params = None + self.params = None def filter(self, **params): """Stream statuses/filter @@ -97,7 +97,7 @@ class TwythonStreamerTypesStatuses(object): Accepted params found at: https://dev.twitter.com/docs/api/1.1/post/statuses/filter """ - self.params = params + self.params = params def dynamic_filter(self): """Stream statuses/filter with dynamic parameters""" -- 2.39.5 From 4c213ea10d948f30b462b9421dc2c6864acf12b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Polykanine=20A=2EK=2EA=2E=20Menelion=20Elens=C3=BA?= =?UTF-8?q?l=C3=AB?= Date: Thu, 9 Apr 2015 00:55:24 +0300 Subject: [PATCH 119/204] Corrected the docs URL for uploading media, fixes #375 --- twython/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index d0add16..60663ca 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -135,7 +135,7 @@ class EndpointsMixin(object): to the 'update_status' method using the 'media_ids' param. Docs: - https://dev.twitter.com/rest/public/uploading-media + https://dev.twitter.com/rest/reference/post/media/upload """ return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) -- 2.39.5 From 4e6963d8cc8724a9a58804c6c866cef46fb385bf Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Wed, 17 Jun 2015 16:27:31 +0100 Subject: [PATCH 120/204] =?UTF-8?q?special=5Ffunctions.rst:=20typo:=20exce?= =?UTF-8?q?pts=20=E2=86=92=20accepts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/usage/special_functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index b52a245..c89da38 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -85,7 +85,7 @@ will be replaced with: - For user mentions that are replaced we add ``class="twython-mention"`` to the anchor tag - For hashtags that are replaced we add ``class="twython-hashtag"`` to the anchor tag -This function excepts two parameters: ``use_display_url`` and ``use_expanded_url`` +This function accepts two parameters: ``use_display_url`` and ``use_expanded_url`` By default, ``use_display_url`` is ``True``. Meaning the link displayed in the tweet text will appear as (ex. google.com, github.com) If ``use_expanded_url`` is ``True``, it overrides ``use_display_url``. The urls will then be displayed as (ex. http://google.com, https://github.com) If ``use_display_url`` and ``use_expanded_url`` are ``False``, short url will be used (t.co/xxxxx) -- 2.39.5 From e68ba98c8fe9b4feceaaca69c3a557a7d218e8b4 Mon Sep 17 00:00:00 2001 From: kf <7kfpun@gmail.com> Date: Tue, 7 Jul 2015 22:16:42 +0800 Subject: [PATCH 121/204] missing commas --- docs/usage/advanced_usage.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index ff61166..6042b25 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -16,7 +16,7 @@ Create a Twython instance with your application keys and the users OAuth tokens .. code-block:: python from twython import Twython - twitter = Twython(APP_KEY, APP_SECRET + twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) Updating Status with Image @@ -68,12 +68,12 @@ That being said, Twython offers a generator for search results and can be access .. code-block:: python from twython import Twython - twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, - OAUTH_TOKEN_SECRET) + twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, + OAUTH_TOKEN_SECRET) - results = twitter.cursor(twitter.search, q='python') - for result in results: - print result + results = twitter.cursor(twitter.search, q='python') + for result in results: + print result Manipulate the Request (headers, proxies, etc.) ----------------------------------------------- @@ -94,8 +94,8 @@ Here is an example of sending custom headers to a Twitter API request: } } - twitter = Twython(APP_KEY, APP_SECRET - OAUTH_TOKEN, OAUTH_TOKEN_SECRET + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET, client_args=client_args) Here is an example of sending the request through proxies: @@ -111,8 +111,8 @@ Here is an example of sending the request through proxies: } } - twitter = Twython(APP_KEY, APP_SECRET - OAUTH_TOKEN, OAUTH_TOKEN_SECRET + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET, client_args=client_args) or both (and set a timeout variable): @@ -132,8 +132,8 @@ or both (and set a timeout variable): 'timeout': 300, } - twitter = Twython(APP_KEY, APP_SECRET - OAUTH_TOKEN, OAUTH_TOKEN_SECRET + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET, client_args=client_args) Access Headers of Previous Call @@ -146,7 +146,7 @@ If you wish to access headers (ex. x-rate-limit-remaining, x-rate-limit-reset, c from twython import Twython - twitter = Twython(APP_KEY, APP_SECRET + twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) twitter.get_home_timeline() -- 2.39.5 From 1f3eab6a3713161804f242f0159c515da772ddbe Mon Sep 17 00:00:00 2001 From: David Beitey Date: Wed, 8 Jul 2015 20:26:05 +1000 Subject: [PATCH 122/204] Update uploading image advanced examples Previously, these examples were using deprecated API endpoints. This updates the examples accordingly. --- docs/usage/advanced_usage.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index ff61166..e447e8d 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -22,17 +22,24 @@ Create a Twython instance with your application keys and the users OAuth tokens Updating Status with Image -------------------------- -Documentation: https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media +This uploads an image as a media object and associates it with a status update. .. code-block:: python photo = open('/path/to/file/image.jpg', 'rb') - twitter.update_status_with_media(status='Checkout this cool image!', media=photo) + response = twitter.upload_media(media=photo) + twitter.update_status(status='Checkout this cool image!', media_ids=[response['media_id']]) + +Documentation: + +* https://dev.twitter.com/rest/reference/post/statuses/update +* https://dev.twitter.com/rest/reference/post/media/upload Posting a Status with an Editing Image -------------------------------------- - This example resizes an image +This example resizes an image, then uploads it as a media object and associates it +with a status update. .. code-block:: python @@ -55,7 +62,9 @@ Posting a Status with an Editing Image # unable to be read image_io.seek(0) - twitter.update_status_with_media(media=image_io, status='Check out my edited image!') + + response = twitter.upload_media(media=image_io) + twitter.update_status(status='Checkout this cool image!', media_ids=[response['media_id']]) Search Generator -- 2.39.5 From dff85ace497f6fcd633fe07bce2f465a086e43d1 Mon Sep 17 00:00:00 2001 From: Davis Silverman Date: Mon, 13 Jul 2015 15:20:21 -0400 Subject: [PATCH 123/204] Have lookup_user GET and not POST --- twython/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 60663ca..356cd8a 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -458,7 +458,7 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/docs/api/1.1/get/users/lookup """ - return self.post('users/lookup', params=params) + return self.get('users/lookup', params=params) def show_user(self, **params): """Returns a variety of information about the user specified by the -- 2.39.5 From 34db3a58adaa85ca5824323f0488777efbc4b4df Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 18 Jul 2015 10:11:04 -0400 Subject: [PATCH 124/204] Update README.rst [ci skip] --- README.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 80bfa75..6f12862 100644 --- a/README.rst +++ b/README.rst @@ -2,14 +2,17 @@ Twython ======= -.. image:: https://badge.fury.io/py/twython.png - :target: http://badge.fury.io/py/twython -.. image:: https://travis-ci.org/ryanmcgrath/twython.png?branch=master - :target: https://travis-ci.org/ryanmcgrath/twython -.. image:: https://pypip.in/d/twython/badge.png - :target: https://crate.io/packages/twython/ -.. image:: https://coveralls.io/repos/ryanmcgrath/twython/badge.png?branch=master - :target: https://coveralls.io/r/ryanmcgrath/twython?branch=master +.. image:: https://img.shields.io/pypi/v/twython.svg?style=flat-square + :target: https://pypi.python.org/pypi/twython + +.. image:: https://img.shields.io/pypi/dw/twython.svg?style=flat-square + :target: https://pypi.python.org/pypi/twython + +.. image:: https://img.shields.io/travis/ryanmcgrath/twython.svg?style=flat-square + :target: https://travis-ci.org/ryanmcgrath/twython + +.. image:: https://img.shields.io/coveralls/ryanmcgrath/twython/master.svg?style=flat-square + :target: https://coveralls.io/r/ryanmcgrath/twython?branch=master ``Twython`` is the premier Python library providing an easy (and up-to-date) way to access Twitter data. Actively maintained and featuring support for Python 2.6+ and Python 3. It's been battle tested by companies, educational institutions and individuals alike. Try it today! -- 2.39.5 From faee7ea3ff28c3cbbeba08b6005f4c9bb8ec5fd8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 18 Jul 2015 10:32:30 -0400 Subject: [PATCH 125/204] Update AUTHORS.rst, Update HISTORY.rst, Update version [ci skip] --- AUTHORS.rst | 8 ++++++++ HISTORY.rst | 5 ++++- docs/conf.py | 4 ++-- setup.py | 2 +- twython/__init__.py | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 8a16050..739e11f 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -55,3 +55,11 @@ Patches and Suggestions - `Donne Martin `_, Fixed typos in `README.rst` - `Diego Allen `_, Add missing comma in documentation code snippet - `Ben McGinnes `_, Added mute API endpoints, a couple of examples, random bits. +- `Davis Silverman `_, Fixed endpoint HTTP method +- `David Beitey `_, Fixed documentation typos +- `7kfpun `_, Fixed documentation typos +- `Will Thompson `_, Fixed documentation typos +- `Andre Polykanine `_, Fixed documentation typos +- `Ben Bertka `_, Added dynamic filtering in streamer +- `Takahashi Shuuji `_, Fixed documentation typos +- `Jeremy Keen `_, Updated documentation example diff --git a/HISTORY.rst b/HISTORY.rst index a7c961d..05f1f71 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,9 +3,12 @@ History ------- -3.3.0 (2014-xx-xx) +3.3.0 (2015-18-07) ++++++++++++++++++ - Added support for muting users +- Fix typos in documentation +- Updated documentation examples +- Added dynamic filtering to streamer 3.2.0 (2014-10-30) ++++++++++++++++++ diff --git a/docs/conf.py b/docs/conf.py index a0f9ef4..480a355 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.2.0' +version = '3.3.0' # The full version, including alpha/beta/rc tags. -release = '3.2.0' +release = '3.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index e203cc9..6d05baf 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.2.0' +__version__ = '3.3.0' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index a79667d..3525f3c 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.2.0' +__version__ = '3.3.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From a875f270a83dd9003b577f6d9922cf32835e1514 Mon Sep 17 00:00:00 2001 From: Ethan Baer Date: Fri, 31 Jul 2015 15:03:26 -0500 Subject: [PATCH 126/204] fixed cursor function code to return next pages when funciton iter_mode is 'id' --- twython/api.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/twython/api.py b/twython/api.py index d2d5c03..6046f13 100644 --- a/twython/api.py +++ b/twython/api.py @@ -18,6 +18,7 @@ from requests_oauthlib import OAuth1, OAuth2 from . import __version__ from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 +from .compat import urlsplit from .endpoints import EndpointsMixin from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .helpers import _transparent_params @@ -498,19 +499,27 @@ class Twython(EndpointsMixin, object): try: if function.iter_mode == 'id': - if 'max_id' not in params: - # Add 1 to the id because since_id and - # max_id are inclusive - if hasattr(function, 'iter_metadata'): - since_id = content[function.iter_metadata].get('since_id_str') + # Set max_id in params to one less than lowest tweet id + if hasattr(function, 'iter_metadata'): + # Get supplied next max_id + metadata = content.get(function.iter_metadata) + if 'next_results' in metadata: + next_results = urlsplit(metadata['next_results']) + params = dict(parse_qsl(next_results.query)) else: - since_id = content[0]['id_str'] - params['since_id'] = (int(since_id) - 1) + # No more results + raise StopIteration + else: + # Twitter gives tweets in reverse chronological order: + params['max_id'] = str(int(content[-1]['id_str']) - 1) elif function.iter_mode == 'cursor': params['cursor'] = content['next_cursor_str'] except (TypeError, ValueError): # pragma: no cover raise TwythonError('Unable to generate next page of search \ results, `page` is not a number.') + except (KeyError, AttributeError): #pragma no cover + raise TwythonError('Unable to generate next page of search \ + results, content has unexpected structure.') @staticmethod def unicode2utf8(text): -- 2.39.5 From 0b3df413d825af9026182a3fb96febde0a050a95 Mon Sep 17 00:00:00 2001 From: Ethan Baer Date: Fri, 31 Jul 2015 15:07:25 -0500 Subject: [PATCH 127/204] added urlsplit import for use in api.cursor function --- twython/compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/compat.py b/twython/compat.py index c36b326..7c049b0 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -25,7 +25,7 @@ except ImportError: if is_py2: from urllib import urlencode, quote_plus - from urlparse import parse_qsl + from urlparse import parse_qsl, urlsplit str = unicode basestring = basestring @@ -33,7 +33,7 @@ if is_py2: elif is_py3: - from urllib.parse import urlencode, quote_plus, parse_qsl + from urllib.parse import urlencode, quote_plus, parse_qsl, urlsplit str = str basestring = (str, bytes) -- 2.39.5 From 4a362a42aaf2eeee4a2ccac8524afc19c107db69 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Thu, 13 Aug 2015 21:52:53 -0400 Subject: [PATCH 128/204] Update Cory Dolphin's Github username in AUTHORS. Cory Dolphin changed Github usernames from "wcdolphin" to "corydolphin". Github does not automatically redirect profile links when you change usernames so his link in the AUTHORS file went to a 404. https://help.github.com/articles/what-happens-when-i-change-my-username/#changes-that-arent-automatic --- AUTHORS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 739e11f..6b7d14e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -44,7 +44,7 @@ Patches and Suggestions - `Oleg Anashkin `_, streaming ``handlers`` functionality - `Luis Alberto Santana `_, Fixed issue where Twython was unnecessarily disabling compression -- `Cory Dolphin `_, Added retry_after attribute to TwythonRateLimitError +- `Cory Dolphin `_, Added retry_after attribute to TwythonRateLimitError - `Natan L `_, Fixed typo in documentation - `Cash Costello `_, Moved tests to use `responsoes`, fixed typos in documentation - `Joe Cabrera `_, PEP 8 contributions -- 2.39.5 From 2ea45abb734af5a3de1e89a4ed2ea1a63e482160 Mon Sep 17 00:00:00 2001 From: ping Date: Sat, 15 Aug 2015 19:43:59 +0800 Subject: [PATCH 129/204] Fix html_for_tweet when a hashtag/mention is a substring of another --- twython/api.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/twython/api.py b/twython/api.py index d2d5c03..d83c2da 100644 --- a/twython/api.py +++ b/twython/api.py @@ -10,6 +10,7 @@ dealing with the Twitter API """ import warnings +import re import requests from requests.auth import HTTPBasicAuth @@ -550,19 +551,22 @@ class Twython(EndpointsMixin, object): entities = tweet['entities'] # Mentions - for entity in entities['user_mentions']: + for entity in sorted(entities['user_mentions'], + key=lambda mention: len(mention['screen_name']), reverse=True): start, end = entity['indices'][0], entity['indices'][1] mention_html = '@%(screen_name)s' - text = text.replace(tweet['text'][start:end], - mention_html % {'screen_name': entity['screen_name']}) + text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', + mention_html % {'screen_name': entity['screen_name']}, text) # Hashtags - for entity in entities['hashtags']: + for entity in sorted(entities['hashtags'], + key=lambda hashtag: len(hashtag['text']), reverse=True): start, end = entity['indices'][0], entity['indices'][1] hashtag_html = '#%(hashtag)s' - text = text.replace(tweet['text'][start:end], hashtag_html % {'hashtag': entity['text']}) + text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', + hashtag_html % {'hashtag': entity['text']}, text) # Urls for entity in entities['urls']: -- 2.39.5 From 62d51c74314b7c95976bd83602092408a07580e9 Mon Sep 17 00:00:00 2001 From: ping Date: Mon, 17 Aug 2015 14:22:32 +0800 Subject: [PATCH 130/204] Add support for quoted_status in html_for_tweet() --- twython/api.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index d2d5c03..68019f7 100644 --- a/twython/api.py +++ b/twython/api.py @@ -528,7 +528,7 @@ class Twython(EndpointsMixin, object): return str(text) @staticmethod - def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False): + def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False, expand_quoted_status=False): """Return HTML for a tweet (urls, mentions, hashtags replaced with links) :param tweet: Tweet object from received from Twitter API @@ -595,4 +595,16 @@ class Twython(EndpointsMixin, object): text = text.replace(tweet['text'][start:end], url_html % (entity['url'], shown_url)) + if expand_quoted_status and tweet['is_quote_status']: + quoted_status = tweet['quoted_status'] + text += '
%(quote)s' \ + '%(quote_user_name)s' \ + '@%(quote_user_screen_name)s' \ + '
' % \ + {'quote': Twython.html_for_tweet(quoted_status, use_display_url, use_expanded_url, False), + 'quote_tweet_link': 'https://twitter.com/%s/status/%s' % + (quoted_status['user']['screen_name'], quoted_status['id_str']), + 'quote_user_name': quoted_status['user']['name'], + 'quote_user_screen_name': quoted_status['user']['screen_name']} + return text -- 2.39.5 From 2cca89507992a760e65833ba88f0cec7be8d4cb8 Mon Sep 17 00:00:00 2001 From: Karambir Singh Nain Date: Wed, 23 Sep 2015 19:00:45 +0530 Subject: [PATCH 131/204] Add video upload endpoint --- docs/usage/advanced_usage.rst | 16 +++++++++ twython/api.py | 5 ++- twython/endpoints.py | 65 +++++++++++++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index d72b79f..fac35c2 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -35,6 +35,22 @@ Documentation: * https://dev.twitter.com/rest/reference/post/statuses/update * https://dev.twitter.com/rest/reference/post/media/upload +Updating Status with Video +-------------------------- + +This uploads a video as a media object and associates it with a status update. + +.. code-block:: python + + video = open('/path/to/file/video.mp4', 'rb') + response = twitter.upload_video(media=video, media_type='video/mp4') + twitter.update_status(status='Checkout this cool video!', media_ids=[response['media_id']]) + +Documentation: + +* https://dev.twitter.com/rest/reference/post/statuses/update +* https://dev.twitter.com/rest/reference/post/media/upload + Posting a Status with an Editing Image -------------------------------------- diff --git a/twython/api.py b/twython/api.py index 21b37da..3805e2f 100644 --- a/twython/api.py +++ b/twython/api.py @@ -194,7 +194,10 @@ class Twython(EndpointsMixin, object): retry_after=response.headers.get('X-Rate-Limit-Reset')) try: - content = response.json() + if response.status_code == 204: + content = response.content + else: + content = response.json() except ValueError: raise TwythonError('Response was not valid JSON. \ Unable to decode.') diff --git a/twython/endpoints.py b/twython/endpoints.py index 356cd8a..561ec45 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -14,7 +14,12 @@ This map is organized the order functions are documented at: https://dev.twitter.com/docs/api/1.1 """ +import os import warnings +try: + from StringIO import StringIO +except ImportError: + from io import StringIO from .advisory import TwythonDeprecationWarning @@ -139,6 +144,62 @@ class EndpointsMixin(object): """ return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) + def upload_video(self, media, media_type, size=None): + """Uploads video file to Twitter servers in chunks. The file will be available to be attached + to a status for 60 minutes. To attach to a update, pass a list of returned media ids + to the 'update_status' method using the 'media_ids' param. + + Upload happens in 3 stages: + - INIT call with size of media to be uploaded(in bytes). If this is more than 15mb, twitter will return error. + - APPEND calls each with media chunk. This returns a 204(No Content) if chunk is received. + - FINALIZE call to complete media upload. This returns media_id to be used with status update. + + Twitter media upload api expects each chunk to be not more than 5mb. We are sending chunk of 1mb each. + + Docs: + https://dev.twitter.com/rest/public/uploading-media#chunkedupload + """ + upload_url = 'https://upload.twitter.com/1.1/media/upload.json' + if not size: + media.seek(0, os.SEEK_END) + size = media.tell() + media.seek(0) + + # Stage 1: INIT call + params = { + 'command': 'INIT', + 'media_type': media_type, + 'total_bytes': size + } + response_init = self.post(upload_url, params=params) + media_id = response_init['media_id'] + + # Stage 2: APPEND calls with 1mb chunks + segment_index = 0 + while True: + data = media.read(1*1024*1024) + if not data: + break + media_chunk = StringIO() + media_chunk.write(data) + media_chunk.seek(0) + + params = { + 'command': 'APPEND', + 'media_id': media_id, + 'segment_index': segment_index, + 'media': media_chunk, + } + self.post(upload_url, params=params) + segment_index += 1 + + # Stage 3: FINALIZE call to complete upload + params = { + 'command': 'FINALIZE', + 'media_id': media_id + } + return self.post(upload_url, params=params) + def get_oembed_tweet(self, **params): """Returns information allowing the creation of an embedded representation of a Tweet on third party sites. @@ -546,7 +607,7 @@ class EndpointsMixin(object): list_mute_ids.iter_key = 'ids' def create_mute(self, **params): - """Mutes the specified user, preventing their tweets appearing + """Mutes the specified user, preventing their tweets appearing in the authenticating user's timeline. Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/create @@ -555,7 +616,7 @@ class EndpointsMixin(object): return self.post('mutes/users/create', params=params) def destroy_mute(self, **params): - """Un-mutes the user specified in the user or ID parameter for + """Un-mutes the user specified in the user or ID parameter for the authenticating user. Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/destroy -- 2.39.5 From ff4655c314277d9c2c18bda56f07ac03d6972d76 Mon Sep 17 00:00:00 2001 From: ping Date: Thu, 8 Oct 2015 11:31:07 +0800 Subject: [PATCH 132/204] Fix html_for_tweet for pre-quote implementation tweet html --- twython/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index 3805e2f..2ed50bc 100644 --- a/twython/api.py +++ b/twython/api.py @@ -597,7 +597,7 @@ class Twython(EndpointsMixin, object): text = text.replace(tweet['text'][start:end], url_html % (entity['url'], shown_url)) - if expand_quoted_status and tweet['is_quote_status']: + if expand_quoted_status and tweet.get('is_quote_status'): quoted_status = tweet['quoted_status'] text += '
%(quote)s' \ '%(quote_user_name)s' \ -- 2.39.5 From 99e9cebdc38c6c0271148a6da70a1d74214f0e77 Mon Sep 17 00:00:00 2001 From: ping Date: Tue, 29 Mar 2016 10:44:04 +0800 Subject: [PATCH 133/204] Fix issue where is_quote_status is true but there is no quoted_status --- twython/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index 2ed50bc..1d514eb 100644 --- a/twython/api.py +++ b/twython/api.py @@ -597,7 +597,7 @@ class Twython(EndpointsMixin, object): text = text.replace(tweet['text'][start:end], url_html % (entity['url'], shown_url)) - if expand_quoted_status and tweet.get('is_quote_status'): + if expand_quoted_status and tweet.get('is_quote_status') and tweet.get('quoted_status'): quoted_status = tweet['quoted_status'] text += '
%(quote)s' \ '%(quote_user_name)s' \ -- 2.39.5 From a1640f4a170ad49a88b1b29a46a9dad474c33326 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Fri, 22 Apr 2016 14:22:32 +0100 Subject: [PATCH 134/204] Link $IBM-style symbols in tweets in html_for_tweet() Fixes #412 --- docs/usage/special_functions.rst | 5 +++-- tests/config.py | 2 ++ tests/test_core.py | 9 ++++++++- twython/api.py | 11 ++++++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index c89da38..97e674c 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -72,11 +72,11 @@ The above code takes all the tweets from a specific users timeline, loops over t So: - http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ https://t.co/67pwRvY6z9 http://t.co/N6InAO4B71 + http://t.co/FCmXyI6VHd is #cool, lol! @mikehelmick shd #checkitout. Love, @__twython__ $IBM https://t.co/67pwRvY6z9 http://t.co/N6InAO4B71 will be replaced with: - google.com is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ github.com pic.twitter.com/N6InAO4B71 + google.com is #cool, lol! @mikehelmick shd #checkitout. Love, @__twython__ $IBM github.com pic.twitter.com/N6InAO4B71 .. note:: When converting the string to HTML we add a class to each HTML tag so that you can maninpulate the DOM later on. @@ -84,6 +84,7 @@ will be replaced with: - For media urls that are replaced we add ``class="twython-media"`` to the anchor tag - For user mentions that are replaced we add ``class="twython-mention"`` to the anchor tag - For hashtags that are replaced we add ``class="twython-hashtag"`` to the anchor tag +- For symbols that are replaced we add ``class="twython-symbol"`` to the anchor tag This function accepts two parameters: ``use_display_url`` and ``use_expanded_url`` By default, ``use_display_url`` is ``True``. Meaning the link displayed in the tweet text will appear as (ex. google.com, github.com) diff --git a/tests/config.py b/tests/config.py index 21baa69..d520a7c 100644 --- a/tests/config.py +++ b/tests/config.py @@ -28,3 +28,5 @@ test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', 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_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHER and $A.', u'contributors': None, u'geo': None, u'favorited': True, u'in_reply_to_user_id_str': None, u'user': {u'location': u'', u'id_str': u'2030131', u'protected': False, u'profile_background_tile': False, u'friends_count': 18, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'entities': {u'description': {u'urls': []}}, u'lang': u'en', u'listed_count': 5, u'default_profile_image': True, u'default_profile': False, u'statuses_count': 447, u'notifications': False, u'profile_background_color': u'9AE4E8', u'profile_sidebar_fill_color': u'E0FF92', u'profile_link_color': u'0000FF', u'profile_image_url_https': u'https://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png', u'followers_count': 8, u'geo_enabled': True, u'following': True, u'has_extended_profile': False, u'profile_use_background_image': True, u'profile_text_color': u'000000', u'screen_name': u'philgyfordtest', u'contributors_enabled': False, u'verified': False, u'name': u'Phil Gyford Test', u'profile_sidebar_border_color': u'000000', u'utc_offset': 0, u'profile_image_url': u'http://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png', u'id': 2030131, u'favourites_count': 0, u'time_zone': u'London', u'url': None, u'is_translation_enabled': False, u'is_translator': False, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'description': u'', u'created_at': u'Fri Mar 23 16:56:52 +0000 2007', u'follow_request_sent': False}, u'in_reply_to_user_id': None, u'retweeted': False, u'coordinates': None, u'place': None, u'in_reply_to_status_id': None, u'lang': u'en', u'in_reply_to_status_id_str': None, u'truncated': False, u'retweet_count': 0, u'is_quote_status': False, u'id': 662694880657989632, u'id_str': u'662694880657989632', u'in_reply_to_screen_name': None, u'favorite_count': 1, u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [{u'indices': [14, 19], u'text': u'AAPL'}, {u'indices': [24, 28], u'text': u'PEP'}, {u'indices': [46, 48], u'text': u'A'}], u'urls': []}, u'created_at': u'Fri Nov 06 18:15:46 +0000 2015', u'source': u'Tweetbot for Mac'} diff --git a/tests/test_core.py b/tests/test_core.py index 1aa0eea..a7c2758 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,7 @@ from twython import Twython, TwythonError, TwythonAuthError, TwythonRateLimitError from .config import ( - test_tweet_object, test_tweet_html, unittest + test_tweet_object, test_tweet_html, test_tweet_symbols_object, unittest ) import responses @@ -317,3 +317,10 @@ class TwythonAPITestCase(unittest.TestCase): # Make sure HTML doesn't contain the display OR expanded url self.assertTrue('http://google.com' not in tweet_text) self.assertTrue('google.com' not in tweet_text) + + def test_html_for_tweet_symbols(self): + tweet_text = self.api.html_for_tweet(test_tweet_symbols_object) + # Should only link symbols listed in entities: + self.assertTrue('$AAPL' in tweet_text) + self.assertTrue('$ANOTHER' not in tweet_text) + diff --git a/twython/api.py b/twython/api.py index 2ed50bc..fc93283 100644 --- a/twython/api.py +++ b/twython/api.py @@ -528,7 +528,7 @@ class Twython(EndpointsMixin, object): @staticmethod def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False, expand_quoted_status=False): - """Return HTML for a tweet (urls, mentions, hashtags replaced with links) + """Return HTML for a tweet (urls, mentions, hashtags, symbols replaced with links) :param tweet: Tweet object from received from Twitter API :param use_display_url: Use display URL to represent link @@ -566,6 +566,15 @@ class Twython(EndpointsMixin, object): text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', hashtag_html % {'hashtag': entity['text']}, text) + # Symbols + for entity in sorted(entities['symbols'], + key=lambda symbol: len(symbol['text']), reverse=True): + start, end = entity['indices'][0], entity['indices'][1] + + symbol_html = '$%(symbol)s' + text = re.sub(r'(?)' + re.escape(tweet['text'][start:end]) + '(?!)', + symbol_html % {'symbol': entity['text']}, text) + # Urls for entity in entities['urls']: start, end = entity['indices'][0], entity['indices'][1] -- 2.39.5 From 3f6700373f9b4bf6d0734912adf6386af4a22ff2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 30 Apr 2016 05:22:26 -0400 Subject: [PATCH 135/204] 3.4.0 release, update version and history --- HISTORY.rst | 6 ++++++ docs/conf.py | 4 ++-- setup.py | 2 +- twython/__init__.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 05f1f71..f8b0b31 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ History ------- +3.4.0 (2016-30-04) +++++++++++++++++++ +- Added `upload_video` endpoint +- Fix quoted status checks in `html_for_tweet` +- Fix `html_for_tweet` method response when hashtag/mention is a substring of another + 3.3.0 (2015-18-07) ++++++++++++++++++ - Added support for muting users diff --git a/docs/conf.py b/docs/conf.py index 480a355..23044e3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.3.0' +version = '3.4.0' # The full version, including alpha/beta/rc tags. -release = '3.3.0' +release = '3.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 6d05baf..7b38b99 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.3.0' +__version__ = '3.4.0' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index 3525f3c..1e57cee 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.3.0' +__version__ = '3.4.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 86f878aad86bd76b6d7a09cbc709572b7ce4d4c9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 30 Apr 2016 05:31:13 -0400 Subject: [PATCH 136/204] Fixes #417, update ReadTheDocs TLD in README --- README.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 6f12862..061d1b7 100644 --- a/README.rst +++ b/README.rst @@ -3,16 +3,16 @@ Twython .. image:: https://img.shields.io/pypi/v/twython.svg?style=flat-square - :target: https://pypi.python.org/pypi/twython + :target: https://pypi.python.org/pypi/twython .. image:: https://img.shields.io/pypi/dw/twython.svg?style=flat-square - :target: https://pypi.python.org/pypi/twython - + :target: https://pypi.python.org/pypi/twython + .. image:: https://img.shields.io/travis/ryanmcgrath/twython.svg?style=flat-square - :target: https://travis-ci.org/ryanmcgrath/twython + :target: https://travis-ci.org/ryanmcgrath/twython .. image:: https://img.shields.io/coveralls/ryanmcgrath/twython/master.svg?style=flat-square - :target: https://coveralls.io/r/ryanmcgrath/twython?branch=master + :target: https://coveralls.io/r/ryanmcgrath/twython?branch=master ``Twython`` is the premier Python library providing an easy (and up-to-date) way to access Twitter data. Actively maintained and featuring support for Python 2.6+ and Python 3. It's been battle tested by companies, educational institutions and individuals alike. Try it today! @@ -62,7 +62,7 @@ Or, if you want the code that is currently on GitHub Documentation ------------- -Documentation is available at https://twython.readthedocs.org/en/latest/ +Documentation is available at https://twython.readthedocs.io/en/latest/ Starting Out ------------ @@ -142,7 +142,7 @@ Once you have the final user tokens, store them in a database for later use:: OAUTH_TOKEN = final_step['oauth_token'] OAUTH_TOKEN_SECRET = final_step['oauth_token_secret'] -For OAuth 2 (Application Only, read-only) authentication, see `our documentation `_ +For OAuth 2 (Application Only, read-only) authentication, see `our documentation `_ Dynamic Function Arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -174,7 +174,7 @@ Documentation: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline Updating Status ~~~~~~~~~~~~~~~ -This method makes use of dynamic arguments, `read more about them `_ +This method makes use of dynamic arguments, `read more about them `_ Documentation: https://dev.twitter.com/docs/api/1/post/statuses/update @@ -195,8 +195,8 @@ Searching Advanced Usage -------------- -- `Advanced Twython Usage `_ -- `Streaming with Twython `_ +- `Advanced Twython Usage `_ +- `Streaming with Twython `_ Notes -- 2.39.5 From f91da7cadf9d3ddeefd7e01b99622ad84fd14e88 Mon Sep 17 00:00:00 2001 From: Gage Coprivnicar Date: Sun, 12 Jun 2016 16:42:25 -0600 Subject: [PATCH 137/204] added get_direct_message example added a get_direct_message example hope this is helpful. I spent a lot of time trying to figure it out and figured I could help others with the same problem --- get_direct_message example | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 get_direct_message example diff --git a/get_direct_message example b/get_direct_message example new file mode 100644 index 0000000..5a27a06 --- /dev/null +++ b/get_direct_message example @@ -0,0 +1,17 @@ +from twython import Twython, TwythonError +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + +get_list = twitter.get_direct_messages() +#Returns All Twitter DM information which is a lot in a list format + +dm_dict = get_list[0] +#Sets get_list to a dictionary, the number in the list is the direct message retrieved +#That means that 0 is the most recent and n-1 is the last DM revieved. +#You can cycle through all the numbers and it will return the text and the sender id of each + +print dm_dict['text'] +#Gets the text from the dictionary + +print dm_dict['sender']['id'] +#Gets the ID of the sender -- 2.39.5 From f7ddbcf414924ae31db4144bc5ffa16a000ca8ba Mon Sep 17 00:00:00 2001 From: ping Date: Wed, 22 Jun 2016 22:20:26 +0800 Subject: [PATCH 138/204] Use GET request if the media upload command is STATUS --- twython/endpoints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython/endpoints.py b/twython/endpoints.py index 561ec45..1131d83 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -142,6 +142,10 @@ class EndpointsMixin(object): Docs: https://dev.twitter.com/rest/reference/post/media/upload """ + # https://dev.twitter.com/rest/reference/get/media/upload-status + if params and params.get('command', '') == 'STATUS': + return self.get('https://upload.twitter.com/1.1/media/upload.json', params=params) + return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) def upload_video(self, media, media_type, size=None): -- 2.39.5 From 1b20f8f0f9eeeb398c89feb1787e541b21098741 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Wed, 17 Aug 2016 14:18:59 +0100 Subject: [PATCH 139/204] Fix upload_video "string argument detected, got 'bytes'" bug. Needed to use BytesIO instead of StringIO when uploading video to Twitter. Tested with python 2.6.9, 2.7.11, 3.3.6, 3.5.1. fixes #422 --- twython/endpoints.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 561ec45..9fe7734 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -16,10 +16,11 @@ https://dev.twitter.com/docs/api/1.1 import os import warnings -try: - from StringIO import StringIO -except ImportError: - from io import StringIO +from io import BytesIO +#try: + #from StringIO import StringIO +#except ImportError: + #from io import StringIO from .advisory import TwythonDeprecationWarning @@ -180,7 +181,7 @@ class EndpointsMixin(object): data = media.read(1*1024*1024) if not data: break - media_chunk = StringIO() + media_chunk = BytesIO() media_chunk.write(data) media_chunk.seek(0) -- 2.39.5 From 2c02b622a2cf8db5a2f060b3cb5672ff4e88d189 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Wed, 14 Sep 2016 15:35:51 -0700 Subject: [PATCH 140/204] added media_category and support for STATUS calls --- twython/endpoints.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 9fe7734..8814fee 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -199,7 +199,28 @@ class EndpointsMixin(object): 'command': 'FINALIZE', 'media_id': media_id } - return self.post(upload_url, params=params) + response_finalize = self.post(upload_url, params=params) + + # Stage 4: STATUS call if still processing + params = { + 'command': 'STATUS', + 'media_id': media_id + } + + processing_state = response_finalize['processing_info'].get('state', None) + + if processing_state is not None: + while (processing_state == "pending" or processing_state == "in_progress") : + # get the secs to wait + check_after_secs = response_finalize['processing_info'].get('check_after_secs', None) + + if check_after_secs is not None: + time.sleep(check_after_secs) + response_finalize = self.get(upload_url, params=params) + # get new state after waiting + processing_state = response_finalize['processing_info'].get('state') + + return response_finalize def get_oembed_tweet(self, **params): """Returns information allowing the creation of an embedded -- 2.39.5 From c57c4bfc3488c0905de49dbcfdd06a121d55f4b8 Mon Sep 17 00:00:00 2001 From: ping Date: Tue, 20 Sep 2016 17:18:21 +0800 Subject: [PATCH 141/204] Update html_for_tweet to support extended tweets --- tests/config.py | 4 ++++ tests/test_core.py | 15 +++++++++++- twython/api.py | 57 +++++++++++++++++++++++++++++++++------------- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/tests/config.py b/tests/config.py index d520a7c..607bdd8 100644 --- a/tests/config.py +++ b/tests/config.py @@ -30,3 +30,7 @@ test_tweet_object = {u'contributors': None, u'truncated': False, u'text': u'http test_tweet_html = 'google.com is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ github.com pic.twitter.com/N6InAO4B71' test_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHER and $A.', u'contributors': None, u'geo': None, u'favorited': True, u'in_reply_to_user_id_str': None, u'user': {u'location': u'', u'id_str': u'2030131', u'protected': False, u'profile_background_tile': False, u'friends_count': 18, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'entities': {u'description': {u'urls': []}}, u'lang': u'en', u'listed_count': 5, u'default_profile_image': True, u'default_profile': False, u'statuses_count': 447, u'notifications': False, u'profile_background_color': u'9AE4E8', u'profile_sidebar_fill_color': u'E0FF92', u'profile_link_color': u'0000FF', u'profile_image_url_https': u'https://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png', u'followers_count': 8, u'geo_enabled': True, u'following': True, u'has_extended_profile': False, u'profile_use_background_image': True, u'profile_text_color': u'000000', u'screen_name': u'philgyfordtest', u'contributors_enabled': False, u'verified': False, u'name': u'Phil Gyford Test', u'profile_sidebar_border_color': u'000000', u'utc_offset': 0, u'profile_image_url': u'http://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png', u'id': 2030131, u'favourites_count': 0, u'time_zone': u'London', u'url': None, u'is_translation_enabled': False, u'is_translator': False, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'description': u'', u'created_at': u'Fri Mar 23 16:56:52 +0000 2007', u'follow_request_sent': False}, u'in_reply_to_user_id': None, u'retweeted': False, u'coordinates': None, u'place': None, u'in_reply_to_status_id': None, u'lang': u'en', u'in_reply_to_status_id_str': None, u'truncated': False, u'retweet_count': 0, u'is_quote_status': False, u'id': 662694880657989632, u'id_str': u'662694880657989632', u'in_reply_to_screen_name': None, u'favorite_count': 1, u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [{u'indices': [14, 19], u'text': u'AAPL'}, {u'indices': [24, 28], u'text': u'PEP'}, {u'indices': [46, 48], u'text': u'A'}], u'urls': []}, u'created_at': u'Fri Nov 06 18:15:46 +0000 2015', u'source': u'Tweetbot for Mac'} + +test_tweet_compat_object = {u'contributors': None, u'truncated': True, u'text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/SRmsuks2ru', u'indices': [117, 140], u'expanded_url': u'https://twitter.com/i/web/status/777915304261193728', u'display_url': u'twitter.com/i/web/status/7\u2026'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None} +test_tweet_extended_object = {u'full_text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. https://t.co/I9pUC0NdZC", u'truncated': False, u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'contributors': None, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [], u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'id_str': u'777914712382058496', u'indices': [140, 163], u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'type': u'photo', u'id': 777914712382058496, u'display_url': u'pic.twitter.com/I9pUC0NdZC'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'display_text_range': [0, 139], u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None, u'extended_entities': {u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'display_url': u'pic.twitter.com/I9pUC0NdZC', u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'video_info': {u'aspect_ratio': [1, 1], u'variants': [{u'url': u'https://pbs.twimg.com/tweet_video/Csu1TzEVMAAAEv7.mp4', u'bitrate': 0, u'content_type': u'video/mp4'}]}, u'id_str': u'777914712382058496', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'indices': [140, 163], u'type': u'animated_gif', u'id': 777914712382058496, u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg'}]}} +test_tweet_extended_html = 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC' diff --git a/tests/test_core.py b/tests/test_core.py index a7c2758..c7cf2a2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,10 @@ +# -*- coding: utf-8 -*- from twython import Twython, TwythonError, TwythonAuthError, TwythonRateLimitError from .config import ( - test_tweet_object, test_tweet_html, test_tweet_symbols_object, unittest + test_tweet_object, test_tweet_html, test_tweet_symbols_object, + test_tweet_compat_object, test_tweet_extended_object, test_tweet_extended_html, + unittest ) import responses @@ -324,3 +327,13 @@ class TwythonAPITestCase(unittest.TestCase): self.assertTrue('$AAPL' in tweet_text) self.assertTrue('$ANOTHER' not in tweet_text) + def test_html_for_tweet_compatmode(self): + tweet_text = self.api.html_for_tweet(test_tweet_compat_object) + # link to compat web status link + self.assertTrue( + u'twitter.com/i/web/status/7…' in tweet_text) + + def test_html_for_tweet_extendedmode(self): + tweet_text = self.api.html_for_tweet(test_tweet_extended_object) + # full tweet rendered with suffix + self.assertEqual(test_tweet_extended_html, tweet_text) diff --git a/twython/api.py b/twython/api.py index ce11a68..f806cea 100644 --- a/twython/api.py +++ b/twython/api.py @@ -544,8 +544,18 @@ class Twython(EndpointsMixin, object): if 'retweeted_status' in tweet: tweet = tweet['retweeted_status'] + if 'extended_tweet' in tweet: + tweet = tweet['extended_tweet'] + + orig_tweet_text = tweet.get('full_text') or tweet['text'] + + display_text_range = tweet.get('display_text_range') or [0, len(orig_tweet_text)] + display_text_start, display_text_end = display_text_range[0], display_text_range[1] + display_text = orig_tweet_text[display_text_start:display_text_end] + prefix_text = orig_tweet_text[0:display_text_start] + suffix_text = orig_tweet_text[display_text_end:len(orig_tweet_text)] + if 'entities' in tweet: - text = tweet['text'] entities = tweet['entities'] # Mentions @@ -553,9 +563,13 @@ class Twython(EndpointsMixin, object): key=lambda mention: len(mention['screen_name']), reverse=True): start, end = entity['indices'][0], entity['indices'][1] - mention_html = '@%(screen_name)s' - text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', - mention_html % {'screen_name': entity['screen_name']}, text) + mention_html = '@%(screen_name)s' % {'screen_name': entity['screen_name']} + sub_expr = r'(?)' + orig_tweet_text[start:end] + '(?!)' + if display_text_start <= start <= display_text_end: + display_text = re.sub(sub_expr, mention_html, display_text) + else: + prefix_text = re.sub(sub_expr, mention_html, prefix_text) # Hashtags for entity in sorted(entities['hashtags'], @@ -563,8 +577,8 @@ class Twython(EndpointsMixin, object): start, end = entity['indices'][0], entity['indices'][1] hashtag_html = '#%(hashtag)s' - text = re.sub(r'(?)' + tweet['text'][start:end] + '(?!)', - hashtag_html % {'hashtag': entity['text']}, text) + display_text = re.sub(r'(?)' + orig_tweet_text[start:end] + '(?!)', + hashtag_html % {'hashtag': entity['text']}, display_text) # Symbols for entity in sorted(entities['symbols'], @@ -572,8 +586,8 @@ class Twython(EndpointsMixin, object): start, end = entity['indices'][0], entity['indices'][1] symbol_html = '$%(symbol)s' - text = re.sub(r'(?)' + re.escape(tweet['text'][start:end]) + '(?!)', - symbol_html % {'symbol': entity['text']}, text) + display_text = re.sub(r'(?)' + re.escape(orig_tweet_text[start:end]) + r'\b(?!)', + symbol_html % {'symbol': entity['text']}, display_text) # Urls for entity in entities['urls']: @@ -586,9 +600,11 @@ class Twython(EndpointsMixin, object): else: shown_url = entity['url'] - url_html = '%s' - text = text.replace(tweet['text'][start:end], - url_html % (entity['url'], shown_url)) + url_html = '%s' % (entity['url'], shown_url) + if display_text_start <= start <= display_text_end: + display_text = display_text.replace(orig_tweet_text[start:end], url_html) + else: + suffix_text = suffix_text.replace(orig_tweet_text[start:end], url_html) # Media if 'media' in entities: @@ -602,13 +618,17 @@ class Twython(EndpointsMixin, object): else: shown_url = entity['url'] - url_html = '%s' - text = text.replace(tweet['text'][start:end], - url_html % (entity['url'], shown_url)) + url_html = '%s' % (entity['url'], shown_url) + if display_text_start <= start <= display_text_end: + # for compatibility with pre-extended tweets + display_text = display_text.replace(orig_tweet_text[start:end], url_html) + else: + suffix_text = suffix_text.replace(orig_tweet_text[start:end], url_html) + quote_text = '' if expand_quoted_status and tweet.get('is_quote_status') and tweet.get('quoted_status'): quoted_status = tweet['quoted_status'] - text += '
%(quote)s' \ + quote_text += '
%(quote)s' \ '%(quote_user_name)s' \ '@%(quote_user_screen_name)s' \ '
' % \ @@ -618,4 +638,9 @@ class Twython(EndpointsMixin, object): 'quote_user_name': quoted_status['user']['name'], 'quote_user_screen_name': quoted_status['user']['screen_name']} - return text + return '%(prefix)s%(display)s%(suffix)s%(quote)s' % { + 'prefix': '%s' % prefix_text if prefix_text else '', + 'display': display_text, + 'suffix': '%s' % suffix_text if suffix_text else '', + 'quote': quote_text + } -- 2.39.5 From 7401adfb640e50021cab7db2041f13a21ce6bad2 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Sun, 25 Sep 2016 18:03:26 -0700 Subject: [PATCH 142/204] - Double -> single quotes - Removed default value from .get() - Added a check_progress param to upload_video to allow users to decide when to check 'STATUS' calls --- twython/endpoints.py | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 8814fee..33d4b00 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -145,7 +145,7 @@ class EndpointsMixin(object): """ return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) - def upload_video(self, media, media_type, size=None): + def upload_video(self, media, media_type, size=None, check_progress=False): """Uploads video file to Twitter servers in chunks. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids to the 'update_status' method using the 'media_ids' param. @@ -199,28 +199,33 @@ class EndpointsMixin(object): 'command': 'FINALIZE', 'media_id': media_id } - response_finalize = self.post(upload_url, params=params) - # Stage 4: STATUS call if still processing - params = { - 'command': 'STATUS', - 'media_id': media_id - } - - processing_state = response_finalize['processing_info'].get('state', None) + # Only get the status if explicity asked to + # Default to False + if check_progress == True: - if processing_state is not None: - while (processing_state == "pending" or processing_state == "in_progress") : - # get the secs to wait - check_after_secs = response_finalize['processing_info'].get('check_after_secs', None) + response_finalize = self.post(upload_url, params=params) - if check_after_secs is not None: - time.sleep(check_after_secs) - response_finalize = self.get(upload_url, params=params) - # get new state after waiting - processing_state = response_finalize['processing_info'].get('state') + # Stage 4: STATUS call if still processing + params = { + 'command': 'STATUS', + 'media_id': media_id + } + + processing_state = response_finalize['processing_info'].get('state') - return response_finalize + if processing_state is not None: + while (processing_state == 'pending' or processing_state == 'in_progress') : + # get the secs to wait + check_after_secs = response_finalize['processing_info'].get('check_after_secs', None) + + if check_after_secs is not None: + time.sleep(check_after_secs) + response_finalize = self.get(upload_url, params=params) + # get new state after waiting + processing_state = response_finalize['processing_info'].get('state') + + return self.post(upload_url, param=params) def get_oembed_tweet(self, **params): """Returns information allowing the creation of an embedded -- 2.39.5 From e76a290166884cebfffaca30bb847b00b1b2b29e Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Mon, 26 Sep 2016 12:51:05 -0700 Subject: [PATCH 143/204] fixed styling issues --- twython/endpoints.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 33d4b00..24ddcb9 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -200,11 +200,11 @@ class EndpointsMixin(object): 'media_id': media_id } + response = self.post(upload_url, params=params) + # Only get the status if explicity asked to # Default to False - if check_progress == True: - - response_finalize = self.post(upload_url, params=params) + if check_progress: # Stage 4: STATUS call if still processing params = { @@ -212,20 +212,20 @@ class EndpointsMixin(object): 'media_id': media_id } - processing_state = response_finalize['processing_info'].get('state') + processing_state = response['processing_info'].get('state') if processing_state is not None: while (processing_state == 'pending' or processing_state == 'in_progress') : # get the secs to wait - check_after_secs = response_finalize['processing_info'].get('check_after_secs', None) + check_after_secs = response['processing_info'].get('check_after_secs') if check_after_secs is not None: time.sleep(check_after_secs) - response_finalize = self.get(upload_url, params=params) + response = self.get(upload_url, params=params) # get new state after waiting - processing_state = response_finalize['processing_info'].get('state') + processing_state = response['processing_info'].get('state') - return self.post(upload_url, param=params) + return response def get_oembed_tweet(self, **params): """Returns information allowing the creation of an embedded -- 2.39.5 From 469432bcf84793db1137aaa6c5b3a4f70f386ad9 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Mon, 26 Sep 2016 14:22:19 -0700 Subject: [PATCH 144/204] - added support for media_category param - added code to handle empty responses for STATUS calls --- twython/endpoints.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 24ddcb9..8919a71 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -17,6 +17,7 @@ https://dev.twitter.com/docs/api/1.1 import os import warnings from io import BytesIO +from time import sleep #try: #from StringIO import StringIO #except ImportError: @@ -145,7 +146,7 @@ class EndpointsMixin(object): """ return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) - def upload_video(self, media, media_type, size=None, check_progress=False): + def upload_video(self, media, media_type, media_category=None, size=None, check_progress=False): """Uploads video file to Twitter servers in chunks. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids to the 'update_status' method using the 'media_ids' param. @@ -170,7 +171,8 @@ class EndpointsMixin(object): params = { 'command': 'INIT', 'media_type': media_type, - 'total_bytes': size + 'total_bytes': size, + 'media_category': media_category } response_init = self.post(upload_url, params=params) media_id = response_init['media_id'] @@ -211,19 +213,24 @@ class EndpointsMixin(object): 'command': 'STATUS', 'media_id': media_id } - - processing_state = response['processing_info'].get('state') - if processing_state is not None: + # added code to handle if media_category is NOT set and check_progress=True + # the API will return a NoneType object in this case + try: + processing_state = response.get('processing_info').get('state') + except AttributeError: + return response + + if processing_state: while (processing_state == 'pending' or processing_state == 'in_progress') : # get the secs to wait - check_after_secs = response['processing_info'].get('check_after_secs') + check_after_secs = response.get('processing_info').get('check_after_secs') - if check_after_secs is not None: - time.sleep(check_after_secs) + if check_after_secs: + sleep(check_after_secs) response = self.get(upload_url, params=params) # get new state after waiting - processing_state = response['processing_info'].get('state') + processing_state = response.get('processing_info').get('state') return response -- 2.39.5 From 574483d87021e8cdd0272e79f9654f1040634153 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 6 Jun 2017 10:17:58 -0400 Subject: [PATCH 145/204] 3.5.0 Release --- HISTORY.rst | 6 ++++++ docs/conf.py | 4 ++-- setup.py | 2 +- twython/__init__.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index f8b0b31..4b0f752 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ History ------- +3.5.0 (2017-06-06) +++++++++++++++++++ +- Added support for "symbols" in `Twython.html_for_tweet()` +- Added support for extended tweets in `Twython.html_for_tweet()` +- You can now check progress of video uploads to Twitter when using `Twython.upload_video()` + 3.4.0 (2016-30-04) ++++++++++++++++++ - Added `upload_video` endpoint diff --git a/docs/conf.py b/docs/conf.py index 23044e3..2a22092 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.4.0' +version = '3.5.0' # The full version, including alpha/beta/rc tags. -release = '3.4.0' +release = '3.5.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 7b38b99..b31d7af 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.4.0' +__version__ = '3.5.0' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index 1e57cee..7404b28 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.4.0' +__version__ = '3.5.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 5a87fc7d842994c48d32da10dc817fb1a9af16d7 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 16 Jun 2017 09:16:22 -0400 Subject: [PATCH 146/204] Fixes #446, add Python 3 classifier to setup.py --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b31d7af..1448fa8 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,12 @@ setup( 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Communications :: Chat', - 'Topic :: Internet' + 'Topic :: Internet', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', ] ) -- 2.39.5 From b366ab55c3412e78c8dcf49ab73f04937fd31e67 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 22 Aug 2017 13:49:40 +0100 Subject: [PATCH 147/204] Improve replacing of entities with links in html_for_tweet() I've re-written the parts of `html_for_tweet()` so that it handles all the replacements of URLs, mentions, symbols and hashtags better. Mainly to fix #447 but it should be a little more robust generally. Shamelessly cribbed from https://stackoverflow.com/a/25514650/250962 Passes all tests, but I haven't checked it beyond that. Fixes #447 --- tests/config.py | 3 ++ tests/test_core.py | 7 +++ twython/api.py | 118 +++++++++++++++++++++++++++------------------ 3 files changed, 80 insertions(+), 48 deletions(-) diff --git a/tests/config.py b/tests/config.py index 607bdd8..9e0aa15 100644 --- a/tests/config.py +++ b/tests/config.py @@ -34,3 +34,6 @@ test_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHE test_tweet_compat_object = {u'contributors': None, u'truncated': True, u'text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/SRmsuks2ru', u'indices': [117, 140], u'expanded_url': u'https://twitter.com/i/web/status/777915304261193728', u'display_url': u'twitter.com/i/web/status/7\u2026'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None} test_tweet_extended_object = {u'full_text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. https://t.co/I9pUC0NdZC", u'truncated': False, u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'contributors': None, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [], u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'id_str': u'777914712382058496', u'indices': [140, 163], u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'type': u'photo', u'id': 777914712382058496, u'display_url': u'pic.twitter.com/I9pUC0NdZC'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'display_text_range': [0, 139], u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None, u'extended_entities': {u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'display_url': u'pic.twitter.com/I9pUC0NdZC', u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'video_info': {u'aspect_ratio': [1, 1], u'variants': [{u'url': u'https://pbs.twimg.com/tweet_video/Csu1TzEVMAAAEv7.mp4', u'bitrate': 0, u'content_type': u'video/mp4'}]}, u'id_str': u'777914712382058496', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'indices': [140, 163], u'type': u'animated_gif', u'id': 777914712382058496, u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg'}]}} test_tweet_extended_html = 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC' + +test_tweet_identical_urls = {u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [], u'urls': [{u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [42, 65], u'url': u'https://t.co/W0uArTMk9N'}, {u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [101, 124], u'url': u'https://t.co/W0uArTMk9N'}]}, u'full_text': u'Use Cases, Trials and Making 5G a Reality https://t.co/W0uArTMk9N #5G #innovation via @5GWorldSeries https://t.co/W0uArTMk9N'} + diff --git a/tests/test_core.py b/tests/test_core.py index c7cf2a2..45d856e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4,6 +4,7 @@ from twython import Twython, TwythonError, TwythonAuthError, TwythonRateLimitErr from .config import ( test_tweet_object, test_tweet_html, test_tweet_symbols_object, test_tweet_compat_object, test_tweet_extended_object, test_tweet_extended_html, + test_tweet_identical_urls, unittest ) @@ -321,6 +322,12 @@ class TwythonAPITestCase(unittest.TestCase): self.assertTrue('http://google.com' not in tweet_text) self.assertTrue('google.com' not in tweet_text) + def test_html_for_tweet_identical_urls(self): + """If the 'url's for different url entities are identical, they should link correctly.""" + tweet_text = self.api.html_for_tweet(test_tweet_identical_urls) + self.assertEqual(tweet_text, + u'Use Cases, Trials and Making 5G a Reality buff.ly/2sEhrgO #5G #innovation via @5GWorldSeries buff.ly/2sEhrgO') + def test_html_for_tweet_symbols(self): tweet_text = self.api.html_for_tweet(test_tweet_symbols_object) # Should only link symbols listed in entities: diff --git a/twython/api.py b/twython/api.py index f806cea..954033f 100644 --- a/twython/api.py +++ b/twython/api.py @@ -556,62 +556,78 @@ class Twython(EndpointsMixin, object): suffix_text = orig_tweet_text[display_text_end:len(orig_tweet_text)] if 'entities' in tweet: - entities = tweet['entities'] + # We'll put all the bits of replacement HTML and their starts/ends + # in this list: + entities = [] # Mentions - for entity in sorted(entities['user_mentions'], - key=lambda mention: len(mention['screen_name']), reverse=True): - start, end = entity['indices'][0], entity['indices'][1] + if 'user_mentions' in tweet['entities']: + for entity in tweet['entities']['user_mentions']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - mention_html = '@%(screen_name)s' % {'screen_name': entity['screen_name']} - sub_expr = r'(?)' + orig_tweet_text[start:end] + '(?!)' - if display_text_start <= start <= display_text_end: - display_text = re.sub(sub_expr, mention_html, display_text) - else: - prefix_text = re.sub(sub_expr, mention_html, prefix_text) + mention_html = '@%(screen_name)s' % {'screen_name': entity['screen_name']} + + if display_text_start <= temp['start'] <= display_text_end: + temp['replacement'] = mention_html + entities.append(temp) + else: + prefix_text = re.sub(sub_expr, mention_html, prefix_text) # Hashtags - for entity in sorted(entities['hashtags'], - key=lambda hashtag: len(hashtag['text']), reverse=True): - start, end = entity['indices'][0], entity['indices'][1] + if 'hashtags' in tweet['entities']: + for entity in tweet['entities']['hashtags']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - hashtag_html = '#%(hashtag)s' - display_text = re.sub(r'(?)' + orig_tweet_text[start:end] + '(?!)', - hashtag_html % {'hashtag': entity['text']}, display_text) + url_html = '#%(hashtag)s' % {'hashtag': entity['text']} + + temp['replacement'] = url_html + entities.append(temp) # Symbols - for entity in sorted(entities['symbols'], - key=lambda symbol: len(symbol['text']), reverse=True): - start, end = entity['indices'][0], entity['indices'][1] + if 'symbols' in tweet['entities']: + for entity in tweet['entities']['symbols']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - symbol_html = '$%(symbol)s' - display_text = re.sub(r'(?)' + re.escape(orig_tweet_text[start:end]) + r'\b(?!)', - symbol_html % {'symbol': entity['text']}, display_text) + url_html = '$%(symbol)s' % {'symbol': entity['text']} - # Urls - for entity in entities['urls']: - start, end = entity['indices'][0], entity['indices'][1] - if use_display_url and entity.get('display_url') \ - and not use_expanded_url: - shown_url = entity['display_url'] - elif use_expanded_url and entity.get('expanded_url'): - shown_url = entity['expanded_url'] - else: - shown_url = entity['url'] + temp['replacement'] = url_html + entities.append(temp) - url_html = '%s' % (entity['url'], shown_url) - if display_text_start <= start <= display_text_end: - display_text = display_text.replace(orig_tweet_text[start:end], url_html) - else: - suffix_text = suffix_text.replace(orig_tweet_text[start:end], url_html) + # URLs + if 'urls' in tweet['entities']: + for entity in tweet['entities']['urls']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - # Media - if 'media' in entities: - for entity in entities['media']: - start, end = entity['indices'][0], entity['indices'][1] - if use_display_url and entity.get('display_url') \ - and not use_expanded_url: + if use_display_url and entity.get('display_url') and not use_expanded_url: + shown_url = entity['display_url'] + elif use_expanded_url and entity.get('expanded_url'): + shown_url = entity['expanded_url'] + else: + shown_url = entity['url'] + + url_html = '%s' % (entity['url'], shown_url) + + if display_text_start <= temp['start'] <= display_text_end: + temp['replacement'] = url_html + entities.append(temp) + else: + suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) + + if 'media' in tweet['entities']: + for entity in tweet['entities']['media']: + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] + + if use_display_url and entity.get('display_url') and not use_expanded_url: shown_url = entity['display_url'] elif use_expanded_url and entity.get('expanded_url'): shown_url = entity['expanded_url'] @@ -619,11 +635,17 @@ class Twython(EndpointsMixin, object): shown_url = entity['url'] url_html = '%s' % (entity['url'], shown_url) - if display_text_start <= start <= display_text_end: - # for compatibility with pre-extended tweets - display_text = display_text.replace(orig_tweet_text[start:end], url_html) + + if display_text_start <= temp['start'] <= display_text_end: + temp['replacement'] = url_html + entities.append(temp) else: - suffix_text = suffix_text.replace(orig_tweet_text[start:end], url_html) + suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) + + # Now do all the replacements, starting from the end, so that the + # start/end indices still work: + for entity in sorted(entities, key=lambda e: e['start'], reverse=True): + display_text = display_text[0:entity['start']] + entity['replacement'] + display_text[entity['end']:] quote_text = '' if expand_quoted_status and tweet.get('is_quote_status') and tweet.get('quoted_status'): -- 2.39.5 From 6890802b2ae528557fdb1a528b4efd17470b5702 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 22 Aug 2017 13:55:22 +0100 Subject: [PATCH 148/204] Add test for missing `symbols` in entities If a tweet has no `symbols` in its `entities` then `html_for_tweet()` was failing. I'm not sure how common this is but, for example, tweets in a downloaded archive do not have `symbols` for some reason. The previous change (b366ab5) fixed this, but I'm adding a test for this case. --- tests/test_core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 45d856e..2a30df5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,18 @@ class TwythonAPITestCase(unittest.TestCase): self.assertTrue('$AAPL' in tweet_text) self.assertTrue('$ANOTHER' not in tweet_text) + def test_html_for_tweet_no_symbols(self): + """Should still work if tweet object has no symbols list""" + tweet = test_tweet_symbols_object + # Save a copy: + symbols = tweet['entities']['symbols'] + del tweet['entities']['symbols'] + tweet_text = self.api.html_for_tweet(tweet) + self.assertTrue('symbols: $AAPL and' in tweet_text) + self.assertTrue('and $ANOTHER and $A.' in tweet_text) + # Put the symbols back: + test_tweet_symbols_object['entities']['symbols'] = symbols + def test_html_for_tweet_compatmode(self): tweet_text = self.api.html_for_tweet(test_tweet_compat_object) # link to compat web status link -- 2.39.5 From ede941cf1a259f927861d988bd4873f5cd4e5d65 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 23 Aug 2017 11:29:20 -0400 Subject: [PATCH 149/204] Version 3.6.0 and update travis python versions --- .travis.yml | 5 +++-- HISTORY.rst | 5 +++++ docs/conf.py | 4 ++-- setup.py | 5 ++--- twython/__init__.py | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91ebf1d..8da6c43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ language: python python: - 2.6 - 2.7 - - 3.3 + - 3.5 + - 3.6 env: global: - secure: USjLDneiXlVvEjkUVqTt+LBi0XJ4QhkRcJzqVXA9gEau1NTjAkNTPmHjUbOygp0dkfoV0uWrZKCw6fL1g+HJgWl0vHeHzcNl4mUkA+OwkGFHgaeIhvUfnyyJA8P3Zm21XHC+ehzMpEFN5fVNNhREjnRj+CXMc0FgA6knwBRobu4= @@ -17,7 +18,7 @@ env: - TEST_LIST_SLUG=team - TEST_LIST_OWNER_SCREEN_NAME=twitterapi - ACCESS_TOKEN_B64=U2FsdGVkX18QdBhvMNshM4PGy04tU3HLwKP+nNSoNZHKsvGLjELcWEXN2LIu/T+yngX1vGONf9lo14ElnfS4k7sfhiru8phR4+rZuBVP3bDvC2A6fXJuhuLqNhBrWqg32WQewvxLWDWBoKmnvRHg5b74GHh+IN/12tU0cBF2HK8= -install: +install: - pip install -r requirements.txt - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing diff --git a/HISTORY.rst b/HISTORY.rst index 4b0f752..4249539 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ History ------- +3.6.0 (2017-23-08) +++++++++++++++++++ +- Improve replacing of entities with links in `html_for_tweet()` +- Update classifiers for PyPI + 3.5.0 (2017-06-06) ++++++++++++++++++ - Added support for "symbols" in `Twython.html_for_tweet()` diff --git a/docs/conf.py b/docs/conf.py index 2a22092..601d066 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.5.0' +version = '3.6.0' # The full version, including alpha/beta/rc tags. -release = '3.5.0' +release = '3.6.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 1448fa8..40b05b7 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.5.0' +__version__ = '3.6.0' packages = [ 'twython', @@ -45,8 +45,7 @@ setup( 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ] ) diff --git a/twython/__init__.py b/twython/__init__.py index 7404b28..84e9ee6 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.5.0' +__version__ = '3.6.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From c63ed8559e04b098f965aa94ad5f3644ffca5fad Mon Sep 17 00:00:00 2001 From: Clayton A Davis Date: Wed, 6 Sep 2017 18:30:32 -0400 Subject: [PATCH 150/204] Improve error handling for api.cursor --- twython/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/twython/api.py b/twython/api.py index 954033f..5f0c50d 100644 --- a/twython/api.py +++ b/twython/api.py @@ -470,6 +470,11 @@ class Twython(EndpointsMixin, object): >>> print result """ + if not callable(function): + raise TypeError('.cursor() takes a Twython function as its first \ + argument. Did you provide the result of a \ + function call?') + if not hasattr(function, 'iter_mode'): raise TwythonError('Unable to create generator for Twython \ method "%s"' % function.__name__) -- 2.39.5 From 6166e86807fdf474c6521ed3b818d2a28cbda687 Mon Sep 17 00:00:00 2001 From: Clayton A Davis Date: Mon, 11 Sep 2017 13:52:43 -0400 Subject: [PATCH 151/204] Add test for cursor creation --- tests/test_core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 2a30df5..44adf95 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -356,3 +356,15 @@ class TwythonAPITestCase(unittest.TestCase): tweet_text = self.api.html_for_tweet(test_tweet_extended_object) # full tweet rendered with suffix self.assertEqual(test_tweet_extended_html, tweet_text) + + def test_cursor_requires_twython_function(self): + """Test that cursor() raises when called without a Twython function""" + def init_and_iterate_cursor(*args, **kwargs): + cursor = self.api.cursor(*args, **kwargs) + return next(cursor) + + non_function = object() + non_twython_function = lambda x: x + + self.assertRaises(TypeError, init_and_iterate_cursor, non_function) + self.assertRaises(TwythonError, init_and_iterate_cursor, non_twython_function) -- 2.39.5 From 0ee9b76b5cb6fd86ce8b75287ca33579fd1d18b5 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 3 Oct 2017 19:12:02 +0100 Subject: [PATCH 152/204] Make html_for_tweet() link a replied-to username If a tweet was a reply, then when `html_for_tweet()` tried to turn the initial "@username" into a link, there was: > NameError: name 'sub_expr' is not defined This is now fixed, with a test to ensure the "@username" becomes a link. --- tests/config.py | 2 ++ tests/test_core.py | 10 ++++++++-- twython/api.py | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/config.py b/tests/config.py index 9e0aa15..c3ea809 100644 --- a/tests/config.py +++ b/tests/config.py @@ -37,3 +37,5 @@ test_tweet_extended_html = 'Say more about what\'s happening! Rolling out now: p test_tweet_identical_urls = {u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [], u'urls': [{u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [42, 65], u'url': u'https://t.co/W0uArTMk9N'}, {u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [101, 124], u'url': u'https://t.co/W0uArTMk9N'}]}, u'full_text': u'Use Cases, Trials and Making 5G a Reality https://t.co/W0uArTMk9N #5G #innovation via @5GWorldSeries https://t.co/W0uArTMk9N'} +test_tweet_reply = { u'display_text_range': [12,114], u'in_reply_to_status_id_str':u'742374355531923456', u'source':u'web', u'geo':None, u'full_text':u'@philgyford Here\u2019s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr', u'extended_entities':{ u'media':[] }, u'id_str':u'300', u'in_reply_to_status_id':742374355531923456, u'id':300, u'in_reply_to_screen_name':u'philgyford', u'retweet_count':0, u'user':{ }, u'created_at':u'Mon Jun 13 15:48:06 +0000 2016', u'lang':u'en', u'favorite_count':0, u'coordinates':None, u'place':None, u'contributors':None, u'in_reply_to_user_id':12552, u'in_reply_to_user_id_str':u'12552', u'retweeted':False, u'favorited':False, u'truncated':False, u'entities':{ u'user_mentions':[ { u'id_str':u'12552', u'id':12552, u'screen_name':u'philgyford', u'name':u'Phil Gyford', u'indices':[ 0, 11 ] } ], u'media':[ ], u'hashtags':[ ], u'symbols':[ ], u'urls':[ ] }, u'is_quote_status':False, u'possibly_sensitive':False } + diff --git a/tests/test_core.py b/tests/test_core.py index 44adf95..b824f86 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,8 +3,8 @@ from twython import Twython, TwythonError, TwythonAuthError, TwythonRateLimitErr from .config import ( test_tweet_object, test_tweet_html, test_tweet_symbols_object, - test_tweet_compat_object, test_tweet_extended_object, test_tweet_extended_html, - test_tweet_identical_urls, + test_tweet_compat_object, test_tweet_extended_object, + test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, unittest ) @@ -308,6 +308,12 @@ class TwythonAPITestCase(unittest.TestCase): tweet_text = self.api.html_for_tweet(test_tweet_object) self.assertEqual(test_tweet_html, tweet_text) + def test_html_for_tweet_reply(self): + """Test HTML for Tweet links the replied-to username.""" + tweet_text = self.api.html_for_tweet(test_tweet_reply) + self.assertEqual(tweet_text, + u'@philgyford Here’s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr') + def test_html_for_tweet_expanded_url(self): """Test using expanded url in HTML for Tweet displays full urls""" tweet_text = self.api.html_for_tweet(test_tweet_object, diff --git a/twython/api.py b/twython/api.py index 5f0c50d..2782168 100644 --- a/twython/api.py +++ b/twython/api.py @@ -578,6 +578,9 @@ class Twython(EndpointsMixin, object): temp['replacement'] = mention_html entities.append(temp) else: + # Make the '@username' at the start, before + # display_text, into a link: + sub_expr = r'(?)' + orig_tweet_text[temp['start']:temp['end']] + '(?!)' prefix_text = re.sub(sub_expr, mention_html, prefix_text) # Hashtags -- 2.39.5 From 4f1e41a9e5e3cfc7814c6912f4213161e20c8d84 Mon Sep 17 00:00:00 2001 From: Clayton Davis Date: Fri, 6 Oct 2017 14:45:25 -0400 Subject: [PATCH 153/204] Update print statements to print() functions --- docs/usage/advanced_usage.rst | 2 +- docs/usage/special_functions.rst | 8 ++++---- docs/usage/streaming_api.rst | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index fac35c2..ce609a6 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -98,7 +98,7 @@ That being said, Twython offers a generator for search results and can be access results = twitter.cursor(twitter.search, q='python') for result in results: - print result + print(result) Manipulate the Request (headers, proxies, etc.) ----------------------------------------------- diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index 97e674c..cde8f99 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -25,7 +25,7 @@ The Old Way results = twitter.search(q='twitter') if results.get('statuses'): for result in results['statuses']: - print result['id_str'] + print(result['id_str']) The New Way ^^^^^^^^^^^ @@ -39,7 +39,7 @@ The New Way results = twitter.cursor(twitter.search, q='twitter') for result in results: - print result['id_str'] + print(result['id_str']) Another example: @@ -47,7 +47,7 @@ Another example: results = twitter.cursor(t.get_mentions_timeline) for result in results: - print result['id_str'] + print(result['id_str']) HTML for Tweet @@ -66,7 +66,7 @@ This function takes a tweet object received from the Twitter API and returns an include_rts=True) for tweet in user_tweets: tweet['text'] = Twython.html_for_tweet(tweet) - print tweet['text'] + print(tweet['text']) The above code takes all the tweets from a specific users timeline, loops over them and replaces the value of ``tweet['text']`` with HTML. diff --git a/docs/usage/streaming_api.rst b/docs/usage/streaming_api.rst index 8b21c70..4d6c1c3 100644 --- a/docs/usage/streaming_api.rst +++ b/docs/usage/streaming_api.rst @@ -27,10 +27,10 @@ Now set up how you want to handle the signals. class MyStreamer(TwythonStreamer): def on_success(self, data): if 'text' in data: - print data['text'].encode('utf-8') + print(data['text']) def on_error(self, status_code, data): - print status_code + print(status_code) # Want to stop trying to get data because of the error? # Uncomment the next line! -- 2.39.5 From c086449818d53ed1f6e0eb3d6ef96946de2e6282 Mon Sep 17 00:00:00 2001 From: Clayton Davis Date: Fri, 6 Oct 2017 14:57:36 -0400 Subject: [PATCH 154/204] Update StringIO import for py3k --- docs/usage/advanced_usage.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index ce609a6..32334c0 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -62,7 +62,12 @@ with a status update. # Assume you are working with a JPEG from PIL import Image - from StringIO import StringIO + try: + # Python 3 + from io import StringIO + except ImportError: + # Python 2 + from StringIO import StringIO photo = Image.open('/path/to/file/image.jpg') -- 2.39.5 From e87b80710db0f74af8508353611e05608cd1cd19 Mon Sep 17 00:00:00 2001 From: FoxMaSk Date: Sat, 7 Oct 2017 16:46:56 +0200 Subject: [PATCH 155/204] update the link to the doc of the twitter API --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 1aa13e0..05d2838 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Features - Twitter lists - Timelines - Direct Messages - - and anything found in `the Twitter API docs `_. + - and anything found in `the Twitter API docs `_. - Image Uploading: - Update user status with an image - Change user avatar -- 2.39.5 From 748d28cc71f05a67407e36aa31c5de429d8ce8db Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Sat, 7 Oct 2017 18:26:21 +0200 Subject: [PATCH 156/204] twython/api.py: JSON error is not raised if the response content is empty and the status code is not 204. If params is not a dictionary, _transparent_params is not called --- twython/api.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/twython/api.py b/twython/api.py index 2782168..577d6a6 100644 --- a/twython/api.py +++ b/twython/api.py @@ -140,7 +140,11 @@ class Twython(EndpointsMixin, object): params = params or {} func = getattr(self.client, method) - params, files = _transparent_params(params) + if type(params) is dict: + params, files = _transparent_params(params) + else: + params = params + files = list() requests_args = {} for k, v in self.client_args.items(): @@ -192,15 +196,16 @@ class Twython(EndpointsMixin, object): error_message, error_code=response.status_code, retry_after=response.headers.get('X-Rate-Limit-Reset')) - + content="" try: if response.status_code == 204: content = response.content else: content = response.json() except ValueError: - raise TwythonError('Response was not valid JSON. \ - Unable to decode.') + if response.content!="": + raise TwythonError('Response was not valid JSON. \ + Unable to decode.') return content -- 2.39.5 From 6fc7b9e0386110eb05ed897d561004115407c508 Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Sat, 7 Oct 2017 18:29:19 +0200 Subject: [PATCH 157/204] Added create_metadata endpoint --- twython/endpoints.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/twython/endpoints.py b/twython/endpoints.py index 95e33d2..1c1d80c 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -14,6 +14,7 @@ This map is organized the order functions are documented at: https://dev.twitter.com/docs/api/1.1 """ +import json import os import warnings from io import BytesIO @@ -150,6 +151,13 @@ class EndpointsMixin(object): return self.post('https://upload.twitter.com/1.1/media/upload.json', params=params) + def create_metadata(self, **params): + """ Adds metadata to a media element, such as image descriptions for visually impaired. + Docs: https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create + """ + params = json.dumps(params) + return self.post("https://upload.twitter.com/1.1/media/metadata/create.json", params=params) + def upload_video(self, media, media_type, media_category=None, size=None, check_progress=False): """Uploads video file to Twitter servers in chunks. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids -- 2.39.5 From 1511ee7b4d983c27292f9cb6060c011366cc7ac1 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Sat, 7 Oct 2017 18:08:01 +0100 Subject: [PATCH 158/204] Split test_html_for_tweet() tests into their own file --- tests/test_core.py | 68 +-------------------------------- tests/test_html_for_tweet.py | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 66 deletions(-) create mode 100644 tests/test_html_for_tweet.py diff --git a/tests/test_core.py b/tests/test_core.py index b824f86..4c215d4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- from twython import Twython, TwythonError, TwythonAuthError, TwythonRateLimitError -from .config import ( - test_tweet_object, test_tweet_html, test_tweet_symbols_object, - test_tweet_compat_object, test_tweet_extended_object, - test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, - unittest -) +from .config import unittest import responses import requests @@ -303,66 +298,6 @@ class TwythonAPITestCase(unittest.TestCase): """Test encoding UTF-8 works""" self.api.encode('Twython is awesome!') - def test_html_for_tweet(self): - """Test HTML for Tweet returns what we want""" - tweet_text = self.api.html_for_tweet(test_tweet_object) - self.assertEqual(test_tweet_html, tweet_text) - - def test_html_for_tweet_reply(self): - """Test HTML for Tweet links the replied-to username.""" - tweet_text = self.api.html_for_tweet(test_tweet_reply) - self.assertEqual(tweet_text, - u'@philgyford Here’s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr') - - def test_html_for_tweet_expanded_url(self): - """Test using expanded url in HTML for Tweet displays full urls""" - tweet_text = self.api.html_for_tweet(test_tweet_object, - use_expanded_url=True) - # Make sure full url is in HTML - self.assertTrue('http://google.com' in tweet_text) - - def test_html_for_tweet_short_url(self): - """Test using expanded url in HTML for Tweet displays full urls""" - tweet_text = self.api.html_for_tweet(test_tweet_object, False) - # Make sure HTML doesn't contain the display OR expanded url - self.assertTrue('http://google.com' not in tweet_text) - self.assertTrue('google.com' not in tweet_text) - - def test_html_for_tweet_identical_urls(self): - """If the 'url's for different url entities are identical, they should link correctly.""" - tweet_text = self.api.html_for_tweet(test_tweet_identical_urls) - self.assertEqual(tweet_text, - u'Use Cases, Trials and Making 5G a Reality buff.ly/2sEhrgO #5G #innovation via @5GWorldSeries buff.ly/2sEhrgO') - - def test_html_for_tweet_symbols(self): - tweet_text = self.api.html_for_tweet(test_tweet_symbols_object) - # Should only link symbols listed in entities: - self.assertTrue('$AAPL' in tweet_text) - self.assertTrue('$ANOTHER' not in tweet_text) - - def test_html_for_tweet_no_symbols(self): - """Should still work if tweet object has no symbols list""" - tweet = test_tweet_symbols_object - # Save a copy: - symbols = tweet['entities']['symbols'] - del tweet['entities']['symbols'] - tweet_text = self.api.html_for_tweet(tweet) - self.assertTrue('symbols: $AAPL and' in tweet_text) - self.assertTrue('and $ANOTHER and $A.' in tweet_text) - # Put the symbols back: - test_tweet_symbols_object['entities']['symbols'] = symbols - - def test_html_for_tweet_compatmode(self): - tweet_text = self.api.html_for_tweet(test_tweet_compat_object) - # link to compat web status link - self.assertTrue( - u'twitter.com/i/web/status/7…' in tweet_text) - - def test_html_for_tweet_extendedmode(self): - tweet_text = self.api.html_for_tweet(test_tweet_extended_object) - # full tweet rendered with suffix - self.assertEqual(test_tweet_extended_html, tweet_text) - def test_cursor_requires_twython_function(self): """Test that cursor() raises when called without a Twython function""" def init_and_iterate_cursor(*args, **kwargs): @@ -374,3 +309,4 @@ class TwythonAPITestCase(unittest.TestCase): self.assertRaises(TypeError, init_and_iterate_cursor, non_function) self.assertRaises(TwythonError, init_and_iterate_cursor, non_twython_function) + diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py new file mode 100644 index 0000000..b8b5dc4 --- /dev/null +++ b/tests/test_html_for_tweet.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from twython import Twython, TwythonError + +from .config import ( + test_tweet_object, test_tweet_html, test_tweet_symbols_object, + test_tweet_compat_object, test_tweet_extended_object, + test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, + unittest +) + + +class TestHtmlForTweetTestCase(unittest.TestCase): + def setUp(self): + self.api = Twython('', '', '', '') + + def test_basic(self): + """Test HTML for Tweet returns what we want""" + tweet_text = self.api.html_for_tweet(test_tweet_object) + self.assertEqual(test_tweet_html, tweet_text) + + def test_reply(self): + """Test HTML for Tweet links the replied-to username.""" + tweet_text = self.api.html_for_tweet(test_tweet_reply) + self.assertEqual(tweet_text, + u'@philgyford Here’s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr') + + def test_expanded_url(self): + """Test using expanded url in HTML for Tweet displays full urls""" + tweet_text = self.api.html_for_tweet(test_tweet_object, + use_expanded_url=True) + # Make sure full url is in HTML + self.assertTrue('http://google.com' in tweet_text) + + def test_short_url(self): + """Test using expanded url in HTML for Tweet displays full urls""" + tweet_text = self.api.html_for_tweet(test_tweet_object, False) + # Make sure HTML doesn't contain the display OR expanded url + self.assertTrue('http://google.com' not in tweet_text) + self.assertTrue('google.com' not in tweet_text) + + def test_identical_urls(self): + """If the 'url's for different url entities are identical, they should link correctly.""" + tweet_text = self.api.html_for_tweet(test_tweet_identical_urls) + self.assertEqual(tweet_text, + u'Use Cases, Trials and Making 5G a Reality buff.ly/2sEhrgO #5G #innovation via @5GWorldSeries buff.ly/2sEhrgO') + + def test_symbols(self): + tweet_text = self.api.html_for_tweet(test_tweet_symbols_object) + # Should only link symbols listed in entities: + self.assertTrue('$AAPL' in tweet_text) + self.assertTrue('$ANOTHER' not in tweet_text) + + def test_no_symbols(self): + """Should still work if tweet object has no symbols list""" + tweet = test_tweet_symbols_object + # Save a copy: + symbols = tweet['entities']['symbols'] + del tweet['entities']['symbols'] + tweet_text = self.api.html_for_tweet(tweet) + self.assertTrue('symbols: $AAPL and' in tweet_text) + self.assertTrue('and $ANOTHER and $A.' in tweet_text) + # Put the symbols back: + test_tweet_symbols_object['entities']['symbols'] = symbols + + def test_compatmode(self): + tweet_text = self.api.html_for_tweet(test_tweet_compat_object) + # link to compat web status link + self.assertTrue( + u'twitter.com/i/web/status/7…' in tweet_text) + + def test_extendedmode(self): + tweet_text = self.api.html_for_tweet(test_tweet_extended_object) + # full tweet rendered with suffix + self.assertEqual(test_tweet_extended_html, tweet_text) -- 2.39.5 From a27efd9da8b6eb7350d650c0fc14aca80c32b466 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Sat, 7 Oct 2017 18:38:20 +0100 Subject: [PATCH 159/204] Fix html_for_tweet()s handling of media URLs We were trying to link to each media item using its `url`/`expanded_url`. But there is only one of these, shared across all of a tweet's media items. So attempting to put it in several times, in the same location, was a bit of a mess! So it now only puts the `url`/`expanded_url` in once, no matter how many media items there are. --- tests/config.py | 1 + tests/test_html_for_tweet.py | 9 +++ tweet.json | 142 +++++++++++++++++++++++++++++++++++ twython/api.py | 38 +++++----- 4 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 tweet.json diff --git a/tests/config.py b/tests/config.py index c3ea809..07ca96d 100644 --- a/tests/config.py +++ b/tests/config.py @@ -39,3 +39,4 @@ test_tweet_identical_urls = {u'entities': {u'hashtags': [], u'user_mentions': [] test_tweet_reply = { u'display_text_range': [12,114], u'in_reply_to_status_id_str':u'742374355531923456', u'source':u'web', u'geo':None, u'full_text':u'@philgyford Here\u2019s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr', u'extended_entities':{ u'media':[] }, u'id_str':u'300', u'in_reply_to_status_id':742374355531923456, u'id':300, u'in_reply_to_screen_name':u'philgyford', u'retweet_count':0, u'user':{ }, u'created_at':u'Mon Jun 13 15:48:06 +0000 2016', u'lang':u'en', u'favorite_count':0, u'coordinates':None, u'place':None, u'contributors':None, u'in_reply_to_user_id':12552, u'in_reply_to_user_id_str':u'12552', u'retweeted':False, u'favorited':False, u'truncated':False, u'entities':{ u'user_mentions':[ { u'id_str':u'12552', u'id':12552, u'screen_name':u'philgyford', u'name':u'Phil Gyford', u'indices':[ 0, 11 ] } ], u'media':[ ], u'hashtags':[ ], u'symbols':[ ], u'urls':[ ] }, u'is_quote_status':False, u'possibly_sensitive':False } +test_tweet_media = {'user': {'name': 'Phil Gyford', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', 'verified': False, 'screen_name': 'philgyford', 'id': 12552, 'id_str': '12552', 'protected': False}, 'geo': {}, 'id': 905105588279013377, 'text': 'I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg', 'entities': {'user_mentions': [], 'hashtags': [], 'media': [{'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'id': 905105571765944320, 'media_url': 'http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'sizes': [{'h': 256, 'w': 680, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105571765944320'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'id': 905105572529393668, 'media_url': 'http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'sizes': [{'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 271, 'w': 680, 'resize': 'fit'}, {'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 399, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105572529393668'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'id': 905105573255016448, 'media_url': 'http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'sizes': [{'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 195, 'w': 680, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 287, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105573255016448'}], 'urls': [{'display_url': 'gyford.com/phil/writing/2…', 'url': 'https://t.co/2yUmmn3TOc', 'indices': [107, 130], 'expanded_url': 'http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php'}]}, 'source': 'web', 'created_at': '2017-09-05 16:29:22 +0000', 'id_str': '905105588279013377'} diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index b8b5dc4..dc44047 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -5,6 +5,7 @@ from .config import ( test_tweet_object, test_tweet_html, test_tweet_symbols_object, test_tweet_compat_object, test_tweet_extended_object, test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, + test_tweet_media, unittest ) @@ -72,3 +73,11 @@ class TestHtmlForTweetTestCase(unittest.TestCase): tweet_text = self.api.html_for_tweet(test_tweet_extended_object) # full tweet rendered with suffix self.assertEqual(test_tweet_extended_html, tweet_text) + + def test_media(self): + tweet_text = self.api.html_for_tweet(test_tweet_media) + + self.assertEqual( + """I made some D3.js charts showing the years covered by books in a series compared to their publishing dates gyford.com/phil/writing/2… pic.twitter.com/OwNc6uJklg""", + tweet_text) + diff --git a/tweet.json b/tweet.json new file mode 100644 index 0000000..5e61ed8 --- /dev/null +++ b/tweet.json @@ -0,0 +1,142 @@ +{ + "source":"web", + "entities":{ + "user_mentions":[ ], + "media":[ + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "indices":[ 131, 154 ], + "url":"https://t.co/OwNc6uJklg", + "media_url":"http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg", + "id_str":"905105571765944320", + "id":905105571765944320, + "media_url_https":"https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg", + "sizes":[ + { + "h":256, + "resize":"fit", + "w":680 + }, + { + "h":376, + "resize":"fit", + "w":1000 + }, + { + "h":150, + "resize":"crop", + "w":150 + }, + { + "h":376, + "resize":"fit", + "w":1000 + }, + { + "h":376, + "resize":"fit", + "w":1000 + } + ], + "display_url":"pic.twitter.com/OwNc6uJklg" + }, + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "indices":[ 131, 154 ], + "url":"https://t.co/OwNc6uJklg", + "media_url":"http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg", + "id_str":"905105572529393668", + "id":905105572529393668, + "media_url_https":"https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg", + "sizes":[ + { + "h":399, + "resize":"fit", + "w":1000 + }, + { + "h":271, + "resize":"fit", + "w":680 + }, + { + "h":399, + "resize":"fit", + "w":1000 + }, + { + "h":150, + "resize":"crop", + "w":150 + }, + { + "h":399, + "resize":"fit", + "w":1000 + } + ], + "display_url":"pic.twitter.com/OwNc6uJklg" + }, + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "indices":[ 131, 154 ], + "url":"https://t.co/OwNc6uJklg", + "media_url":"http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg", + "id_str":"905105573255016448", + "id":905105573255016448, + "media_url_https":"https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg", + "sizes":[ + { + "h":287, + "resize":"fit", + "w":1000 + }, + { + "h":195, + "resize":"fit", + "w":680 + }, + { + "h":150, + "resize":"crop", + "w":150 + }, + { + "h":287, + "resize":"fit", + "w":1000 + }, + { + "h":287, + "resize":"fit", + "w":1000 + } + ], + "display_url":"pic.twitter.com/OwNc6uJklg" + } + ], + "hashtags":[ ], + "urls":[ + { + "indices":[ 107, 130 ], + "url":"https://t.co/2yUmmn3TOc", + "expanded_url":"http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php", + "display_url":"gyford.com/phil/writing/2\u2026" + } + ] + }, + "geo":{ }, + "id_str":"905105588279013377", + "text":"I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg", + "id":905105588279013377, + "created_at":"2017-09-05 16:29:22 +0000", + "user":{ + "name":"Phil Gyford", + "screen_name":"philgyford", + "protected":false, + "id_str":"12552", + "profile_image_url_https":"https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg", + "id":12552, + "verified":false + } +} diff --git a/twython/api.py b/twython/api.py index 2782168..fac7928 100644 --- a/twython/api.py +++ b/twython/api.py @@ -629,26 +629,30 @@ class Twython(EndpointsMixin, object): else: suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) - if 'media' in tweet['entities']: - for entity in tweet['entities']['media']: - temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + if 'media' in tweet['entities'] and len(tweet['entities']['media']) > 0: + # We just link to the overall URL for the tweet's media, + # rather than to each individual item. + # So, we get the URL from the first media item: + entity = tweet['entities']['media'][0] - if use_display_url and entity.get('display_url') and not use_expanded_url: - shown_url = entity['display_url'] - elif use_expanded_url and entity.get('expanded_url'): - shown_url = entity['expanded_url'] - else: - shown_url = entity['url'] + temp = {} + temp['start'] = entity['indices'][0] + temp['end'] = entity['indices'][1] - url_html = '%s' % (entity['url'], shown_url) + if use_display_url and entity.get('display_url') and not use_expanded_url: + shown_url = entity['display_url'] + elif use_expanded_url and entity.get('expanded_url'): + shown_url = entity['expanded_url'] + else: + shown_url = entity['url'] - if display_text_start <= temp['start'] <= display_text_end: - temp['replacement'] = url_html - entities.append(temp) - else: - suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) + url_html = '%s' % (entity['url'], shown_url) + + if display_text_start <= temp['start'] <= display_text_end: + temp['replacement'] = url_html + entities.append(temp) + else: + suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html) # Now do all the replacements, starting from the end, so that the # start/end indices still work: -- 2.39.5 From 89755a8643805457aa874f2d7f34c3700e662609 Mon Sep 17 00:00:00 2001 From: FoxMaSk Date: Sun, 8 Oct 2017 08:57:04 +0200 Subject: [PATCH 160/204] update the link to the doc of the twitter API --- docs/usage/advanced_usage.rst | 10 +++++----- docs/usage/basic_usage.rst | 8 ++++---- docs/usage/starting_out.rst | 2 +- docs/usage/streaming_api.rst | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index 32334c0..bdb0b23 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -29,11 +29,11 @@ This uploads an image as a media object and associates it with a status update. photo = open('/path/to/file/image.jpg', 'rb') response = twitter.upload_media(media=photo) twitter.update_status(status='Checkout this cool image!', media_ids=[response['media_id']]) - + Documentation: -* https://dev.twitter.com/rest/reference/post/statuses/update -* https://dev.twitter.com/rest/reference/post/media/upload +* https://developer.twitter.com/en/docs/api-reference-index +* https://developer.twitter.com/en/docs/media/upload-media/overview Updating Status with Video -------------------------- @@ -48,8 +48,8 @@ This uploads a video as a media object and associates it with a status update. Documentation: -* https://dev.twitter.com/rest/reference/post/statuses/update -* https://dev.twitter.com/rest/reference/post/media/upload +* https://developer.twitter.com/en/docs/api-reference-index +* https://developer.twitter.com/en/docs/media/upload-media/overview Posting a Status with an Editing Image -------------------------------------- diff --git a/docs/usage/basic_usage.rst b/docs/usage/basic_usage.rst index 84ffb08..d7e32e3 100644 --- a/docs/usage/basic_usage.rst +++ b/docs/usage/basic_usage.rst @@ -28,7 +28,7 @@ Create a Twython instance with your application keys and the users OAuth tokens User Information ^^^^^^^^^^^^^^^^ -Documentation: https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials +Documentation: https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials .. code-block:: python @@ -37,7 +37,7 @@ Documentation: https://dev.twitter.com/docs/api/1.1/get/account/verify_credentia Authenticated Users Home Timeline ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Documentation: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline +Documentation: https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline .. code-block:: python @@ -48,7 +48,7 @@ Updating Status This method makes use of dynamic arguments, :ref:`read more about them ` -Documentation: https://dev.twitter.com/docs/api/1.1/post/statuses/update +Documentation: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update .. code-block:: python @@ -72,7 +72,7 @@ Searching .. note:: Searching can be done whether you're authenticated via OAuth 1 or OAuth 2 -Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets +Documentation: https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets .. code-block:: python diff --git a/docs/usage/starting_out.rst b/docs/usage/starting_out.rst index 0a681ac..7bb3d94 100644 --- a/docs/usage/starting_out.rst +++ b/docs/usage/starting_out.rst @@ -10,7 +10,7 @@ This section is going to help you understand creating a Twitter Application, aut Beginning --------- -First, you'll want to head over to https://dev.twitter.com/apps and register an application! +First, you'll want to head over to https://apps.twitter.com/ and register an application! After you register, grab your applications ``Consumer Key`` and ``Consumer Secret`` from the application details tab. diff --git a/docs/usage/streaming_api.rst b/docs/usage/streaming_api.rst index 4d6c1c3..97ef6ea 100644 --- a/docs/usage/streaming_api.rst +++ b/docs/usage/streaming_api.rst @@ -5,7 +5,7 @@ Streaming API This section will cover how to use Twython and interact with the Twitter Streaming API. -Streaming Documentation: https://dev.twitter.com/docs/streaming-apis +Streaming Documentation: https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types .. important:: The Streaming API requires that you have OAuth 1 authentication credentials. If you don't have credentials, head over to the :ref:`authentication section ` and find out how! -- 2.39.5 From 2cb2ed4a31826349ad81e39a5c035a7eed98c6b1 Mon Sep 17 00:00:00 2001 From: Jose Manuel Delicado Date: Mon, 9 Oct 2017 18:11:24 +0200 Subject: [PATCH 161/204] Twython/api.py: suggested changes in review have been made for pull request 460 --- twython/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index 577d6a6..0ee942c 100644 --- a/twython/api.py +++ b/twython/api.py @@ -140,7 +140,7 @@ class Twython(EndpointsMixin, object): params = params or {} func = getattr(self.client, method) - if type(params) is dict: + if isinstance(params, dict): params, files = _transparent_params(params) else: params = params @@ -196,14 +196,14 @@ class Twython(EndpointsMixin, object): error_message, error_code=response.status_code, retry_after=response.headers.get('X-Rate-Limit-Reset')) - content="" + content = '' try: if response.status_code == 204: content = response.content else: content = response.json() except ValueError: - if response.content!="": + if response.content != '': raise TwythonError('Response was not valid JSON. \ Unable to decode.') -- 2.39.5 From 13fd0a868436a1b38b7422c04da1755d94e6e1a8 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 10 Oct 2017 11:57:03 +0100 Subject: [PATCH 162/204] Define encoding for tests config file --- tests/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/config.py b/tests/config.py index 07ca96d..7fc4759 100644 --- a/tests/config.py +++ b/tests/config.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import sys -- 2.39.5 From 9ade0946b58c4243759a393e35f87bd499134e5f Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 10 Oct 2017 12:24:09 +0100 Subject: [PATCH 163/204] Add test for html_for_tweet() for quoted tweets --- tests/config.py | 5 +++++ tests/test_html_for_tweet.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/config.py b/tests/config.py index 7fc4759..795ba07 100644 --- a/tests/config.py +++ b/tests/config.py @@ -34,10 +34,15 @@ test_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHE test_tweet_compat_object = {u'contributors': None, u'truncated': True, u'text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/SRmsuks2ru', u'indices': [117, 140], u'expanded_url': u'https://twitter.com/i/web/status/777915304261193728', u'display_url': u'twitter.com/i/web/status/7\u2026'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None} test_tweet_extended_object = {u'full_text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. https://t.co/I9pUC0NdZC", u'truncated': False, u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'contributors': None, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [], u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'id_str': u'777914712382058496', u'indices': [140, 163], u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'type': u'photo', u'id': 777914712382058496, u'display_url': u'pic.twitter.com/I9pUC0NdZC'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'display_text_range': [0, 139], u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None, u'extended_entities': {u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'display_url': u'pic.twitter.com/I9pUC0NdZC', u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'video_info': {u'aspect_ratio': [1, 1], u'variants': [{u'url': u'https://pbs.twimg.com/tweet_video/Csu1TzEVMAAAEv7.mp4', u'bitrate': 0, u'content_type': u'video/mp4'}]}, u'id_str': u'777914712382058496', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'indices': [140, 163], u'type': u'animated_gif', u'id': 777914712382058496, u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg'}]}} + test_tweet_extended_html = 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC' test_tweet_identical_urls = {u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [], u'urls': [{u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [42, 65], u'url': u'https://t.co/W0uArTMk9N'}, {u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [101, 124], u'url': u'https://t.co/W0uArTMk9N'}]}, u'full_text': u'Use Cases, Trials and Making 5G a Reality https://t.co/W0uArTMk9N #5G #innovation via @5GWorldSeries https://t.co/W0uArTMk9N'} test_tweet_reply = { u'display_text_range': [12,114], u'in_reply_to_status_id_str':u'742374355531923456', u'source':u'web', u'geo':None, u'full_text':u'@philgyford Here\u2019s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr', u'extended_entities':{ u'media':[] }, u'id_str':u'300', u'in_reply_to_status_id':742374355531923456, u'id':300, u'in_reply_to_screen_name':u'philgyford', u'retweet_count':0, u'user':{ }, u'created_at':u'Mon Jun 13 15:48:06 +0000 2016', u'lang':u'en', u'favorite_count':0, u'coordinates':None, u'place':None, u'contributors':None, u'in_reply_to_user_id':12552, u'in_reply_to_user_id_str':u'12552', u'retweeted':False, u'favorited':False, u'truncated':False, u'entities':{ u'user_mentions':[ { u'id_str':u'12552', u'id':12552, u'screen_name':u'philgyford', u'name':u'Phil Gyford', u'indices':[ 0, 11 ] } ], u'media':[ ], u'hashtags':[ ], u'symbols':[ ], u'urls':[ ] }, u'is_quote_status':False, u'possibly_sensitive':False } + test_tweet_media = {'user': {'name': 'Phil Gyford', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', 'verified': False, 'screen_name': 'philgyford', 'id': 12552, 'id_str': '12552', 'protected': False}, 'geo': {}, 'id': 905105588279013377, 'text': 'I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg', 'entities': {'user_mentions': [], 'hashtags': [], 'media': [{'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'id': 905105571765944320, 'media_url': 'http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'sizes': [{'h': 256, 'w': 680, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105571765944320'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'id': 905105572529393668, 'media_url': 'http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'sizes': [{'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 271, 'w': 680, 'resize': 'fit'}, {'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 399, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105572529393668'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'id': 905105573255016448, 'media_url': 'http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'sizes': [{'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 195, 'w': 680, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 287, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105573255016448'}], 'urls': [{'display_url': 'gyford.com/phil/writing/2…', 'url': 'https://t.co/2yUmmn3TOc', 'indices': [107, 130], 'expanded_url': 'http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php'}]}, 'source': 'web', 'created_at': '2017-09-05 16:29:22 +0000', 'id_str': '905105588279013377'} + +test_tweet_quoted = {u'contributors': None, u'truncated': False, u'text': u'Here\u2019s a quoted tweet. https://t.co/3neKzof0gT', u'is_quote_status': True, u'in_reply_to_status_id': None, u'id': 917706313785905157, u'favorite_count': 0, u'source': u'Tweetbot for Mac', u'quoted_status_id': 917699069916729344, u'retweeted': False, u'coordinates': None, u'quoted_status': {u'contributors': None, u'truncated': False, u'text': u'The quoted tweet text.', u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 917699069916729344, u'favorite_count': 1, u'source': u'web', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': []}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917699069916729344', u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': False, u'default_profile_image': False, u'id': 12552, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'verified': False, u'translator_type': u'none', u'profile_text_color': u'000000', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', u'profile_sidebar_fill_color': u'EEEEEE', u'entities': {u'url': {u'urls': [{u'url': u'https://t.co/FsYzXrATit', u'indices': [0, 23], u'expanded_url': u'http://www.gyford.com', u'display_url': u'gyford.com'}]}, u'description': {u'urls': []}}, u'followers_count': 2615, u'profile_sidebar_border_color': u'FFFFFF', u'id_str': u'12552', u'profile_background_color': u'C0C0C0', u'listed_count': 130, u'is_translation_enabled': False, u'utc_offset': 3600, u'statuses_count': 19182, u'description': u'Revised and updated edition for 2004.', u'friends_count': 308, u'location': u'London, UK', u'profile_link_color': u'0000FF', u'profile_image_url': u'http://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', u'following': False, u'geo_enabled': True, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'philgyford', u'lang': u'en-gb', u'profile_background_tile': False, u'favourites_count': 2203, u'name': u'Phil Gyford', u'notifications': False, u'url': u'https://t.co/FsYzXrATit', u'created_at': u'Wed Nov 15 16:55:59 +0000 2006', u'contributors_enabled': False, u'time_zone': u'London', u'protected': False, u'default_profile': False, u'is_translator': False}, u'geo': None, u'in_reply_to_user_id_str': None, u'lang': u'ht', u'created_at': u'Tue Oct 10 10:31:22 +0000 2017', u'in_reply_to_status_id_str': None, u'place': None}, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/3neKzof0gT', u'indices': [23, 46], u'expanded_url': u'https://twitter.com/philgyford/status/917699069916729344', u'display_url': u'twitter.com/philgyford/sta\u2026'}]}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917706313785905157', u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'default_profile_image': True, u'id': 2030131, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'verified': False, u'translator_type': u'none', u'profile_text_color': u'000000', u'profile_image_url_https': u'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', u'profile_sidebar_fill_color': u'E0FF92', u'entities': {u'description': {u'urls': [{u'url': u'https://t.co/FsYzXrATit', u'indices': [17, 40], u'expanded_url': u'http://www.gyford.com', u'display_url': u'gyford.com'}]}}, u'followers_count': 12, u'profile_sidebar_border_color': u'000000', u'id_str': u'2030131', u'profile_background_color': u'9AE4E8', u'listed_count': 4, u'is_translation_enabled': False, u'utc_offset': 3600, u'statuses_count': 497, u'description': u'Testing #testing https://t.co/FsYzXrATit $IBM #test @philgyford', u'friends_count': 18, u'location': u'', u'profile_link_color': u'0000FF', u'profile_image_url': u'http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', u'following': True, u'geo_enabled': True, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'philgyfordtest', u'lang': u'en', u'profile_background_tile': False, u'favourites_count': 0, u'name': u'Phil Gyford Test', u'notifications': False, u'url': None, u'created_at': u'Fri Mar 23 16:56:52 +0000 2007', u'contributors_enabled': False, u'time_zone': u'London', 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'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Tue Oct 10 11:00:10 +0000 2017', u'quoted_status_id_str': u'917699069916729344', u'in_reply_to_status_id_str': None, u'place': None} + diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index dc44047..6f1ffea 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -5,7 +5,7 @@ from .config import ( test_tweet_object, test_tweet_html, test_tweet_symbols_object, test_tweet_compat_object, test_tweet_extended_object, test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, - test_tweet_media, + test_tweet_media, test_tweet_quoted, unittest ) @@ -81,3 +81,12 @@ class TestHtmlForTweetTestCase(unittest.TestCase): """I made some D3.js charts showing the years covered by books in a series compared to their publishing dates gyford.com/phil/writing/2… pic.twitter.com/OwNc6uJklg""", tweet_text) + def test_quoted(self): + "With expand_quoted_status=True it should include a quoted tweet." + tweet_text = self.api.html_for_tweet(test_tweet_quoted, + expand_quoted_status=True) + + self.assertEqual( + u"""Here\u2019s a quoted tweet. twitter.com/philgyford/sta\u2026
The quoted tweet text.Phil Gyford@philgyford
""", + tweet_text) + -- 2.39.5 From 9ccdb48248c6ed033da60fc740fcfd8b808a94a3 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 10 Oct 2017 12:31:57 +0100 Subject: [PATCH 164/204] Add test for html_for_tweet() for retweets --- tests/config.py | 2 ++ tests/test_html_for_tweet.py | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/config.py b/tests/config.py index 795ba07..d4fc471 100644 --- a/tests/config.py +++ b/tests/config.py @@ -46,3 +46,5 @@ test_tweet_media = {'user': {'name': 'Phil Gyford', 'profile_image_url_https': ' test_tweet_quoted = {u'contributors': None, u'truncated': False, u'text': u'Here\u2019s a quoted tweet. https://t.co/3neKzof0gT', u'is_quote_status': True, u'in_reply_to_status_id': None, u'id': 917706313785905157, u'favorite_count': 0, u'source': u'Tweetbot for Mac', u'quoted_status_id': 917699069916729344, u'retweeted': False, u'coordinates': None, u'quoted_status': {u'contributors': None, u'truncated': False, u'text': u'The quoted tweet text.', u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 917699069916729344, u'favorite_count': 1, u'source': u'web', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': []}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917699069916729344', u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': False, u'default_profile_image': False, u'id': 12552, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'verified': False, u'translator_type': u'none', u'profile_text_color': u'000000', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', u'profile_sidebar_fill_color': u'EEEEEE', u'entities': {u'url': {u'urls': [{u'url': u'https://t.co/FsYzXrATit', u'indices': [0, 23], u'expanded_url': u'http://www.gyford.com', u'display_url': u'gyford.com'}]}, u'description': {u'urls': []}}, u'followers_count': 2615, u'profile_sidebar_border_color': u'FFFFFF', u'id_str': u'12552', u'profile_background_color': u'C0C0C0', u'listed_count': 130, u'is_translation_enabled': False, u'utc_offset': 3600, u'statuses_count': 19182, u'description': u'Revised and updated edition for 2004.', u'friends_count': 308, u'location': u'London, UK', u'profile_link_color': u'0000FF', u'profile_image_url': u'http://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', u'following': False, u'geo_enabled': True, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'philgyford', u'lang': u'en-gb', u'profile_background_tile': False, u'favourites_count': 2203, u'name': u'Phil Gyford', u'notifications': False, u'url': u'https://t.co/FsYzXrATit', u'created_at': u'Wed Nov 15 16:55:59 +0000 2006', u'contributors_enabled': False, u'time_zone': u'London', u'protected': False, u'default_profile': False, u'is_translator': False}, u'geo': None, u'in_reply_to_user_id_str': None, u'lang': u'ht', u'created_at': u'Tue Oct 10 10:31:22 +0000 2017', u'in_reply_to_status_id_str': None, u'place': None}, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/3neKzof0gT', u'indices': [23, 46], u'expanded_url': u'https://twitter.com/philgyford/status/917699069916729344', u'display_url': u'twitter.com/philgyford/sta\u2026'}]}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917706313785905157', u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'default_profile_image': True, u'id': 2030131, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'verified': False, u'translator_type': u'none', u'profile_text_color': u'000000', u'profile_image_url_https': u'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', u'profile_sidebar_fill_color': u'E0FF92', u'entities': {u'description': {u'urls': [{u'url': u'https://t.co/FsYzXrATit', u'indices': [17, 40], u'expanded_url': u'http://www.gyford.com', u'display_url': u'gyford.com'}]}}, u'followers_count': 12, u'profile_sidebar_border_color': u'000000', u'id_str': u'2030131', u'profile_background_color': u'9AE4E8', u'listed_count': 4, u'is_translation_enabled': False, u'utc_offset': 3600, u'statuses_count': 497, u'description': u'Testing #testing https://t.co/FsYzXrATit $IBM #test @philgyford', u'friends_count': 18, u'location': u'', u'profile_link_color': u'0000FF', u'profile_image_url': u'http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', u'following': True, u'geo_enabled': True, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'philgyfordtest', u'lang': u'en', u'profile_background_tile': False, u'favourites_count': 0, u'name': u'Phil Gyford Test', u'notifications': False, u'url': None, u'created_at': u'Fri Mar 23 16:56:52 +0000 2007', u'contributors_enabled': False, u'time_zone': u'London', 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'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Tue Oct 10 11:00:10 +0000 2017', u'quoted_status_id_str': u'917699069916729344', u'in_reply_to_status_id_str': None, u'place': None} +test_tweet_retweet = {'coordinates': None, 'source': 'Tweetbot for Mac', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 0, 'user': {'has_extended_profile': False, 'following': True, 'url': None, 'profile_background_tile': False, 'geo_enabled': True, 'favourites_count': 0, 'lang': 'en', 'follow_request_sent': False, 'utc_offset': 3600, 'time_zone': 'London', 'profile_sidebar_border_color': '000000', 'verified': False, 'profile_use_background_image': True, 'name': 'Phil Gyford Test', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'default_profile_image': True, 'friends_count': 18, 'profile_image_url': 'http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', 'profile_image_url_https': 'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', 'default_profile': False, 'screen_name': 'philgyfordtest', 'profile_link_color': '0000FF', 'profile_text_color': '000000', 'id_str': '2030131', 'profile_background_color': '9AE4E8', 'followers_count': 12, 'entities': {'description': {'urls': [{'indices': [17, 40], 'url': 'https://t.co/FsYzXrATit', 'expanded_url': 'http://www.gyford.com', 'display_url': 'gyford.com'}]}}, 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'protected': False, 'notifications': False, 'description': 'Testing #testing https://t.co/FsYzXrATit $IBM #test @philgyford', 'is_translation_enabled': False, 'is_translator': False, 'translator_type': 'none', 'contributors_enabled': False, 'statuses_count': 498, 'listed_count': 4, 'created_at': 'Fri Mar 23 16:56:52 +0000 2007', 'profile_sidebar_fill_color': 'E0FF92', 'id': 2030131, 'location': ''}, 'favorited': False, 'retweeted_status': {'coordinates': None, 'source': 'Pepys\' Diary', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 21, 'user': {'has_extended_profile': False, 'following': True, 'url': 'https://t.co/b5ZyzWwQQA', 'profile_background_tile': False, 'geo_enabled': False, 'favourites_count': 1, 'lang': 'en', 'follow_request_sent': False, 'utc_offset': 3600, 'time_zone': 'London', 'profile_sidebar_border_color': 'CCCCCC', 'verified': False, 'profile_banner_url': 'https://pbs.twimg.com/profile_banners/14475268/1432652170', 'profile_use_background_image': False, 'name': 'Samuel Pepys', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'default_profile_image': False, 'friends_count': 14, 'profile_image_url': 'http://pbs.twimg.com/profile_images/629231884304695296/VZ-9FQ28_normal.jpg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/629231884304695296/VZ-9FQ28_normal.jpg', 'default_profile': False, 'screen_name': 'samuelpepys', 'profile_link_color': '549355', 'profile_text_color': '333333', 'id_str': '14475268', 'profile_background_color': 'F1F4EB', 'followers_count': 55980, 'entities': {'description': {'urls': []}, 'url': {'urls': [{'indices': [0, 23], 'url': 'https://t.co/b5ZyzWwQQA', 'expanded_url': 'http://www.pepysdiary.com/', 'display_url': 'pepysdiary.com'}]}}, 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'protected': False, 'notifications': False, 'description': 'The diaries of Samuel Pepys in real time, 1660-69. Currently tweeting the events of 1664. Run by @philgyford.', 'is_translation_enabled': False, 'is_translator': False, 'translator_type': 'none', 'contributors_enabled': False, 'statuses_count': 11060, 'listed_count': 1223, 'created_at': 'Tue Apr 22 14:39:20 +0000 2008', 'profile_sidebar_fill_color': 'E8E7DC', 'id': 14475268, 'location': 'London, UK'}, 'favorited': False, 'id': 917459832885653506, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917459832885653506', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Mon Oct 09 18:40:44 +0000 2017', 'is_quote_status': False, 'retweeted': False}, 'id': 917712989649801216, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917712989649801216', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [{'id_str': '14475268', 'indices': [3, 15], 'name': 'Samuel Pepys', 'id': 14475268, 'screen_name': 'samuelpepys'}], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'RT @samuelpepys: My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Tue Oct 10 11:26:41 +0000 2017', 'is_quote_status': False, 'retweeted': False} + diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index 6f1ffea..9ba25c6 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -5,7 +5,7 @@ from .config import ( test_tweet_object, test_tweet_html, test_tweet_symbols_object, test_tweet_compat_object, test_tweet_extended_object, test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, - test_tweet_media, test_tweet_quoted, + test_tweet_media, test_tweet_quoted, test_tweet_retweet, unittest ) @@ -90,3 +90,11 @@ class TestHtmlForTweetTestCase(unittest.TestCase): u"""Here\u2019s a quoted tweet. twitter.com/philgyford/sta\u2026
The quoted tweet text.Phil Gyford@philgyford
""", tweet_text) + def test_retweet(self): + "With expand_quoted_status=True it should include a quoted tweet." + tweet_text = self.api.html_for_tweet(test_tweet_retweet) + + self.assertEqual( + u"""My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.""", + tweet_text) + -- 2.39.5 From 5c55aa88449a7110da1e1c4fea130d9109f303a6 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Tue, 10 Oct 2017 12:41:00 +0100 Subject: [PATCH 165/204] Cut some un-needed data out of the test tweet objects --- tests/config.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/config.py b/tests/config.py index d4fc471..4d76d1c 100644 --- a/tests/config.py +++ b/tests/config.py @@ -28,11 +28,12 @@ test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', 'twitterapi') 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_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHER and $A.', u'contributors': None, u'geo': None, u'favorited': True, u'in_reply_to_user_id_str': None, u'user': {u'location': u'', u'id_str': u'2030131', u'protected': False, u'profile_background_tile': False, u'friends_count': 18, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'entities': {u'description': {u'urls': []}}, u'lang': u'en', u'listed_count': 5, u'default_profile_image': True, u'default_profile': False, u'statuses_count': 447, u'notifications': False, u'profile_background_color': u'9AE4E8', u'profile_sidebar_fill_color': u'E0FF92', u'profile_link_color': u'0000FF', u'profile_image_url_https': u'https://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png', u'followers_count': 8, u'geo_enabled': True, u'following': True, u'has_extended_profile': False, u'profile_use_background_image': True, u'profile_text_color': u'000000', u'screen_name': u'philgyfordtest', u'contributors_enabled': False, u'verified': False, u'name': u'Phil Gyford Test', u'profile_sidebar_border_color': u'000000', u'utc_offset': 0, u'profile_image_url': u'http://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png', u'id': 2030131, u'favourites_count': 0, u'time_zone': u'London', u'url': None, u'is_translation_enabled': False, u'is_translator': False, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'description': u'', u'created_at': u'Fri Mar 23 16:56:52 +0000 2007', u'follow_request_sent': False}, u'in_reply_to_user_id': None, u'retweeted': False, u'coordinates': None, u'place': None, u'in_reply_to_status_id': None, u'lang': u'en', u'in_reply_to_status_id_str': None, u'truncated': False, u'retweet_count': 0, u'is_quote_status': False, u'id': 662694880657989632, u'id_str': u'662694880657989632', u'in_reply_to_screen_name': None, u'favorite_count': 1, u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [{u'indices': [14, 19], u'text': u'AAPL'}, {u'indices': [24, 28], u'text': u'PEP'}, {u'indices': [46, 48], u'text': u'A'}], u'urls': []}, u'created_at': u'Fri Nov 06 18:15:46 +0000 2015', u'source': u'Tweetbot for Mac'} +test_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHER and $A.', u'contributors': None, u'geo': None, u'favorited': True, u'in_reply_to_user_id_str': None, u'user': {u'screen_name': u'philgyfordtest', u'name': u'Phil Gyford Test'}, u'in_reply_to_user_id': None, u'retweeted': False, u'coordinates': None, u'place': None, u'in_reply_to_status_id': None, u'lang': u'en', u'in_reply_to_status_id_str': None, u'truncated': False, u'retweet_count': 0, u'is_quote_status': False, u'id': 662694880657989632, u'id_str': u'662694880657989632', u'in_reply_to_screen_name': None, u'favorite_count': 1, u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [{u'indices': [14, 19], u'text': u'AAPL'}, {u'indices': [24, 28], u'text': u'PEP'}, {u'indices': [46, 48], u'text': u'A'}], u'urls': []}, u'created_at': u'Fri Nov 06 18:15:46 +0000 2015', u'source': u'web'} -test_tweet_compat_object = {u'contributors': None, u'truncated': True, u'text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/SRmsuks2ru', u'indices': [117, 140], u'expanded_url': u'https://twitter.com/i/web/status/777915304261193728', u'display_url': u'twitter.com/i/web/status/7\u2026'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None} +test_tweet_compat_object = {u'contributors': None, u'truncated': True, u'text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/SRmsuks2ru', u'indices': [117, 140], u'expanded_url': u'https://twitter.com/i/web/status/777915304261193728', u'display_url': u'twitter.com/i/web/status/7\u2026'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'name': u'Twitter', u'screen_name': u'twitter'}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None} test_tweet_extended_object = {u'full_text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. https://t.co/I9pUC0NdZC", u'truncated': False, u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'contributors': None, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [], u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'id_str': u'777914712382058496', u'indices': [140, 163], u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'type': u'photo', u'id': 777914712382058496, u'display_url': u'pic.twitter.com/I9pUC0NdZC'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'display_text_range': [0, 139], u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None, u'extended_entities': {u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'display_url': u'pic.twitter.com/I9pUC0NdZC', u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'video_info': {u'aspect_ratio': [1, 1], u'variants': [{u'url': u'https://pbs.twimg.com/tweet_video/Csu1TzEVMAAAEv7.mp4', u'bitrate': 0, u'content_type': u'video/mp4'}]}, u'id_str': u'777914712382058496', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'indices': [140, 163], u'type': u'animated_gif', u'id': 777914712382058496, u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg'}]}} test_tweet_extended_html = 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC' @@ -42,9 +43,10 @@ test_tweet_identical_urls = {u'entities': {u'hashtags': [], u'user_mentions': [] test_tweet_reply = { u'display_text_range': [12,114], u'in_reply_to_status_id_str':u'742374355531923456', u'source':u'web', u'geo':None, u'full_text':u'@philgyford Here\u2019s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr', u'extended_entities':{ u'media':[] }, u'id_str':u'300', u'in_reply_to_status_id':742374355531923456, u'id':300, u'in_reply_to_screen_name':u'philgyford', u'retweet_count':0, u'user':{ }, u'created_at':u'Mon Jun 13 15:48:06 +0000 2016', u'lang':u'en', u'favorite_count':0, u'coordinates':None, u'place':None, u'contributors':None, u'in_reply_to_user_id':12552, u'in_reply_to_user_id_str':u'12552', u'retweeted':False, u'favorited':False, u'truncated':False, u'entities':{ u'user_mentions':[ { u'id_str':u'12552', u'id':12552, u'screen_name':u'philgyford', u'name':u'Phil Gyford', u'indices':[ 0, 11 ] } ], u'media':[ ], u'hashtags':[ ], u'symbols':[ ], u'urls':[ ] }, u'is_quote_status':False, u'possibly_sensitive':False } -test_tweet_media = {'user': {'name': 'Phil Gyford', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', 'verified': False, 'screen_name': 'philgyford', 'id': 12552, 'id_str': '12552', 'protected': False}, 'geo': {}, 'id': 905105588279013377, 'text': 'I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg', 'entities': {'user_mentions': [], 'hashtags': [], 'media': [{'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'id': 905105571765944320, 'media_url': 'http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'sizes': [{'h': 256, 'w': 680, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105571765944320'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'id': 905105572529393668, 'media_url': 'http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'sizes': [{'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 271, 'w': 680, 'resize': 'fit'}, {'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 399, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105572529393668'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'id': 905105573255016448, 'media_url': 'http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'sizes': [{'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 195, 'w': 680, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 287, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105573255016448'}], 'urls': [{'display_url': 'gyford.com/phil/writing/2…', 'url': 'https://t.co/2yUmmn3TOc', 'indices': [107, 130], 'expanded_url': 'http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php'}]}, 'source': 'web', 'created_at': '2017-09-05 16:29:22 +0000', 'id_str': '905105588279013377'} +test_tweet_media = {'user': {'name': 'Phil Gyford', 'screen_name': 'philgyford'}, 'geo': {}, 'id': 905105588279013377, 'text': 'I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg', 'entities': {'user_mentions': [], 'hashtags': [], 'media': [{'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'id': 905105571765944320, 'media_url': 'http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'sizes': [{'h': 256, 'w': 680, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105571765944320'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'id': 905105572529393668, 'media_url': 'http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'sizes': [{'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 271, 'w': 680, 'resize': 'fit'}, {'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 399, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105572529393668'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'id': 905105573255016448, 'media_url': 'http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'sizes': [{'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 195, 'w': 680, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 287, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105573255016448'}], 'urls': [{'display_url': 'gyford.com/phil/writing/2…', 'url': 'https://t.co/2yUmmn3TOc', 'indices': [107, 130], 'expanded_url': 'http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php'}]}, 'source': 'web', 'created_at': '2017-09-05 16:29:22 +0000', 'id_str': '905105588279013377'} -test_tweet_quoted = {u'contributors': None, u'truncated': False, u'text': u'Here\u2019s a quoted tweet. https://t.co/3neKzof0gT', u'is_quote_status': True, u'in_reply_to_status_id': None, u'id': 917706313785905157, u'favorite_count': 0, u'source': u'Tweetbot for Mac', u'quoted_status_id': 917699069916729344, u'retweeted': False, u'coordinates': None, u'quoted_status': {u'contributors': None, u'truncated': False, u'text': u'The quoted tweet text.', u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 917699069916729344, u'favorite_count': 1, u'source': u'web', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': []}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917699069916729344', u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': False, u'default_profile_image': False, u'id': 12552, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'verified': False, u'translator_type': u'none', u'profile_text_color': u'000000', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', u'profile_sidebar_fill_color': u'EEEEEE', u'entities': {u'url': {u'urls': [{u'url': u'https://t.co/FsYzXrATit', u'indices': [0, 23], u'expanded_url': u'http://www.gyford.com', u'display_url': u'gyford.com'}]}, u'description': {u'urls': []}}, u'followers_count': 2615, u'profile_sidebar_border_color': u'FFFFFF', u'id_str': u'12552', u'profile_background_color': u'C0C0C0', u'listed_count': 130, u'is_translation_enabled': False, u'utc_offset': 3600, u'statuses_count': 19182, u'description': u'Revised and updated edition for 2004.', u'friends_count': 308, u'location': u'London, UK', u'profile_link_color': u'0000FF', u'profile_image_url': u'http://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg', u'following': False, u'geo_enabled': True, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'philgyford', u'lang': u'en-gb', u'profile_background_tile': False, u'favourites_count': 2203, u'name': u'Phil Gyford', u'notifications': False, u'url': u'https://t.co/FsYzXrATit', u'created_at': u'Wed Nov 15 16:55:59 +0000 2006', u'contributors_enabled': False, u'time_zone': u'London', u'protected': False, u'default_profile': False, u'is_translator': False}, u'geo': None, u'in_reply_to_user_id_str': None, u'lang': u'ht', u'created_at': u'Tue Oct 10 10:31:22 +0000 2017', u'in_reply_to_status_id_str': None, u'place': None}, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/3neKzof0gT', u'indices': [23, 46], u'expanded_url': u'https://twitter.com/philgyford/status/917699069916729344', u'display_url': u'twitter.com/philgyford/sta\u2026'}]}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917706313785905157', u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'default_profile_image': True, u'id': 2030131, u'profile_background_image_url_https': u'https://abs.twimg.com/images/themes/theme1/bg.png', u'verified': False, u'translator_type': u'none', u'profile_text_color': u'000000', u'profile_image_url_https': u'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', u'profile_sidebar_fill_color': u'E0FF92', u'entities': {u'description': {u'urls': [{u'url': u'https://t.co/FsYzXrATit', u'indices': [17, 40], u'expanded_url': u'http://www.gyford.com', u'display_url': u'gyford.com'}]}}, u'followers_count': 12, u'profile_sidebar_border_color': u'000000', u'id_str': u'2030131', u'profile_background_color': u'9AE4E8', u'listed_count': 4, u'is_translation_enabled': False, u'utc_offset': 3600, u'statuses_count': 497, u'description': u'Testing #testing https://t.co/FsYzXrATit $IBM #test @philgyford', u'friends_count': 18, u'location': u'', u'profile_link_color': u'0000FF', u'profile_image_url': u'http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', u'following': True, u'geo_enabled': True, u'profile_background_image_url': u'http://abs.twimg.com/images/themes/theme1/bg.png', u'screen_name': u'philgyfordtest', u'lang': u'en', u'profile_background_tile': False, u'favourites_count': 0, u'name': u'Phil Gyford Test', u'notifications': False, u'url': None, u'created_at': u'Fri Mar 23 16:56:52 +0000 2007', u'contributors_enabled': False, u'time_zone': u'London', 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'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Tue Oct 10 11:00:10 +0000 2017', u'quoted_status_id_str': u'917699069916729344', u'in_reply_to_status_id_str': None, u'place': None} +test_tweet_quoted = {u'contributors': None, u'truncated': False, u'text': u'Here\u2019s a quoted tweet. https://t.co/3neKzof0gT', u'is_quote_status': True, u'in_reply_to_status_id': None, u'id': 917706313785905157, u'favorite_count': 0, u'source': u'web', u'quoted_status_id': 917699069916729344, u'retweeted': False, u'coordinates': None, u'quoted_status': {u'contributors': None, u'truncated': False, u'text': u'The quoted tweet text.', u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 917699069916729344, u'favorite_count': 1, u'source': u'web', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': []}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917699069916729344', u'favorited': False, u'user': {u'screen_name': u'philgyford', u'name': u'Phil Gyford'}, u'geo': None, u'in_reply_to_user_id_str': None, u'lang': u'ht', u'created_at': u'Tue Oct 10 10:31:22 +0000 2017', u'in_reply_to_status_id_str': None, u'place': None}, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/3neKzof0gT', u'indices': [23, 46], u'expanded_url': u'https://twitter.com/philgyford/status/917699069916729344', u'display_url': u'twitter.com/philgyford/sta\u2026'}]}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917706313785905157', u'favorited': False, u'user': {u'screen_name': u'philgyfordtest', u'name': u'Phil Gyford Test'}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Tue Oct 10 11:00:10 +0000 2017', u'quoted_status_id_str': u'917699069916729344', u'in_reply_to_status_id_str': None, u'place': None} + +test_tweet_retweet = {'coordinates': None, 'source': 'web', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 0, 'user': {'name': 'Phil Gyford Test', 'screen_name': 'philgyfordtest'}, 'favorited': False, 'retweeted_status': {'coordinates': None, 'source': 'web', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 21, 'user': {'name': 'Samuel Pepys', 'screen_name': 'samuelpepys'}, 'favorited': False, 'id': 917459832885653506, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917459832885653506', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Mon Oct 09 18:40:44 +0000 2017', 'is_quote_status': False, 'retweeted': False}, 'id': 917712989649801216, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917712989649801216', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [{'id_str': '14475268', 'indices': [3, 15], 'name': 'Samuel Pepys', 'id': 14475268, 'screen_name': 'samuelpepys'}], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'RT @samuelpepys: My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Tue Oct 10 11:26:41 +0000 2017', 'is_quote_status': False, 'retweeted': False} -test_tweet_retweet = {'coordinates': None, 'source': 'Tweetbot for Mac', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 0, 'user': {'has_extended_profile': False, 'following': True, 'url': None, 'profile_background_tile': False, 'geo_enabled': True, 'favourites_count': 0, 'lang': 'en', 'follow_request_sent': False, 'utc_offset': 3600, 'time_zone': 'London', 'profile_sidebar_border_color': '000000', 'verified': False, 'profile_use_background_image': True, 'name': 'Phil Gyford Test', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'default_profile_image': True, 'friends_count': 18, 'profile_image_url': 'http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', 'profile_image_url_https': 'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png', 'default_profile': False, 'screen_name': 'philgyfordtest', 'profile_link_color': '0000FF', 'profile_text_color': '000000', 'id_str': '2030131', 'profile_background_color': '9AE4E8', 'followers_count': 12, 'entities': {'description': {'urls': [{'indices': [17, 40], 'url': 'https://t.co/FsYzXrATit', 'expanded_url': 'http://www.gyford.com', 'display_url': 'gyford.com'}]}}, 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'protected': False, 'notifications': False, 'description': 'Testing #testing https://t.co/FsYzXrATit $IBM #test @philgyford', 'is_translation_enabled': False, 'is_translator': False, 'translator_type': 'none', 'contributors_enabled': False, 'statuses_count': 498, 'listed_count': 4, 'created_at': 'Fri Mar 23 16:56:52 +0000 2007', 'profile_sidebar_fill_color': 'E0FF92', 'id': 2030131, 'location': ''}, 'favorited': False, 'retweeted_status': {'coordinates': None, 'source': 'Pepys\' Diary', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 21, 'user': {'has_extended_profile': False, 'following': True, 'url': 'https://t.co/b5ZyzWwQQA', 'profile_background_tile': False, 'geo_enabled': False, 'favourites_count': 1, 'lang': 'en', 'follow_request_sent': False, 'utc_offset': 3600, 'time_zone': 'London', 'profile_sidebar_border_color': 'CCCCCC', 'verified': False, 'profile_banner_url': 'https://pbs.twimg.com/profile_banners/14475268/1432652170', 'profile_use_background_image': False, 'name': 'Samuel Pepys', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'default_profile_image': False, 'friends_count': 14, 'profile_image_url': 'http://pbs.twimg.com/profile_images/629231884304695296/VZ-9FQ28_normal.jpg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/629231884304695296/VZ-9FQ28_normal.jpg', 'default_profile': False, 'screen_name': 'samuelpepys', 'profile_link_color': '549355', 'profile_text_color': '333333', 'id_str': '14475268', 'profile_background_color': 'F1F4EB', 'followers_count': 55980, 'entities': {'description': {'urls': []}, 'url': {'urls': [{'indices': [0, 23], 'url': 'https://t.co/b5ZyzWwQQA', 'expanded_url': 'http://www.pepysdiary.com/', 'display_url': 'pepysdiary.com'}]}}, 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'protected': False, 'notifications': False, 'description': 'The diaries of Samuel Pepys in real time, 1660-69. Currently tweeting the events of 1664. Run by @philgyford.', 'is_translation_enabled': False, 'is_translator': False, 'translator_type': 'none', 'contributors_enabled': False, 'statuses_count': 11060, 'listed_count': 1223, 'created_at': 'Tue Apr 22 14:39:20 +0000 2008', 'profile_sidebar_fill_color': 'E8E7DC', 'id': 14475268, 'location': 'London, UK'}, 'favorited': False, 'id': 917459832885653506, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917459832885653506', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Mon Oct 09 18:40:44 +0000 2017', 'is_quote_status': False, 'retweeted': False}, 'id': 917712989649801216, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917712989649801216', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [{'id_str': '14475268', 'indices': [3, 15], 'name': 'Samuel Pepys', 'id': 14475268, 'screen_name': 'samuelpepys'}], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'RT @samuelpepys: My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Tue Oct 10 11:26:41 +0000 2017', 'is_quote_status': False, 'retweeted': False} -- 2.39.5 From 5a008e7e77eec0016af938d833827332ebfaedc7 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Wed, 11 Oct 2017 18:27:53 +0100 Subject: [PATCH 166/204] Move all the raw tweets for tests into their own JSON files Seems better to have the raw data as JSON, like it comes from the API, then load it into python objects for each test. --- tests/config.py | 23 ---- tests/test_endpoints.py | 2 +- tests/test_html_for_tweet.py | 62 ++++++---- tests/tweets/basic.json | 170 +++++++++++++++++++++++++ tests/tweets/compat.json | 51 ++++++++ tests/tweets/extended.json | 204 ++++++++++++++++++++++++++++++ tests/tweets/identical_urls.json | 34 +++++ tests/tweets/media.json | 205 +++++++++++++++++++++++++++++++ tests/tweets/quoted.json | 94 ++++++++++++++ tests/tweets/reply.json | 62 ++++++++++ tests/tweets/retweet.json | 91 ++++++++++++++ tests/tweets/symbols.json | 61 +++++++++ 12 files changed, 1010 insertions(+), 49 deletions(-) create mode 100644 tests/tweets/basic.json create mode 100644 tests/tweets/compat.json create mode 100644 tests/tweets/extended.json create mode 100644 tests/tweets/identical_urls.json create mode 100644 tests/tweets/media.json create mode 100644 tests/tweets/quoted.json create mode 100644 tests/tweets/reply.json create mode 100644 tests/tweets/retweet.json create mode 100644 tests/tweets/symbols.json diff --git a/tests/config.py b/tests/config.py index 4d76d1c..8812b81 100644 --- a/tests/config.py +++ b/tests/config.py @@ -27,26 +27,3 @@ test_list_slug = os.environ.get('TEST_LIST_SLUG', 'team') test_list_owner_screen_name = os.environ.get('TEST_LIST_OWNER_SCREEN_NAME', 'twitterapi') -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_tweet_symbols_object = {u'text': u'Some symbols: $AAPL and $PEP and $ANOTHER and $A.', u'contributors': None, u'geo': None, u'favorited': True, u'in_reply_to_user_id_str': None, u'user': {u'screen_name': u'philgyfordtest', u'name': u'Phil Gyford Test'}, u'in_reply_to_user_id': None, u'retweeted': False, u'coordinates': None, u'place': None, u'in_reply_to_status_id': None, u'lang': u'en', u'in_reply_to_status_id_str': None, u'truncated': False, u'retweet_count': 0, u'is_quote_status': False, u'id': 662694880657989632, u'id_str': u'662694880657989632', u'in_reply_to_screen_name': None, u'favorite_count': 1, u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [{u'indices': [14, 19], u'text': u'AAPL'}, {u'indices': [24, 28], u'text': u'PEP'}, {u'indices': [46, 48], u'text': u'A'}], u'urls': []}, u'created_at': u'Fri Nov 06 18:15:46 +0000 2015', u'source': u'web'} - -test_tweet_compat_object = {u'contributors': None, u'truncated': True, u'text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/SRmsuks2ru', u'indices': [117, 140], u'expanded_url': u'https://twitter.com/i/web/status/777915304261193728', u'display_url': u'twitter.com/i/web/status/7\u2026'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'name': u'Twitter', u'screen_name': u'twitter'}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None} -test_tweet_extended_object = {u'full_text': u"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. https://t.co/I9pUC0NdZC", u'truncated': False, u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 777915304261193728, u'favorite_count': 13856, u'contributors': None, u'source': u'Twitter Web Client', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [], u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'id_str': u'777914712382058496', u'indices': [140, 163], u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'type': u'photo', u'id': 777914712382058496, u'display_url': u'pic.twitter.com/I9pUC0NdZC'}]}, u'in_reply_to_screen_name': None, u'id_str': u'777915304261193728', u'display_text_range': [0, 139], u'retweet_count': 14767, u'in_reply_to_user_id': None, u'favorited': False, u'user': {u'follow_request_sent': False, u'has_extended_profile': False, u'profile_use_background_image': True, u'id': 783214, u'verified': True, u'profile_text_color': u'333333', u'profile_image_url_https': u'https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'profile_sidebar_fill_color': u'F6F6F6', u'is_translator': False, u'geo_enabled': True, u'entities': {u'url': {u'urls': [{u'url': u'http://t.co/5iRhy7wTgu', u'indices': [0, 22], u'expanded_url': u'http://blog.twitter.com/', u'display_url': u'blog.twitter.com'}]}, u'description': {u'urls': [{u'url': u'https://t.co/qq1HEzvnrA', u'indices': [84, 107], u'expanded_url': u'http://support.twitter.com', u'display_url': u'support.twitter.com'}]}}, u'followers_count': 56827498, u'protected': False, u'location': u'San Francisco, CA', u'default_profile_image': False, u'id_str': u'783214', u'lang': u'en', u'utc_offset': -25200, u'statuses_count': 3161, u'description': u'Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.', u'friends_count': 145, u'profile_link_color': u'226699', u'profile_image_url': u'http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg', u'notifications': False, u'profile_background_image_url_https': u'https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'profile_background_color': u'ACDED6', u'profile_banner_url': u'https://pbs.twimg.com/profile_banners/783214/1471929200', u'profile_background_image_url': u'http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png', u'name': u'Twitter', u'is_translation_enabled': False, u'profile_background_tile': True, u'favourites_count': 2332, u'screen_name': u'twitter', u'url': u'http://t.co/5iRhy7wTgu', u'created_at': u'Tue Feb 20 14:35:54 +0000 2007', u'contributors_enabled': False, u'time_zone': u'Pacific Time (US & Canada)', u'profile_sidebar_border_color': u'FFFFFF', u'default_profile': False, u'following': False, u'listed_count': 90445}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Mon Sep 19 17:00:36 +0000 2016', u'in_reply_to_status_id_str': None, u'place': None, u'extended_entities': {u'media': [{u'expanded_url': u'https://twitter.com/twitter/status/777915304261193728/photo/1', u'display_url': u'pic.twitter.com/I9pUC0NdZC', u'url': u'https://t.co/I9pUC0NdZC', u'media_url_https': u'https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg', u'video_info': {u'aspect_ratio': [1, 1], u'variants': [{u'url': u'https://pbs.twimg.com/tweet_video/Csu1TzEVMAAAEv7.mp4', u'bitrate': 0, u'content_type': u'video/mp4'}]}, u'id_str': u'777914712382058496', u'sizes': {u'small': {u'h': 340, u'w': 340, u'resize': u'fit'}, u'large': {u'h': 700, u'w': 700, u'resize': u'fit'}, u'medium': {u'h': 600, u'w': 600, u'resize': u'fit'}, u'thumb': {u'h': 150, u'w': 150, u'resize': u'crop'}}, u'indices': [140, 163], u'type': u'animated_gif', u'id': 777914712382058496, u'media_url': u'http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg'}]}} - -test_tweet_extended_html = 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC' - -test_tweet_identical_urls = {u'entities': {u'hashtags': [], u'user_mentions': [], u'symbols': [], u'urls': [{u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [42, 65], u'url': u'https://t.co/W0uArTMk9N'}, {u'display_url': u'buff.ly/2sEhrgO', u'expanded_url': u'http://buff.ly/2sEhrgO', u'indices': [101, 124], u'url': u'https://t.co/W0uArTMk9N'}]}, u'full_text': u'Use Cases, Trials and Making 5G a Reality https://t.co/W0uArTMk9N #5G #innovation via @5GWorldSeries https://t.co/W0uArTMk9N'} - -test_tweet_reply = { u'display_text_range': [12,114], u'in_reply_to_status_id_str':u'742374355531923456', u'source':u'web', u'geo':None, u'full_text':u'@philgyford Here\u2019s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr', u'extended_entities':{ u'media':[] }, u'id_str':u'300', u'in_reply_to_status_id':742374355531923456, u'id':300, u'in_reply_to_screen_name':u'philgyford', u'retweet_count':0, u'user':{ }, u'created_at':u'Mon Jun 13 15:48:06 +0000 2016', u'lang':u'en', u'favorite_count':0, u'coordinates':None, u'place':None, u'contributors':None, u'in_reply_to_user_id':12552, u'in_reply_to_user_id_str':u'12552', u'retweeted':False, u'favorited':False, u'truncated':False, u'entities':{ u'user_mentions':[ { u'id_str':u'12552', u'id':12552, u'screen_name':u'philgyford', u'name':u'Phil Gyford', u'indices':[ 0, 11 ] } ], u'media':[ ], u'hashtags':[ ], u'symbols':[ ], u'urls':[ ] }, u'is_quote_status':False, u'possibly_sensitive':False } - - -test_tweet_media = {'user': {'name': 'Phil Gyford', 'screen_name': 'philgyford'}, 'geo': {}, 'id': 905105588279013377, 'text': 'I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg', 'entities': {'user_mentions': [], 'hashtags': [], 'media': [{'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'id': 905105571765944320, 'media_url': 'http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg', 'sizes': [{'h': 256, 'w': 680, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 376, 'w': 1000, 'resize': 'fit'}, {'h': 376, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105571765944320'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'id': 905105572529393668, 'media_url': 'http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg', 'sizes': [{'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 271, 'w': 680, 'resize': 'fit'}, {'h': 399, 'w': 1000, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 399, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105572529393668'}, {'url': 'https://t.co/OwNc6uJklg', 'display_url': 'pic.twitter.com/OwNc6uJklg', 'media_url_https': 'https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'id': 905105573255016448, 'media_url': 'http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg', 'sizes': [{'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 195, 'w': 680, 'resize': 'fit'}, {'h': 150, 'w': 150, 'resize': 'crop'}, {'h': 287, 'w': 1000, 'resize': 'fit'}, {'h': 287, 'w': 1000, 'resize': 'fit'}], 'indices': [131, 154], 'expanded_url': 'https://twitter.com/philgyford/status/905105588279013377/photo/1', 'id_str': '905105573255016448'}], 'urls': [{'display_url': 'gyford.com/phil/writing/2…', 'url': 'https://t.co/2yUmmn3TOc', 'indices': [107, 130], 'expanded_url': 'http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php'}]}, 'source': 'web', 'created_at': '2017-09-05 16:29:22 +0000', 'id_str': '905105588279013377'} - -test_tweet_quoted = {u'contributors': None, u'truncated': False, u'text': u'Here\u2019s a quoted tweet. https://t.co/3neKzof0gT', u'is_quote_status': True, u'in_reply_to_status_id': None, u'id': 917706313785905157, u'favorite_count': 0, u'source': u'web', u'quoted_status_id': 917699069916729344, u'retweeted': False, u'coordinates': None, u'quoted_status': {u'contributors': None, u'truncated': False, u'text': u'The quoted tweet text.', u'is_quote_status': False, u'in_reply_to_status_id': None, u'id': 917699069916729344, u'favorite_count': 1, u'source': u'web', u'retweeted': False, u'coordinates': None, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': []}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917699069916729344', u'favorited': False, u'user': {u'screen_name': u'philgyford', u'name': u'Phil Gyford'}, u'geo': None, u'in_reply_to_user_id_str': None, u'lang': u'ht', u'created_at': u'Tue Oct 10 10:31:22 +0000 2017', u'in_reply_to_status_id_str': None, u'place': None}, u'entities': {u'symbols': [], u'user_mentions': [], u'hashtags': [], u'urls': [{u'url': u'https://t.co/3neKzof0gT', u'indices': [23, 46], u'expanded_url': u'https://twitter.com/philgyford/status/917699069916729344', u'display_url': u'twitter.com/philgyford/sta\u2026'}]}, u'in_reply_to_screen_name': None, u'in_reply_to_user_id': None, u'retweet_count': 0, u'id_str': u'917706313785905157', u'favorited': False, u'user': {u'screen_name': u'philgyfordtest', u'name': u'Phil Gyford Test'}, u'geo': None, u'in_reply_to_user_id_str': None, u'possibly_sensitive': False, u'possibly_sensitive_appealable': False, u'lang': u'en', u'created_at': u'Tue Oct 10 11:00:10 +0000 2017', u'quoted_status_id_str': u'917699069916729344', u'in_reply_to_status_id_str': None, u'place': None} - -test_tweet_retweet = {'coordinates': None, 'source': 'web', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 0, 'user': {'name': 'Phil Gyford Test', 'screen_name': 'philgyfordtest'}, 'favorited': False, 'retweeted_status': {'coordinates': None, 'source': 'web', 'in_reply_to_user_id_str': None, 'truncated': False, 'in_reply_to_user_id': None, 'favorite_count': 21, 'user': {'name': 'Samuel Pepys', 'screen_name': 'samuelpepys'}, 'favorited': False, 'id': 917459832885653506, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917459832885653506', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Mon Oct 09 18:40:44 +0000 2017', 'is_quote_status': False, 'retweeted': False}, 'id': 917712989649801216, 'contributors': None, 'in_reply_to_screen_name': None, 'geo': None, 'in_reply_to_status_id_str': None, 'id_str': '917712989649801216', 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [{'id_str': '14475268', 'indices': [3, 15], 'name': 'Samuel Pepys', 'id': 14475268, 'screen_name': 'samuelpepys'}], 'urls': []}, 'in_reply_to_status_id': None, 'text': 'RT @samuelpepys: My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.', 'retweet_count': 3, 'place': None, 'lang': 'en', 'created_at': 'Tue Oct 10 11:26:41 +0000 2017', 'is_quote_status': False, 'retweeted': False} - - diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index aa79998..ecdd553 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -4,7 +4,7 @@ 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, unittest ) import time diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index 9ba25c6..cc49121 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -1,98 +1,110 @@ # -*- coding: utf-8 -*- +import json + from twython import Twython, TwythonError -from .config import ( - test_tweet_object, test_tweet_html, test_tweet_symbols_object, - test_tweet_compat_object, test_tweet_extended_object, - test_tweet_extended_html, test_tweet_identical_urls, test_tweet_reply, - test_tweet_media, test_tweet_quoted, test_tweet_retweet, - unittest -) +from .config import unittest class TestHtmlForTweetTestCase(unittest.TestCase): def setUp(self): self.api = Twython('', '', '', '') + def load_tweet(self, name): + f = open('tests/tweets/%s.json' % name) + tweet = json.load(f) + f.close() + return tweet + def test_basic(self): """Test HTML for Tweet returns what we want""" - tweet_text = self.api.html_for_tweet(test_tweet_object) - self.assertEqual(test_tweet_html, tweet_text) + tweet_object = self.load_tweet('basic') + tweet_text = self.api.html_for_tweet(tweet_object) + self.assertEqual(tweet_text, + 'google.com is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ github.com pic.twitter.com/N6InAO4B71') def test_reply(self): """Test HTML for Tweet links the replied-to username.""" - tweet_text = self.api.html_for_tweet(test_tweet_reply) + tweet_object = self.load_tweet('reply') + tweet_text = self.api.html_for_tweet(tweet_object) self.assertEqual(tweet_text, u'@philgyford Here’s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr') def test_expanded_url(self): """Test using expanded url in HTML for Tweet displays full urls""" - tweet_text = self.api.html_for_tweet(test_tweet_object, + tweet_object = self.load_tweet('basic') + tweet_text = self.api.html_for_tweet(tweet_object, use_expanded_url=True) # Make sure full url is in HTML self.assertTrue('http://google.com' in tweet_text) def test_short_url(self): """Test using expanded url in HTML for Tweet displays full urls""" - tweet_text = self.api.html_for_tweet(test_tweet_object, False) + tweet_object = self.load_tweet('basic') + tweet_text = self.api.html_for_tweet(tweet_object, False) # Make sure HTML doesn't contain the display OR expanded url self.assertTrue('http://google.com' not in tweet_text) self.assertTrue('google.com' not in tweet_text) def test_identical_urls(self): """If the 'url's for different url entities are identical, they should link correctly.""" - tweet_text = self.api.html_for_tweet(test_tweet_identical_urls) + tweet_object = self.load_tweet('identical_urls') + tweet_text = self.api.html_for_tweet(tweet_object) self.assertEqual(tweet_text, u'Use Cases, Trials and Making 5G a Reality buff.ly/2sEhrgO #5G #innovation via @5GWorldSeries buff.ly/2sEhrgO') def test_symbols(self): - tweet_text = self.api.html_for_tweet(test_tweet_symbols_object) + tweet_object = self.load_tweet('symbols') + tweet_text = self.api.html_for_tweet(tweet_object) # Should only link symbols listed in entities: self.assertTrue('$AAPL' in tweet_text) self.assertTrue('$ANOTHER' not in tweet_text) def test_no_symbols(self): """Should still work if tweet object has no symbols list""" - tweet = test_tweet_symbols_object + tweet = self.load_tweet('symbols') # Save a copy: symbols = tweet['entities']['symbols'] del tweet['entities']['symbols'] tweet_text = self.api.html_for_tweet(tweet) self.assertTrue('symbols: $AAPL and' in tweet_text) self.assertTrue('and $ANOTHER and $A.' in tweet_text) - # Put the symbols back: - test_tweet_symbols_object['entities']['symbols'] = symbols def test_compatmode(self): - tweet_text = self.api.html_for_tweet(test_tweet_compat_object) + tweet_object = self.load_tweet('compat') + tweet_text = self.api.html_for_tweet(tweet_object) # link to compat web status link self.assertTrue( u'twitter.com/i/web/status/7…' in tweet_text) def test_extendedmode(self): - tweet_text = self.api.html_for_tweet(test_tweet_extended_object) + tweet_object = self.load_tweet('extended') + tweet_text = self.api.html_for_tweet(tweet_object) # full tweet rendered with suffix - self.assertEqual(test_tweet_extended_html, tweet_text) + self.assertEqual(tweet_text, + 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC') def test_media(self): - tweet_text = self.api.html_for_tweet(test_tweet_media) + tweet_object = self.load_tweet('media') + tweet_text = self.api.html_for_tweet(tweet_object) self.assertEqual( - """I made some D3.js charts showing the years covered by books in a series compared to their publishing dates gyford.com/phil/writing/2… pic.twitter.com/OwNc6uJklg""", + u"""I made some D3.js charts showing the years covered by books in a series compared to their publishing dates gyford.com/phil/writing/2\u2026 pic.twitter.com/OwNc6uJklg""", tweet_text) def test_quoted(self): "With expand_quoted_status=True it should include a quoted tweet." - tweet_text = self.api.html_for_tweet(test_tweet_quoted, + tweet_object = self.load_tweet('quoted') + tweet_text = self.api.html_for_tweet(tweet_object, expand_quoted_status=True) - self.assertEqual( u"""Here\u2019s a quoted tweet. twitter.com/philgyford/sta\u2026
The quoted tweet text.Phil Gyford@philgyford
""", tweet_text) def test_retweet(self): "With expand_quoted_status=True it should include a quoted tweet." - tweet_text = self.api.html_for_tweet(test_tweet_retweet) + tweet_object = self.load_tweet('retweet') + tweet_text = self.api.html_for_tweet(tweet_object) self.assertEqual( u"""My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.""", diff --git a/tests/tweets/basic.json b/tests/tweets/basic.json new file mode 100644 index 0000000..7243cd8 --- /dev/null +++ b/tests/tweets/basic.json @@ -0,0 +1,170 @@ +{ + "contributors":null, + "truncated":false, + "text":"http://t.co/FCmXyI6VHd is a #cool site, lol! @mikehelmick shd #checkitout. Love, @__twython__ https://t.co/67pwRvY6z9 http://t.co/N6InAO4B71", + "in_reply_to_status_id":null, + "id":349683012054683648, + "favorite_count":0, + "source":"Twitter Web Client", + "retweeted":false, + "coordinates":null, + "entities":{ + "symbols":[ + + ], + "user_mentions":[ + { + "id":29251354, + "indices":[ + 45, + 57 + ], + "id_str":"29251354", + "screen_name":"mikehelmick", + "name":"Mike Helmick" + }, + { + "id":1431865928, + "indices":[ + 81, + 93 + ], + "id_str":"1431865928", + "screen_name":"__twython__", + "name":"Twython" + } + ], + "hashtags":[ + { + "indices":[ + 28, + 33 + ], + "text":"cool" + }, + { + "indices":[ + 62, + 73 + ], + "text":"checkitout" + } + ], + "urls":[ + { + "url":"http://t.co/FCmXyI6VHd", + "indices":[ + 0, + 22 + ], + "expanded_url":"http://google.com", + "display_url":"google.com" + }, + { + "url":"https://t.co/67pwRvY6z9", + "indices":[ + 94, + 117 + ], + "expanded_url":"https://github.com", + "display_url":"github.com" + } + ], + "media":[ + { + "id":537884378513162240, + "id_str":"537884378513162240", + "indices":[ + 118, + 140 + ], + "media_url":"http://pbs.twimg.com/media/B3by_g-CQAAhrO5.jpg", + "media_url_https":"https://pbs.twimg.com/media/B3by_g-CQAAhrO5.jpg", + "url":"http://t.co/N6InAO4B71", + "display_url":"pic.twitter.com/N6InAO4B71", + "expanded_url":"http://twitter.com/pingofglitch/status/537884380060844032/photo/1", + "type":"photo", + "sizes":{ + "large":{ + "w":1024, + "h":640, + "resize":"fit" + }, + "thumb":{ + "w":150, + "h":150, + "resize":"crop" + }, + "medium":{ + "w":600, + "h":375, + "resize":"fit" + }, + "small":{ + "w":340, + "h":212, + "resize":"fit" + } + } + } + ] + }, + "in_reply_to_screen_name":null, + "id_str":"349683012054683648", + "retweet_count":0, + "in_reply_to_user_id":null, + "favorited":false, + "user":{ + "follow_request_sent":false, + "profile_use_background_image":true, + "default_profile_image":true, + "id":1431865928, + "verified":false, + "profile_text_color":"333333", + "profile_image_url_https":"https://si0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png", + "profile_sidebar_fill_color":"DDEEF6", + "entities":{ + "description":{ + "urls":[ + + ] + } + }, + "followers_count":1, + "profile_sidebar_border_color":"C0DEED", + "id_str":"1431865928", + "profile_background_color":"3D3D3D", + "listed_count":0, + "profile_background_image_url_https":"https://si0.twimg.com/images/themes/theme1/bg.png", + "utc_offset":null, + "statuses_count":2, + "description":"", + "friends_count":1, + "location":"", + "profile_link_color":"0084B4", + "profile_image_url":"http://a0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png", + "following":false, + "geo_enabled":false, + "profile_background_image_url":"http://a0.twimg.com/images/themes/theme1/bg.png", + "screen_name":"__twython__", + "lang":"en", + "profile_background_tile":false, + "favourites_count":0, + "name":"Twython", + "notifications":false, + "url":null, + "created_at":"Thu May 16 01:11:09 +0000 2013", + "contributors_enabled":false, + "time_zone":null, + "protected":false, + "default_profile":false, + "is_translator":false + }, + "geo":null, + "in_reply_to_user_id_str":null, + "possibly_sensitive":false, + "lang":"en", + "created_at":"Wed Jun 26 00:18:21 +0000 2013", + "in_reply_to_status_id_str":null, + "place":null +} diff --git a/tests/tweets/compat.json b/tests/tweets/compat.json new file mode 100644 index 0000000..b01b358 --- /dev/null +++ b/tests/tweets/compat.json @@ -0,0 +1,51 @@ +{ + "contributors":null, + "truncated":true, + "text":"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count tow\u2026 https://t.co/SRmsuks2ru", + "is_quote_status":false, + "in_reply_to_status_id":null, + "id":777915304261193728, + "favorite_count":13856, + "source":"Twitter Web Client", + "retweeted":false, + "coordinates":null, + "entities":{ + "symbols":[ + + ], + "user_mentions":[ + + ], + "hashtags":[ + + ], + "urls":[ + { + "url":"https://t.co/SRmsuks2ru", + "indices":[ + 117, + 140 + ], + "expanded_url":"https://twitter.com/i/web/status/777915304261193728", + "display_url":"twitter.com/i/web/status/7\u2026" + } + ] + }, + "in_reply_to_screen_name":null, + "id_str":"777915304261193728", + "retweet_count":14767, + "in_reply_to_user_id":null, + "favorited":false, + "user":{ + "name":"Twitter", + "screen_name":"twitter" + }, + "geo":null, + "in_reply_to_user_id_str":null, + "possibly_sensitive":false, + "possibly_sensitive_appealable":false, + "lang":"en", + "created_at":"Mon Sep 19 17:00:36 +0000 2016", + "in_reply_to_status_id_str":null, + "place":null +} diff --git a/tests/tweets/extended.json b/tests/tweets/extended.json new file mode 100644 index 0000000..105700a --- /dev/null +++ b/tests/tweets/extended.json @@ -0,0 +1,204 @@ +{ + "full_text":"Say more about what's happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. https://t.co/I9pUC0NdZC", + "truncated":false, + "is_quote_status":false, + "in_reply_to_status_id":null, + "id":777915304261193728, + "favorite_count":13856, + "contributors":null, + "source":"Twitter Web Client", + "retweeted":false, + "coordinates":null, + "entities":{ + "symbols":[ + + ], + "user_mentions":[ + + ], + "hashtags":[ + + ], + "urls":[ + + ], + "media":[ + { + "expanded_url":"https://twitter.com/twitter/status/777915304261193728/photo/1", + "sizes":{ + "small":{ + "h":340, + "w":340, + "resize":"fit" + }, + "large":{ + "h":700, + "w":700, + "resize":"fit" + }, + "medium":{ + "h":600, + "w":600, + "resize":"fit" + }, + "thumb":{ + "h":150, + "w":150, + "resize":"crop" + } + }, + "url":"https://t.co/I9pUC0NdZC", + "media_url_https":"https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg", + "id_str":"777914712382058496", + "indices":[ + 140, + 163 + ], + "media_url":"http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg", + "type":"photo", + "id":777914712382058496, + "display_url":"pic.twitter.com/I9pUC0NdZC" + } + ] + }, + "in_reply_to_screen_name":null, + "id_str":"777915304261193728", + "display_text_range":[ + 0, + 139 + ], + "retweet_count":14767, + "in_reply_to_user_id":null, + "favorited":false, + "user":{ + "follow_request_sent":false, + "has_extended_profile":false, + "profile_use_background_image":true, + "id":783214, + "verified":true, + "profile_text_color":"333333", + "profile_image_url_https":"https://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg", + "profile_sidebar_fill_color":"F6F6F6", + "is_translator":false, + "geo_enabled":true, + "entities":{ + "url":{ + "urls":[ + { + "url":"http://t.co/5iRhy7wTgu", + "indices":[ + 0, + 22 + ], + "expanded_url":"http://blog.twitter.com/", + "display_url":"blog.twitter.com" + } + ] + }, + "description":{ + "urls":[ + { + "url":"https://t.co/qq1HEzvnrA", + "indices":[ + 84, + 107 + ], + "expanded_url":"http://support.twitter.com", + "display_url":"support.twitter.com" + } + ] + } + }, + "followers_count":56827498, + "protected":false, + "location":"San Francisco, CA", + "default_profile_image":false, + "id_str":"783214", + "lang":"en", + "utc_offset":-25200, + "statuses_count":3161, + "description":"Your official source for news, updates and tips from Twitter, Inc. Need help? Visit https://t.co/qq1HEzvnrA.", + "friends_count":145, + "profile_link_color":"226699", + "profile_image_url":"http://pbs.twimg.com/profile_images/767879603977191425/29zfZY6I_normal.jpg", + "notifications":false, + "profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png", + "profile_background_color":"ACDED6", + "profile_banner_url":"https://pbs.twimg.com/profile_banners/783214/1471929200", + "profile_background_image_url":"http://pbs.twimg.com/profile_background_images/657090062/l1uqey5sy82r9ijhke1i.png", + "name":"Twitter", + "is_translation_enabled":false, + "profile_background_tile":true, + "favourites_count":2332, + "screen_name":"twitter", + "url":"http://t.co/5iRhy7wTgu", + "created_at":"Tue Feb 20 14:35:54 +0000 2007", + "contributors_enabled":false, + "time_zone":"Pacific Time (US & Canada)", + "profile_sidebar_border_color":"FFFFFF", + "default_profile":false, + "following":false, + "listed_count":90445 + }, + "geo":null, + "in_reply_to_user_id_str":null, + "possibly_sensitive":false, + "possibly_sensitive_appealable":false, + "lang":"en", + "created_at":"Mon Sep 19 17:00:36 +0000 2016", + "in_reply_to_status_id_str":null, + "place":null, + "extended_entities":{ + "media":[ + { + "expanded_url":"https://twitter.com/twitter/status/777915304261193728/photo/1", + "display_url":"pic.twitter.com/I9pUC0NdZC", + "url":"https://t.co/I9pUC0NdZC", + "media_url_https":"https://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg", + "video_info":{ + "aspect_ratio":[ + 1, + 1 + ], + "variants":[ + { + "url":"https://pbs.twimg.com/tweet_video/Csu1TzEVMAAAEv7.mp4", + "bitrate":0, + "content_type":"video/mp4" + } + ] + }, + "id_str":"777914712382058496", + "sizes":{ + "small":{ + "h":340, + "w":340, + "resize":"fit" + }, + "large":{ + "h":700, + "w":700, + "resize":"fit" + }, + "medium":{ + "h":600, + "w":600, + "resize":"fit" + }, + "thumb":{ + "h":150, + "w":150, + "resize":"crop" + } + }, + "indices":[ + 140, + 163 + ], + "type":"animated_gif", + "id":777914712382058496, + "media_url":"http://pbs.twimg.com/tweet_video_thumb/Csu1TzEVMAAAEv7.jpg" + } + ] + } +} diff --git a/tests/tweets/identical_urls.json b/tests/tweets/identical_urls.json new file mode 100644 index 0000000..69840f9 --- /dev/null +++ b/tests/tweets/identical_urls.json @@ -0,0 +1,34 @@ +{ + "entities":{ + "hashtags":[ + + ], + "user_mentions":[ + + ], + "symbols":[ + + ], + "urls":[ + { + "display_url":"buff.ly/2sEhrgO", + "expanded_url":"http://buff.ly/2sEhrgO", + "indices":[ + 42, + 65 + ], + "url":"https://t.co/W0uArTMk9N" + }, + { + "display_url":"buff.ly/2sEhrgO", + "expanded_url":"http://buff.ly/2sEhrgO", + "indices":[ + 101, + 124 + ], + "url":"https://t.co/W0uArTMk9N" + } + ] + }, + "full_text":"Use Cases, Trials and Making 5G a Reality https://t.co/W0uArTMk9N #5G #innovation via @5GWorldSeries https://t.co/W0uArTMk9N" +} diff --git a/tests/tweets/media.json b/tests/tweets/media.json new file mode 100644 index 0000000..4366794 --- /dev/null +++ b/tests/tweets/media.json @@ -0,0 +1,205 @@ +{ + "contributors":null, + "truncated":false, + "is_quote_status":false, + "in_reply_to_status_id":null, + "id":905105588279013377, + "favorite_count":10, + "full_text":"I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg", + "source":"Twitter Web Client", + "retweeted":false, + "coordinates":null, + "entities":{ + "symbols":[ + + ], + "user_mentions":[ + + ], + "hashtags":[ + + ], + "urls":[ + { + "url":"https://t.co/2yUmmn3TOc", + "indices":[ + 107, + 130 + ], + "expanded_url":"http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php", + "display_url":"gyford.com/phil/writing/2\u2026" + } + ], + "media":[ + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "display_url":"pic.twitter.com/OwNc6uJklg", + "url":"https://t.co/OwNc6uJklg", + "media_url_https":"https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg", + "id_str":"905105571765944320", + "sizes":{ + "small":{ + "h":256, + "resize":"fit", + "w":680 + }, + "large":{ + "h":376, + "resize":"fit", + "w":1000 + }, + "medium":{ + "h":376, + "resize":"fit", + "w":1000 + }, + "thumb":{ + "h":150, + "resize":"crop", + "w":150 + } + }, + "indices":[ + 131, + 154 + ], + "type":"photo", + "id":905105571765944320, + "media_url":"http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg" + } + ] + }, + "in_reply_to_screen_name":null, + "in_reply_to_user_id":null, + "display_text_range":[ + 0, + 130 + ], + "retweet_count":1, + "id_str":"905105588279013377", + "favorited":false, + "user":{ + "screen_name":"philgyford", + "name":"Phil Gyford" + }, + "geo":null, + "in_reply_to_user_id_str":null, + "possibly_sensitive":false, + "possibly_sensitive_appealable":false, + "lang":"en", + "created_at":"Tue Sep 05 16:29:22 +0000 2017", + "in_reply_to_status_id_str":null, + "place":null, + "extended_entities":{ + "media":[ + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "display_url":"pic.twitter.com/OwNc6uJklg", + "url":"https://t.co/OwNc6uJklg", + "media_url_https":"https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg", + "id_str":"905105571765944320", + "sizes":{ + "small":{ + "h":256, + "resize":"fit", + "w":680 + }, + "large":{ + "h":376, + "resize":"fit", + "w":1000 + }, + "medium":{ + "h":376, + "resize":"fit", + "w":1000 + }, + "thumb":{ + "h":150, + "resize":"crop", + "w":150 + } + }, + "indices":[ + 131, + 154 + ], + "type":"photo", + "id":905105571765944320, + "media_url":"http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg" + }, + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "display_url":"pic.twitter.com/OwNc6uJklg", + "url":"https://t.co/OwNc6uJklg", + "media_url_https":"https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg", + "id_str":"905105572529393668", + "sizes":{ + "large":{ + "h":399, + "resize":"fit", + "w":1000 + }, + "small":{ + "h":271, + "resize":"fit", + "w":680 + }, + "medium":{ + "h":399, + "resize":"fit", + "w":1000 + }, + "thumb":{ + "h":150, + "resize":"crop", + "w":150 + } + }, + "indices":[ + 131, + 154 + ], + "type":"photo", + "id":905105572529393668, + "media_url":"http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg" + }, + { + "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", + "display_url":"pic.twitter.com/OwNc6uJklg", + "url":"https://t.co/OwNc6uJklg", + "media_url_https":"https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg", + "id_str":"905105573255016448", + "sizes":{ + "small":{ + "h":195, + "resize":"fit", + "w":680 + }, + "large":{ + "h":287, + "resize":"fit", + "w":1000 + }, + "medium":{ + "h":287, + "resize":"fit", + "w":1000 + }, + "thumb":{ + "h":150, + "resize":"crop", + "w":150 + } + }, + "indices":[ + 131, + 154 + ], + "type":"photo", + "id":905105573255016448, + "media_url":"http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg" + } + ] + } +} diff --git a/tests/tweets/quoted.json b/tests/tweets/quoted.json new file mode 100644 index 0000000..c8e372b --- /dev/null +++ b/tests/tweets/quoted.json @@ -0,0 +1,94 @@ +{ + "contributors":null, + "truncated":false, + "text":"Here\u2019s a quoted tweet. https://t.co/3neKzof0gT", + "is_quote_status":true, + "in_reply_to_status_id":null, + "id":917706313785905157, + "favorite_count":0, + "source":"Twitter Web Client", + "quoted_status_id":917699069916729344, + "retweeted":false, + "coordinates":null, + "quoted_status":{ + "contributors":null, + "truncated":false, + "text":"The quoted tweet text.", + "is_quote_status":false, + "in_reply_to_status_id":null, + "id":917699069916729344, + "favorite_count":1, + "source":"Twitter Web Client", + "retweeted":false, + "coordinates":null, + "entities":{ + "symbols":[ + + ], + "user_mentions":[ + + ], + "hashtags":[ + + ], + "urls":[ + + ] + }, + "in_reply_to_screen_name":null, + "in_reply_to_user_id":null, + "retweet_count":0, + "id_str":"917699069916729344", + "favorited":false, + "user":{ + "screen_name":"philgyford", + "name":"Phil Gyford" + }, + "geo":null, + "in_reply_to_user_id_str":null, + "lang":"ht", + "created_at":"Tue Oct 10 10:31:22 +0000 2017", + "in_reply_to_status_id_str":null, + "place":null + }, + "entities":{ + "symbols":[ + + ], + "user_mentions":[ + + ], + "hashtags":[ + + ], + "urls":[ + { + "url":"https://t.co/3neKzof0gT", + "indices":[ + 23, + 46 + ], + "expanded_url":"https://twitter.com/philgyford/status/917699069916729344", + "display_url":"twitter.com/philgyford/sta\u2026" + } + ] + }, + "in_reply_to_screen_name":null, + "in_reply_to_user_id":null, + "retweet_count":0, + "id_str":"917706313785905157", + "favorited":false, + "user":{ + "screen_name":"philgyfordtest", + "name":"Phil Gyford Test" + }, + "geo":null, + "in_reply_to_user_id_str":null, + "possibly_sensitive":false, + "possibly_sensitive_appealable":false, + "lang":"en", + "created_at":"Tue Oct 10 11:00:10 +0000 2017", + "quoted_status_id_str":"917699069916729344", + "in_reply_to_status_id_str":null, + "place":null +} diff --git a/tests/tweets/reply.json b/tests/tweets/reply.json new file mode 100644 index 0000000..2a22712 --- /dev/null +++ b/tests/tweets/reply.json @@ -0,0 +1,62 @@ +{ + "display_text_range":[ + 12, + 114 + ], + "in_reply_to_status_id_str":"742374355531923456", + "source":"Twitter Web Client", + "geo":null, + "full_text":"@philgyford Here\u2019s a test tweet that goes on as much as possible and includes an image. Hi to my fans in testland! https://t.co/tzhyk2QWSr", + "extended_entities":{ + "media":[ + + ] + }, + "id_str":"300", + "in_reply_to_status_id":742374355531923456, + "id":300, + "in_reply_to_screen_name":"philgyford", + "retweet_count":0, + "user":{ + + }, + "created_at":"Mon Jun 13 15:48:06 +0000 2016", + "lang":"en", + "favorite_count":0, + "coordinates":null, + "place":null, + "contributors":null, + "in_reply_to_user_id":12552, + "in_reply_to_user_id_str":"12552", + "retweeted":false, + "favorited":false, + "truncated":false, + "entities":{ + "user_mentions":[ + { + "id_str":"12552", + "id":12552, + "screen_name":"philgyford", + "name":"Phil Gyford", + "indices":[ + 0, + 11 + ] + } + ], + "media":[ + + ], + "hashtags":[ + + ], + "symbols":[ + + ], + "urls":[ + + ] + }, + "is_quote_status":false, + "possibly_sensitive":false +} diff --git a/tests/tweets/retweet.json b/tests/tweets/retweet.json new file mode 100644 index 0000000..046bb64 --- /dev/null +++ b/tests/tweets/retweet.json @@ -0,0 +1,91 @@ +{ + "coordinates":null, + "source":"web", + "in_reply_to_user_id_str":null, + "truncated":false, + "in_reply_to_user_id":null, + "favorite_count":0, + "user":{ + "name":"Phil Gyford Test", + "screen_name":"philgyfordtest" + }, + "favorited":false, + "retweeted_status":{ + "coordinates":null, + "source":"Twitter Web Client", + "in_reply_to_user_id_str":null, + "truncated":false, + "in_reply_to_user_id":null, + "favorite_count":21, + "user":{ + "name":"Samuel Pepys", + "screen_name":"samuelpepys" + }, + "favorited":false, + "id":917459832885653506, + "contributors":null, + "in_reply_to_screen_name":null, + "geo":null, + "in_reply_to_status_id_str":null, + "id_str":"917459832885653506", + "entities":{ + "hashtags":[ + + ], + "symbols":[ + + ], + "user_mentions":[ + + ], + "urls":[ + + ] + }, + "in_reply_to_status_id":null, + "text":"My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.", + "retweet_count":3, + "place":null, + "lang":"en", + "created_at":"Mon Oct 09 18:40:44 +0000 2017", + "is_quote_status":false, + "retweeted":false + }, + "id":917712989649801216, + "contributors":null, + "in_reply_to_screen_name":null, + "geo":null, + "in_reply_to_status_id_str":null, + "id_str":"917712989649801216", + "entities":{ + "hashtags":[ + + ], + "symbols":[ + + ], + "user_mentions":[ + { + "id_str":"14475268", + "indices":[ + 3, + 15 + ], + "name":"Samuel Pepys", + "id":14475268, + "screen_name":"samuelpepys" + } + ], + "urls":[ + + ] + }, + "in_reply_to_status_id":null, + "text":"RT @samuelpepys: My aunt and uncle in a very ill humour one with another, but I made shift with much ado to keep them from scolding.", + "retweet_count":3, + "place":null, + "lang":"en", + "created_at":"Tue Oct 10 11:26:41 +0000 2017", + "is_quote_status":false, + "retweeted":false +} diff --git a/tests/tweets/symbols.json b/tests/tweets/symbols.json new file mode 100644 index 0000000..7519a08 --- /dev/null +++ b/tests/tweets/symbols.json @@ -0,0 +1,61 @@ +{ + "text":"Some symbols: $AAPL and $PEP and $ANOTHER and $A.", + "contributors":null, + "geo":null, + "favorited":true, + "in_reply_to_user_id_str":null, + "user":{ + "screen_name":"philgyfordtest", + "name":"Phil Gyford Test" + }, + "in_reply_to_user_id":null, + "retweeted":false, + "coordinates":null, + "place":null, + "in_reply_to_status_id":null, + "lang":"en", + "in_reply_to_status_id_str":null, + "truncated":false, + "retweet_count":0, + "is_quote_status":false, + "id":662694880657989632, + "id_str":"662694880657989632", + "in_reply_to_screen_name":null, + "favorite_count":1, + "entities":{ + "hashtags":[ + + ], + "user_mentions":[ + + ], + "symbols":[ + { + "indices":[ + 14, + 19 + ], + "text":"AAPL" + }, + { + "indices":[ + 24, + 28 + ], + "text":"PEP" + }, + { + "indices":[ + 46, + 48 + ], + "text":"A" + } + ], + "urls":[ + + ] + }, + "created_at":"Fri Nov 06 18:15:46 +0000 2015", + "source":"Twitter Web Client" +} -- 2.39.5 From d3f5361f4d58b693f013eeb07dd12b698cae5a58 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Wed, 11 Oct 2017 18:40:55 +0100 Subject: [PATCH 167/204] Try to fix loading of JSON files in tests on Travis --- tests/test_html_for_tweet.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index cc49121..7331fa4 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import json +import os from twython import Twython, TwythonError @@ -11,7 +12,11 @@ class TestHtmlForTweetTestCase(unittest.TestCase): self.api = Twython('', '', '', '') def load_tweet(self, name): - f = open('tests/tweets/%s.json' % name) + f = open(os.path.join( + os.path.dirname(__file__), + 'tweets', + '%s.json' % name + )) tweet = json.load(f) f.close() return tweet -- 2.39.5 From f3088b02894053485e0f3a6c46fa468032c99f43 Mon Sep 17 00:00:00 2001 From: Clayton A Davis Date: Wed, 11 Oct 2017 13:58:25 -0400 Subject: [PATCH 168/204] Update links to Twitter docs --- README.rst | 10 +++++----- docs/usage/basic_usage.rst | 2 +- twython/streaming/api.py | 6 +++--- twython/streaming/types.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 061d1b7..edddacc 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ Features - Twitter lists - Timelines - Direct Messages - - and anything found in `the docs `_ + - and anything found in `the docs `_ - Image Uploading: - Update user status with an image - Change user avatar @@ -67,7 +67,7 @@ Documentation is available at https://twython.readthedocs.io/en/latest/ Starting Out ------------ -First, you'll want to head over to https://dev.twitter.com/apps and register an application! +First, you'll want to head over to https://apps.twitter.com and register an application! After you register, grab your applications ``Consumer Key`` and ``Consumer Secret`` from the application details tab. @@ -165,7 +165,7 @@ Create a Twython instance with your application keys and the users OAuth tokens Authenticated Users Home Timeline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Documentation: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline +Documentation: https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline .. code-block:: python @@ -176,7 +176,7 @@ Updating Status This method makes use of dynamic arguments, `read more about them `_ -Documentation: https://dev.twitter.com/docs/api/1/post/statuses/update +Documentation: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update .. code-block:: python @@ -185,7 +185,7 @@ Documentation: https://dev.twitter.com/docs/api/1/post/statuses/update Searching ~~~~~~~~~ - https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes "q" and "result_type" amongst other arguments + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets says it takes "q" and "result_type" amongst other arguments .. code-block:: python diff --git a/docs/usage/basic_usage.rst b/docs/usage/basic_usage.rst index d7e32e3..8fee807 100644 --- a/docs/usage/basic_usage.rst +++ b/docs/usage/basic_usage.rst @@ -80,7 +80,7 @@ Documentation: https://developer.twitter.com/en/docs/tweets/search/api-reference .. _dynamicargexplaination: -.. important:: To help explain :ref:`dynamic function arguments ` a little more, you can see that the previous call used the keyword argument ``q``, that is because Twitter specifies in their `search documentation `_ that the search call accepts the parameter "q". You can pass mutiple keyword arguments. The search documentation also specifies that the call accepts the parameter "result_type" +.. important:: To help explain :ref:`dynamic function arguments ` a little more, you can see that the previous call used the keyword argument ``q``, that is because Twitter specifies in their `search documentation `_ that the search call accepts the parameter "q". You can pass mutiple keyword arguments. The search documentation also specifies that the call accepts the parameter "result_type" .. code-block:: python diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 47678e4..0195f38 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -169,9 +169,9 @@ class TwythonStreamer(object): Returns True if other handlers for this message should be invoked. Feel free to override this to handle your streaming data how you - want it handled. - See https://dev.twitter.com/docs/streaming-apis/messages for messages - sent along in stream responses. + want it handled. See + https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types + for messages sent along in stream responses. :param data: data recieved from the stream :type data: dict diff --git a/twython/streaming/types.py b/twython/streaming/types.py index aa6b9ad..8755a8d 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -59,7 +59,7 @@ class TwythonStreamerTypesStatuses(object): :param \*\*params: Parameters to send with your stream request Accepted params found at: - https://dev.twitter.com/docs/api/1.1/post/statuses/filter + https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter """ url = 'https://stream.twitter.com/%s/statuses/filter.json' \ % self.streamer.api_version @@ -71,7 +71,7 @@ class TwythonStreamerTypesStatuses(object): :param \*\*params: Parameters to send with your stream request Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/statuses/sample + https://developer.twitter.com/en/docs/tweets/sample-realtime/api-reference/get-statuses-sample """ url = 'https://stream.twitter.com/%s/statuses/sample.json' \ % self.streamer.api_version @@ -95,7 +95,7 @@ class TwythonStreamerTypesStatuses(object): :param \*\*params: Parameters to send with your stream request Accepted params found at: - https://dev.twitter.com/docs/api/1.1/post/statuses/filter + https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter """ self.params = params @@ -104,4 +104,4 @@ class TwythonStreamerTypesStatuses(object): url = 'https://stream.twitter.com/%s/statuses/filter.json' \ % self.streamer.api_version - self.streamer._request(url, 'POST', params=self.params) + self.streamer._request(url, 'POST', params=self.params) -- 2.39.5 From 8f3db4bc8521e8cd0e567b54ac861af5c88a7703 Mon Sep 17 00:00:00 2001 From: Clayton A Davis Date: Wed, 11 Oct 2017 17:51:40 -0400 Subject: [PATCH 169/204] Update endpoint docstrings to match new Twitter docs --- twython/endpoints.py | 296 ++++++++++++++++++++++++++++--------------- 1 file changed, 193 insertions(+), 103 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 1c1d80c..f3e4528 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -10,8 +10,8 @@ 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 +Official documentation for Twitter API endpoints can be found at: +https://developer.twitter.com/en/docs/api-reference-index """ import json @@ -34,7 +34,7 @@ class EndpointsMixin(object): @screen_name) for the authenticating user. Docs: - https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline + https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-mentions_timeline """ return self.get('statuses/mentions_timeline', params=params) @@ -44,7 +44,8 @@ class EndpointsMixin(object): """Returns a collection of the most recent Tweets posted by the user indicated by the screen_name or user_id parameters. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline + Docs: + https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline """ return self.get('statuses/user_timeline', params=params) @@ -54,7 +55,8 @@ class EndpointsMixin(object): """Returns a collection of the most recent Tweets and retweets posted by the authenticating user and the users they follow. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline + Docs: + https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline """ return self.get('statuses/home_timeline', params=params) @@ -64,7 +66,8 @@ class EndpointsMixin(object): """Returns the most recent tweets authored by the authenticating user that have been retweeted by others. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets_of_me + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweets_of_me """ return self.get('statuses/retweets_of_me', params=params) @@ -74,7 +77,8 @@ class EndpointsMixin(object): def get_retweets(self, **params): """Returns up to 100 of the first retweets of a given tweet. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id """ return self.get('statuses/retweets/%s' % params.get('id'), @@ -83,7 +87,8 @@ class EndpointsMixin(object): def show_status(self, **params): """Returns a single Tweet, specified by the id parameter - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id """ return self.get('statuses/show/%s' % params.get('id'), params=params) @@ -93,7 +98,8 @@ class EndpointsMixin(object): request, as specified by comma-separated values passed to the id parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/lookup + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-lookup """ return self.post('statuses/lookup', params=params) @@ -101,7 +107,8 @@ class EndpointsMixin(object): def destroy_status(self, **params): """Destroys the status specified by the required ID parameter - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/destroy/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id """ return self.post('statuses/destroy/%s' % params.get('id')) @@ -109,7 +116,8 @@ class EndpointsMixin(object): def update_status(self, **params): """Updates the authenticating user's current status, also known as tweeting - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/update + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update """ return self.post('statuses/update', params=params) @@ -117,7 +125,8 @@ class EndpointsMixin(object): def retweet(self, **params): """Retweets a tweet specified by the id parameter - Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/%3Aid + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id """ return self.post('statuses/retweet/%s' % params.get('id')) @@ -127,7 +136,7 @@ class EndpointsMixin(object): for upload. In other words, it creates a Tweet with a picture attached. Docs: - https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update_with_media """ warnings.warn( @@ -143,9 +152,10 @@ class EndpointsMixin(object): to the 'update_status' method using the 'media_ids' param. Docs: - https://dev.twitter.com/rest/reference/post/media/upload + https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload + """ - # https://dev.twitter.com/rest/reference/get/media/upload-status + # https://developer.twitter.com/en/docs/media/upload-media/api-reference/get-media-upload-status if params and params.get('command', '') == 'STATUS': return self.get('https://upload.twitter.com/1.1/media/upload.json', params=params) @@ -153,7 +163,10 @@ class EndpointsMixin(object): def create_metadata(self, **params): """ Adds metadata to a media element, such as image descriptions for visually impaired. - Docs: https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create + + Docs: + https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create + """ params = json.dumps(params) return self.post("https://upload.twitter.com/1.1/media/metadata/create.json", params=params) @@ -171,7 +184,8 @@ class EndpointsMixin(object): Twitter media upload api expects each chunk to be not more than 5mb. We are sending chunk of 1mb each. Docs: - https://dev.twitter.com/rest/public/uploading-media#chunkedupload + https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload + """ upload_url = 'https://upload.twitter.com/1.1/media/upload.json' if not size: @@ -216,7 +230,7 @@ class EndpointsMixin(object): response = self.post(upload_url, params=params) - # Only get the status if explicity asked to + # Only get the status if explicity asked to # Default to False if check_progress: @@ -250,7 +264,8 @@ class EndpointsMixin(object): """Returns information allowing the creation of an embedded representation of a Tweet on third party sites. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/oembed + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed """ return self.get('statuses/oembed', params=params) @@ -259,7 +274,8 @@ class EndpointsMixin(object): """Returns a collection of up to 100 user IDs belonging to users who have retweeted the tweet specified by the id parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweeters/ids + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweeters-ids """ return self.get('statuses/retweeters/ids', params=params) @@ -270,7 +286,8 @@ class EndpointsMixin(object): def search(self, **params): """Returns a collection of relevant Tweets matching a specified query. - Docs: https://dev.twitter.com/docs/api/1.1/get/search/tweets + Docs: + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets """ return self.get('search/tweets', params=params) @@ -282,7 +299,8 @@ class EndpointsMixin(object): def get_direct_messages(self, **params): """Returns the 20 most recent direct messages sent to the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-messages """ return self.get('direct_messages', params=params) @@ -291,7 +309,8 @@ class EndpointsMixin(object): def get_sent_messages(self, **params): """Returns the 20 most recent direct messages sent by the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages/sent + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-sent-message """ return self.get('direct_messages/sent', params=params) @@ -300,7 +319,8 @@ class EndpointsMixin(object): def get_direct_message(self, **params): """Returns a single direct message, specified by an id parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages/show + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-message """ return self.get('direct_messages/show', params=params) @@ -308,7 +328,8 @@ class EndpointsMixin(object): def destroy_direct_message(self, **params): """Destroys the direct message specified in the required id parameter - Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/destroy + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message """ return self.post('direct_messages/destroy', params=params) @@ -317,7 +338,8 @@ class EndpointsMixin(object): """Sends a new direct message to the specified user from the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/new + Docs: + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-message """ return self.post('direct_messages/new', params=params) @@ -328,7 +350,7 @@ class EndpointsMixin(object): user does not want to receive retweets from. Docs: - https://dev.twitter.com/docs/api/1.1/get/friendships/no_retweets/ids + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-no_retweets-ids """ return self.get('friendships/no_retweets/ids', params=params) @@ -337,7 +359,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user IDs for every user the specified user is following (otherwise known as their "friends"). - Docs: https://dev.twitter.com/docs/api/1.1/get/friends/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-ids """ return self.get('friends/ids', params=params) @@ -348,7 +371,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user IDs for every user following the specified user. - Docs: https://dev.twitter.com/docs/api/1.1/get/followers/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-ids """ return self.get('followers/ids', params=params) @@ -359,7 +383,8 @@ class EndpointsMixin(object): """Returns the relationships of the authenticating user to the comma-separated list of up to 100 screen_names or user_ids provided. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/lookup + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-lookup """ return self.get('friendships/lookup', params=params) @@ -368,7 +393,8 @@ class EndpointsMixin(object): """Returns a collection of numeric IDs for every user who has a pending request to follow the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/incoming + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-incoming """ return self.get('friendships/incoming', params=params) @@ -379,7 +405,8 @@ class EndpointsMixin(object): """Returns a collection of numeric IDs for every protected user for whom the authenticating user has a pending follow request. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/outgoing + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-outgoing """ return self.get('friendships/outgoing', params=params) @@ -390,7 +417,8 @@ class EndpointsMixin(object): """Allows the authenticating users to follow the user specified in the ID parameter. - Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-create """ return self.post('friendships/create', params=params) @@ -399,7 +427,8 @@ class EndpointsMixin(object): """Allows the authenticating user to unfollow the user specified in the ID parameter. - Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy """ return self.post('friendships/destroy', params=params) @@ -408,7 +437,8 @@ class EndpointsMixin(object): """Allows one to enable or disable retweets and device notifications from the specified user. - Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/update + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-update """ return self.post('friendships/update', params=params) @@ -417,7 +447,8 @@ class EndpointsMixin(object): """Returns detailed information about the relationship between two arbitrary users. - Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-show """ return self.get('friendships/show', params=params) @@ -426,7 +457,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user objects for every user the specified user is following (otherwise known as their "friends"). - Docs: https://dev.twitter.com/docs/api/1.1/get/friends/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friends-list """ return self.get('friends/list', params=params) @@ -437,7 +469,8 @@ class EndpointsMixin(object): """Returns a cursored collection of user objects for users following the specified user. - Docs: https://dev.twitter.com/docs/api/1.1/get/followers/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-followers-list """ return self.get('followers/list', params=params) @@ -449,7 +482,8 @@ class EndpointsMixin(object): """Returns settings (including current trend, geo and sleep time information) for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/get/account/settings + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-settings """ return self.get('account/settings', params=params) @@ -460,7 +494,7 @@ class EndpointsMixin(object): code and an error message if not. Docs: - https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-account-verify_credentials """ return self.get('account/verify_credentials', params=params) @@ -468,7 +502,8 @@ class EndpointsMixin(object): def update_account_settings(self, **params): """Updates the authenticating user's settings. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/settings + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-settings """ return self.post('account/settings', params=params) @@ -486,7 +521,8 @@ class EndpointsMixin(object): """Sets values that users are able to set under the "Account" tab of their settings page. - Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile """ return self.post('account/update_profile', params=params) @@ -495,26 +531,35 @@ class EndpointsMixin(object): """Updates the authenticating user's profile background image. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_background_image """ return self.post('account/update_profile_banner', params=params) - def update_profile_colors(self, **params): + def update_profile_colors(self, **params): # pragma: no cover """Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + This method is deprecated, replaced by the profile_link_color + parameter to update_profile. + Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile """ + warnings.warn( + 'This method is deprecated. You should use the' + ' profile_link_color parameter in Twython.update_profile instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) return self.post('account/update_profile_colors', params=params) def update_profile_image(self, **params): # pragma: no cover """Updates the authenticating user's profile image. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image """ return self.post('account/update_profile_image', params=params) @@ -523,7 +568,8 @@ class EndpointsMixin(object): """Returns a collection of user objects that the authenticating user is blocking. - Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-list """ return self.get('blocks/list', params=params) @@ -533,7 +579,8 @@ class EndpointsMixin(object): def list_block_ids(self, **params): """Returns an array of numeric user ids the authenticating user is blocking. - Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-blocks-ids """ return self.get('blocks/ids', params=params) @@ -543,7 +590,8 @@ class EndpointsMixin(object): def create_block(self, **params): """Blocks the specified user from following the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-create """ return self.post('blocks/create', params=params) @@ -552,7 +600,8 @@ class EndpointsMixin(object): """Un-blocks the user specified in the ID parameter for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-blocks-destroy """ return self.post('blocks/destroy', params=params) @@ -562,7 +611,8 @@ class EndpointsMixin(object): as specified by comma-separated values passed to the user_id and/or screen_name parameters. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/lookup + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup """ return self.get('users/lookup', params=params) @@ -571,7 +621,8 @@ class EndpointsMixin(object): """Returns a variety of information about the user specified by the required user_id or screen_name parameter. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show """ return self.get('users/show', params=params) @@ -580,7 +631,8 @@ class EndpointsMixin(object): """Provides a simple, relevance-based search interface to public user accounts on Twitter. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/search + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-search """ return self.get('users/search', params=params) @@ -606,7 +658,7 @@ class EndpointsMixin(object): Returns HTTP 200 upon success. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/remove_profile_banner + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-remove_profile_banner """ return self.post('account/remove_profile_banner', params=params) @@ -615,7 +667,7 @@ class EndpointsMixin(object): """Uploads a profile banner on behalf of the authenticating user. Docs: - https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_banner """ return self.post('account/update_profile_background_image', @@ -625,7 +677,8 @@ class EndpointsMixin(object): """Returns a map of the available size variations of the specified user's profile banner. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/profile_banner + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/get-users-profile_banner """ return self.get('users/profile_banner', params=params) @@ -634,7 +687,8 @@ class EndpointsMixin(object): """Returns a collection of user objects that the authenticating user is muting. - Docs: https://dev.twitter.com/docs/api/1.1/get/mutes/users/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-mutes-users-list """ return self.get('mutes/users/list', params=params) @@ -645,7 +699,8 @@ class EndpointsMixin(object): """Returns an array of numeric user ids the authenticating user is muting. - Docs: https://dev.twitter.com/docs/api/1.1/get/mutes/users/ids + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/get-mutes-users-ids """ return self.get('mutes/users/ids', params=params) @@ -656,7 +711,8 @@ class EndpointsMixin(object): """Mutes the specified user, preventing their tweets appearing in the authenticating user's timeline. - Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-create """ return self.post('mutes/users/create', params=params) @@ -665,7 +721,8 @@ class EndpointsMixin(object): """Un-mutes the user specified in the user or ID parameter for the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/mutes/users/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-destroy """ return self.post('mutes/users/destroy', params=params) @@ -675,7 +732,7 @@ class EndpointsMixin(object): """Access the users in a given category of the Twitter suggested user list. Docs: - https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions-slug """ return self.get('users/suggestions/%s' % params.get('slug'), @@ -684,7 +741,8 @@ class EndpointsMixin(object): def get_user_suggestions(self, **params): """Access to Twitter's suggested user list. - Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions """ return self.get('users/suggestions', params=params) @@ -695,7 +753,7 @@ class EndpointsMixin(object): user. Docs: - https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug/members + https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-suggestions-slug-members """ return self.get('users/suggestions/%s/members' % params.get('slug'), @@ -706,7 +764,8 @@ class EndpointsMixin(object): """Returns the 20 most recent Tweets favorited by the authenticating or specified user. - Docs: https://dev.twitter.com/docs/api/1.1/get/favorites/list + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-favorites-list """ return self.get('favorites/list', params=params) @@ -716,7 +775,8 @@ class EndpointsMixin(object): """Un-favorites the status specified in the ID parameter as the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/destroy + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-favorites-destroy """ return self.post('favorites/destroy', params=params) @@ -725,7 +785,8 @@ class EndpointsMixin(object): """Favorites the status specified in the ID parameter as the authenticating user. - Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/create + Docs: + https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-favorites-create """ return self.post('favorites/create', params=params) @@ -735,7 +796,8 @@ class EndpointsMixin(object): """Returns all lists the authenticating or specified user subscribes to, including their own. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/list + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list """ return self.get('lists/list', params=params) @@ -743,7 +805,8 @@ class EndpointsMixin(object): def get_list_statuses(self, **params): """Returns a timeline of tweets authored by members of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/statuses + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-statuses """ return self.get('lists/statuses', params=params) @@ -752,7 +815,8 @@ class EndpointsMixin(object): def delete_list_member(self, **params): """Removes the specified member from the list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-destroy """ return self.post('lists/members/destroy', params=params) @@ -760,7 +824,8 @@ class EndpointsMixin(object): def get_list_memberships(self, **params): """Returns the lists the specified user has been added to. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/memberships + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-memberships """ return self.get('lists/memberships', params=params) @@ -770,7 +835,8 @@ class EndpointsMixin(object): def get_list_subscribers(self, **params): """Returns the subscribers of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscribers """ return self.get('lists/subscribers', params=params) @@ -781,7 +847,7 @@ class EndpointsMixin(object): """Subscribes the authenticated user to the specified list. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-subscribers-create """ return self.post('lists/subscribers/create', params=params) @@ -789,7 +855,8 @@ class EndpointsMixin(object): def is_list_subscriber(self, **params): """Check if the specified user is a subscriber of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscribers-show """ return self.get('lists/subscribers/show', params=params) @@ -798,7 +865,7 @@ class EndpointsMixin(object): """Unsubscribes the authenticated user from the specified list. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-subscribers-destroy """ return self.post('lists/subscribers/destroy', params=params) @@ -808,7 +875,7 @@ class EndpointsMixin(object): list of member ids or screen names. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create_all """ return self.post('lists/members/create_all', params=params) @@ -816,7 +883,8 @@ class EndpointsMixin(object): def is_list_member(self, **params): """Check if the specified user is a member of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/members/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-members-show """ return self.get('lists/members/show', params=params) @@ -824,7 +892,8 @@ class EndpointsMixin(object): def get_list_members(self, **params): """Returns the members of the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/members + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-members """ return self.get('lists/members', params=params) @@ -834,7 +903,8 @@ class EndpointsMixin(object): def add_list_member(self, **params): """Add a member to a list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-create """ return self.post('lists/members/create', params=params) @@ -842,7 +912,8 @@ class EndpointsMixin(object): def delete_list(self, **params): """Deletes the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/destroy + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-destroy """ return self.post('lists/destroy', params=params) @@ -850,7 +921,8 @@ class EndpointsMixin(object): def update_list(self, **params): """Updates the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/update + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-update """ return self.post('lists/update', params=params) @@ -858,7 +930,8 @@ class EndpointsMixin(object): def create_list(self, **params): """Creates a new list for the authenticated user. - Docs: https://dev.twitter.com/docs/api/1.1/post/lists/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-create """ return self.post('lists/create', params=params) @@ -866,7 +939,8 @@ class EndpointsMixin(object): def get_specific_list(self, **params): """Returns the specified list. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/show + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-show """ return self.get('lists/show', params=params) @@ -874,7 +948,8 @@ class EndpointsMixin(object): def get_list_subscriptions(self, **params): """Obtain a collection of the lists the specified user is subscribed to. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscriptions + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-subscriptions """ return self.get('lists/subscriptions', params=params) @@ -886,7 +961,7 @@ class EndpointsMixin(object): comma-separated list of member ids or screen names. Docs: - https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/post-lists-members-destroy_all """ return self.post('lists/members/destroy_all', params=params) @@ -894,7 +969,8 @@ class EndpointsMixin(object): def show_owned_lists(self, **params): """Returns the lists owned by the specified Twitter user. - Docs: https://dev.twitter.com/docs/api/1.1/get/lists/ownerships + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-ownerships """ return self.get('lists/ownerships', params=params) @@ -905,7 +981,8 @@ class EndpointsMixin(object): def get_saved_searches(self, **params): """Returns the authenticated user's saved search queries. - Docs: https://dev.twitter.com/docs/api/1.1/get/saved_searches/list + Docs: + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-list """ return self.get('saved_searches/list', params=params) @@ -914,7 +991,7 @@ class EndpointsMixin(object): """Retrieve the information for the saved search represented by the given id. Docs: - https://dev.twitter.com/docs/api/1.1/get/saved_searches/show/%3Aid + https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-show-id """ return self.get('saved_searches/show/%s' % params.get('id'), @@ -923,7 +1000,8 @@ class EndpointsMixin(object): def create_saved_search(self, **params): """Create a new saved search for the authenticated user. - Docs: https://dev.twitter.com/docs/api/1.1/post/saved_searches/create + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-mutes-users-create """ return self.post('saved_searches/create', params=params) @@ -932,7 +1010,7 @@ class EndpointsMixin(object): """Destroys a saved search for the authenticating user. Docs: - https://dev.twitter.com/docs/api/1.1/post/saved_searches/destroy/%3Aid + https://developer.twitter.com/en/docs/tweets/search/api-reference/post-saved_searches-destroy-id """ return self.post('saved_searches/destroy/%s' % params.get('id'), @@ -942,7 +1020,8 @@ class EndpointsMixin(object): def get_geo_info(self, **params): """Returns all the information about a known place. - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/id/%3Aplace_id + Docs: + https://developer.twitter.com/en/docs/geo/place-information/api-reference/get-geo-id-place_id """ return self.get('geo/id/%s' % params.get('place_id'), params=params) @@ -951,7 +1030,8 @@ class EndpointsMixin(object): """Given a latitude and a longitude, searches for up to 20 places that can be used as a place_id when updating a status. - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/reverse_geocode + Docs: + https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-reverse_geocode """ return self.get('geo/reverse_geocode', params=params) @@ -959,7 +1039,8 @@ class EndpointsMixin(object): def search_geo(self, **params): """Search for places that can be attached to a statuses/update. - Docs: https://dev.twitter.com/docs/api/1.1/get/geo/search + Docs: + https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-search """ return self.get('geo/search', params=params) @@ -985,7 +1066,8 @@ class EndpointsMixin(object): """Returns the top 10 trending topics for a specific WOEID, if trending information is available for it. - Docs: https://dev.twitter.com/docs/api/1.1/get/trends/place + Docs: + https://developer.twitter.com/en/docs/trends/trends-for-location/api-reference/get-trends-place """ return self.get('trends/place', params=params) @@ -993,7 +1075,8 @@ class EndpointsMixin(object): def get_available_trends(self, **params): """Returns the locations that Twitter has trending topic information for. - Docs: https://dev.twitter.com/docs/api/1.1/get/trends/available + Docs: + https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-available """ return self.get('trends/available', params=params) @@ -1002,7 +1085,8 @@ class EndpointsMixin(object): """Returns the locations that Twitter has trending topic information for, closest to a specified location. - Docs: https://dev.twitter.com/docs/api/1.1/get/trends/closest + Docs: + https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-closest """ return self.get('trends/closest', params=params) @@ -1011,7 +1095,8 @@ class EndpointsMixin(object): def report_spam(self, **params): # pragma: no cover """Report the specified user as a spam account to Twitter. - Docs: https://dev.twitter.com/docs/api/1.1/post/users/report_spam + Docs: + https://developer.twitter.com/en/docs/accounts-and-users/mute-block-report-users/api-reference/post-users-report_spam """ return self.post('users/report_spam', params=params) @@ -1021,7 +1106,8 @@ class EndpointsMixin(object): """Allows a registered application to revoke an issued OAuth 2 Bearer Token by presenting its client credentials. - Docs: https://dev.twitter.com/docs/api/1.1/post/oauth2/invalidate_token + Docs: + https://developer.twitter.com/en/docs/basics/authentication/api-reference/invalidate_token """ return self.post('oauth2/invalidate_token', params=params) @@ -1030,7 +1116,8 @@ class EndpointsMixin(object): def get_twitter_configuration(self, **params): """Returns the current configuration used by Twitter - Docs: https://dev.twitter.com/docs/api/1.1/get/help/configuration + Docs: + https://developer.twitter.com/en/docs/developer-utilities/configuration/api-reference/get-help-configuration """ return self.get('help/configuration', params=params) @@ -1039,7 +1126,8 @@ class EndpointsMixin(object): """Returns the list of languages supported by Twitter along with their ISO 639-1 code. - Docs: https://dev.twitter.com/docs/api/1.1/get/help/languages + Docs: + https://developer.twitter.com/en/docs/developer-utilities/supported-languages/api-reference/get-help-languages """ return self.get('help/languages', params=params) @@ -1047,7 +1135,8 @@ class EndpointsMixin(object): def get_privacy_policy(self, **params): """Returns Twitter's Privacy Policy - Docs: https://dev.twitter.com/docs/api/1.1/get/help/privacy + Docs: + https://developer.twitter.com/en/docs/developer-utilities/privacy-policy/api-reference/get-help-privacy """ return self.get('help/privacy', params=params) @@ -1055,7 +1144,8 @@ class EndpointsMixin(object): def get_tos(self, **params): """Return the Twitter Terms of Service - Docs: https://dev.twitter.com/docs/api/1.1/get/help/tos + Docs: + https://developer.twitter.com/en/docs/developer-utilities/terms-of-service/api-reference/get-help-tos """ return self.get('help/tos', params=params) @@ -1065,13 +1155,13 @@ class EndpointsMixin(object): specified resource families. Docs: - https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status + https://developer.twitter.com/en/docs/developer-utilities/rate-limit-status/api-reference/get-application-rate_limit_status """ return self.get('application/rate_limit_status', params=params) -# from https://dev.twitter.com/docs/error-codes-responses +# from https://developer.twitter.com/en/docs/ads/general/guides/response-codes TWITTER_HTTP_STATUS_CODE = { 200: ('OK', 'Success!'), 304: ('Not Modified', 'There was no new data to return.'), -- 2.39.5 From 0f64978a08fe36281b1a24e27485c821e5cc4d0d Mon Sep 17 00:00:00 2001 From: Clayton A Davis Date: Wed, 18 Oct 2017 14:01:34 -0400 Subject: [PATCH 170/204] Fix typo in cursor example --- docs/usage/special_functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index cde8f99..9d40076 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -45,7 +45,7 @@ Another example: .. code-block:: python - results = twitter.cursor(t.get_mentions_timeline) + results = twitter.cursor(twitter.get_mentions_timeline) for result in results: print(result['id_str']) -- 2.39.5 From db40a1c56c57e711937171e1072a7f965354440c Mon Sep 17 00:00:00 2001 From: Clayton A Davis Date: Wed, 18 Oct 2017 14:02:56 -0400 Subject: [PATCH 171/204] Add doc for cursor's return_pages kwarg --- docs/usage/special_functions.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/usage/special_functions.rst b/docs/usage/special_functions.rst index 9d40076..301abd3 100644 --- a/docs/usage/special_functions.rst +++ b/docs/usage/special_functions.rst @@ -49,6 +49,19 @@ Another example: for result in results: print(result['id_str']) +Items vs Pages +^^^^^^^^^^^^^^ + +By default, the cursor yields one item at a time. If instead you prefer to work with entire pages of results, specify ``return_pages=True`` as a keyword argument. + +.. code-block:: python + + results = twitter.cursor(twitter.get_mentions_timeline, return_pages=True) + # page is a list + for page in results: + for result in page: + print(result['id_str']) + HTML for Tweet -------------- -- 2.39.5 From 05fbf6b8b261f70cd6c9f5bbcbb923983a7d836a Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 6 Nov 2017 10:35:26 -0500 Subject: [PATCH 172/204] - Cleaned up some docstrings with Sphinx markup and param names. --- HISTORY.rst | 4 ++++ twython/endpoints.py | 46 +++++++++++++++++++------------------- twython/streaming/types.py | 7 +++--- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4249539..129f5e9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ History ------- +Unreleased +++++++++++++++++++ +- Cleaned up some docstrings with Sphinx markup and param names. + 3.6.0 (2017-23-08) ++++++++++++++++++ - Improve replacing of entities with links in `html_for_tweet()` diff --git a/twython/endpoints.py b/twython/endpoints.py index f3e4528..00392b8 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -42,7 +42,7 @@ class EndpointsMixin(object): def get_user_timeline(self, **params): """Returns a collection of the most recent Tweets posted by the user - indicated by the screen_name or user_id parameters. + indicated by the ``screen_name`` or ``user_id`` parameters. Docs: https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-user_timeline @@ -85,7 +85,7 @@ class EndpointsMixin(object): params=params) def show_status(self, **params): - """Returns a single Tweet, specified by the id parameter + """Returns a single Tweet, specified by the ``id`` parameter Docs: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-show-id @@ -95,7 +95,7 @@ class EndpointsMixin(object): def lookup_status(self, **params): """Returns fully-hydrated tweet objects for up to 100 tweets per - request, as specified by comma-separated values passed to the id + request, as specified by comma-separated values passed to the ``id`` parameter. Docs: @@ -105,7 +105,7 @@ class EndpointsMixin(object): return self.post('statuses/lookup', params=params) def destroy_status(self, **params): - """Destroys the status specified by the required ID parameter + """Destroys the status specified by the required ``id`` parameter Docs: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-destroy-id @@ -123,7 +123,7 @@ class EndpointsMixin(object): return self.post('statuses/update', params=params) def retweet(self, **params): - """Retweets a tweet specified by the id parameter + """Retweets a tweet specified by the ``id`` parameter Docs: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-retweet-id @@ -149,7 +149,7 @@ class EndpointsMixin(object): def upload_media(self, **params): """Uploads media file to Twitter servers. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids - to the 'update_status' method using the 'media_ids' param. + to the :meth:`update_status` method using the ``media_ids`` param. Docs: https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload @@ -174,7 +174,7 @@ class EndpointsMixin(object): def upload_video(self, media, media_type, media_category=None, size=None, check_progress=False): """Uploads video file to Twitter servers in chunks. The file will be available to be attached to a status for 60 minutes. To attach to a update, pass a list of returned media ids - to the 'update_status' method using the 'media_ids' param. + to the :meth:`update_status` method using the ``media_ids`` param. Upload happens in 3 stages: - INIT call with size of media to be uploaded(in bytes). If this is more than 15mb, twitter will return error. @@ -272,7 +272,7 @@ class EndpointsMixin(object): def get_retweeters_ids(self, **params): """Returns a collection of up to 100 user IDs belonging to users who - have retweeted the tweet specified by the id parameter. + have retweeted the tweet specified by the ``id`` parameter. Docs: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-retweeters-ids @@ -317,7 +317,7 @@ class EndpointsMixin(object): get_sent_messages.iter_mode = 'id' def get_direct_message(self, **params): - """Returns a single direct message, specified by an id parameter. + """Returns a single direct message, specified by an ``id`` parameter. Docs: https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-message @@ -326,7 +326,7 @@ class EndpointsMixin(object): return self.get('direct_messages/show', params=params) def destroy_direct_message(self, **params): - """Destroys the direct message specified in the required id parameter + """Destroys the direct message specified in the required ``id`` parameter Docs: https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message @@ -381,7 +381,7 @@ class EndpointsMixin(object): def lookup_friendships(self, **params): """Returns the relationships of the authenticating user to the - comma-separated list of up to 100 screen_names or user_ids provided. + comma-separated list of up to 100 ``screen_names`` or ``user_ids`` provided. Docs: https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-friendships-lookup @@ -415,7 +415,7 @@ class EndpointsMixin(object): def create_friendship(self, **params): """Allows the authenticating users to follow the user specified - in the ID parameter. + in the ``id`` parameter. Docs: https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-create @@ -425,7 +425,7 @@ class EndpointsMixin(object): def destroy_friendship(self, **params): """Allows the authenticating user to unfollow the user specified - in the ID parameter. + in the ``id`` parameter. Docs: https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy @@ -540,8 +540,8 @@ class EndpointsMixin(object): """Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. - This method is deprecated, replaced by the profile_link_color - parameter to update_profile. + This method is deprecated, replaced by the ``profile_link_color`` + parameter to :meth:`update_profile`. Docs: https://developer.twitter.com/en/docs/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile @@ -597,7 +597,7 @@ class EndpointsMixin(object): return self.post('blocks/create', params=params) def destroy_block(self, **params): - """Un-blocks the user specified in the ID parameter for the + """Un-blocks the user specified in the ``id`` parameter for the authenticating user. Docs: @@ -608,8 +608,8 @@ class EndpointsMixin(object): def lookup_user(self, **params): """Returns fully-hydrated user objects for up to 100 users per request, - as specified by comma-separated values passed to the user_id and/or - screen_name parameters. + as specified by comma-separated values passed to the ``user_id`` and/or + ``screen_name`` parameters. Docs: https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-lookup @@ -619,7 +619,7 @@ class EndpointsMixin(object): def show_user(self, **params): """Returns a variety of information about the user specified by the - required user_id or screen_name parameter. + required ``user_id`` or ``screen_name`` parameter. Docs: https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/get-users-show @@ -718,7 +718,7 @@ class EndpointsMixin(object): return self.post('mutes/users/create', params=params) def destroy_mute(self, **params): - """Un-mutes the user specified in the user or ID parameter for + """Un-mutes the user specified in the user or ``id`` parameter for the authenticating user. Docs: @@ -772,7 +772,7 @@ class EndpointsMixin(object): get_favorites.iter_mode = 'id' def destroy_favorite(self, **params): - """Un-favorites the status specified in the ID parameter as the + """Un-favorites the status specified in the ``id`` parameter as the authenticating user. Docs: @@ -782,7 +782,7 @@ class EndpointsMixin(object): return self.post('favorites/destroy', params=params) def create_favorite(self, **params): - """Favorites the status specified in the ID parameter as the + """Favorites the status specified in the ``id`` parameter as the authenticating user. Docs: @@ -988,7 +988,7 @@ class EndpointsMixin(object): return self.get('saved_searches/list', params=params) def show_saved_search(self, **params): - """Retrieve the information for the saved search represented by the given id. + """Retrieve the information for the saved search represented by the given ``id``. Docs: https://developer.twitter.com/en/docs/tweets/search/api-reference/get-saved_searches-show-id diff --git a/twython/streaming/types.py b/twython/streaming/types.py index 8755a8d..81c5c07 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -44,9 +44,10 @@ class TwythonStreamerTypes(object): class TwythonStreamerTypesStatuses(object): """Class for different statuses endpoints - Available so TwythonStreamer.statuses.filter() is available. - Just a bit cleaner than TwythonStreamer.statuses_filter(), - statuses_sample(), etc. all being single methods in TwythonStreamer + Available so :meth:`TwythonStreamer.statuses.filter()` is available. + Just a bit cleaner than :meth:`TwythonStreamer.statuses_filter()`, + :meth:`statuses_sample()`, etc. all being single methods in + :class:`TwythonStreamer`. """ def __init__(self, streamer): -- 2.39.5 From 9b46ca5845685d525ad92a67b9a386311dff9672 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 6 Nov 2017 12:49:10 -0500 Subject: [PATCH 173/204] Update HISTORY.rst --- HISTORY.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 129f5e9..4087a68 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,9 +3,6 @@ History ------- -Unreleased -++++++++++++++++++ -- Cleaned up some docstrings with Sphinx markup and param names. 3.6.0 (2017-23-08) ++++++++++++++++++ -- 2.39.5 From b009ed30b84cc41d624c6e68ff31ee138dc796d6 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Sat, 6 Jan 2018 17:27:18 +0000 Subject: [PATCH 174/204] Removing unused file Sorry, I'm not sure how or why I added this in, but it's not needed by anything! --- tweet.json | 142 ----------------------------------------------------- 1 file changed, 142 deletions(-) delete mode 100644 tweet.json diff --git a/tweet.json b/tweet.json deleted file mode 100644 index 5e61ed8..0000000 --- a/tweet.json +++ /dev/null @@ -1,142 +0,0 @@ -{ - "source":"web", - "entities":{ - "user_mentions":[ ], - "media":[ - { - "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", - "indices":[ 131, 154 ], - "url":"https://t.co/OwNc6uJklg", - "media_url":"http://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg", - "id_str":"905105571765944320", - "id":905105571765944320, - "media_url_https":"https://pbs.twimg.com/media/DI-UuNlWAAA_cvz.jpg", - "sizes":[ - { - "h":256, - "resize":"fit", - "w":680 - }, - { - "h":376, - "resize":"fit", - "w":1000 - }, - { - "h":150, - "resize":"crop", - "w":150 - }, - { - "h":376, - "resize":"fit", - "w":1000 - }, - { - "h":376, - "resize":"fit", - "w":1000 - } - ], - "display_url":"pic.twitter.com/OwNc6uJklg" - }, - { - "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", - "indices":[ 131, 154 ], - "url":"https://t.co/OwNc6uJklg", - "media_url":"http://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg", - "id_str":"905105572529393668", - "id":905105572529393668, - "media_url_https":"https://pbs.twimg.com/media/DI-UuQbXUAQ1WlR.jpg", - "sizes":[ - { - "h":399, - "resize":"fit", - "w":1000 - }, - { - "h":271, - "resize":"fit", - "w":680 - }, - { - "h":399, - "resize":"fit", - "w":1000 - }, - { - "h":150, - "resize":"crop", - "w":150 - }, - { - "h":399, - "resize":"fit", - "w":1000 - } - ], - "display_url":"pic.twitter.com/OwNc6uJklg" - }, - { - "expanded_url":"https://twitter.com/philgyford/status/905105588279013377/photo/1", - "indices":[ 131, 154 ], - "url":"https://t.co/OwNc6uJklg", - "media_url":"http://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg", - "id_str":"905105573255016448", - "id":905105573255016448, - "media_url_https":"https://pbs.twimg.com/media/DI-UuTIXcAA-t1_.jpg", - "sizes":[ - { - "h":287, - "resize":"fit", - "w":1000 - }, - { - "h":195, - "resize":"fit", - "w":680 - }, - { - "h":150, - "resize":"crop", - "w":150 - }, - { - "h":287, - "resize":"fit", - "w":1000 - }, - { - "h":287, - "resize":"fit", - "w":1000 - } - ], - "display_url":"pic.twitter.com/OwNc6uJklg" - } - ], - "hashtags":[ ], - "urls":[ - { - "indices":[ 107, 130 ], - "url":"https://t.co/2yUmmn3TOc", - "expanded_url":"http://www.gyford.com/phil/writing/2017/09/05/book-series-charts.php", - "display_url":"gyford.com/phil/writing/2\u2026" - } - ] - }, - "geo":{ }, - "id_str":"905105588279013377", - "text":"I made some D3.js charts showing the years covered by books in a series compared to their publishing dates https://t.co/2yUmmn3TOc https://t.co/OwNc6uJklg", - "id":905105588279013377, - "created_at":"2017-09-05 16:29:22 +0000", - "user":{ - "name":"Phil Gyford", - "screen_name":"philgyford", - "protected":false, - "id_str":"12552", - "profile_image_url_https":"https://pbs.twimg.com/profile_images/1167616130/james_200208_300x300_normal.jpg", - "id":12552, - "verified":false - } -} -- 2.39.5 From 73982c78f4bd541c82916c77d7aeb4ad757e9007 Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Sat, 6 Jan 2018 19:03:17 +0000 Subject: [PATCH 175/204] Fix links in tweets when there's a prefix If a tweet had a prefix (@names that it was replying to) then the length of these is counted in the indices that show the locations of entities within the tweet. But we were applying those indices to the 'display' part of the tweet that doesn't include the prefix. So, if the tweet was: @bob Please meet @bill and the prefix was `@bob `, then the indices for linking `@bill` are something like `17,21`. But we were applying the link around `@bill` to the display text part of the tweet, which is: Please meet @bill And so the indices no longer lined up with `@bill`. Now they do, and the same for URLs and hashtags. --- tests/test_html_for_tweet.py | 13 +++++ tests/tweets/entities_with_prefix.json | 68 ++++++++++++++++++++++++++ twython/api.py | 14 +++--- 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 tests/tweets/entities_with_prefix.json diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index 7331fa4..934c2dc 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -89,6 +89,19 @@ class TestHtmlForTweetTestCase(unittest.TestCase): self.assertEqual(tweet_text, 'Say more about what\'s happening! Rolling out now: photos, videos, GIFs, polls, and Quote Tweets no longer count toward your 140 characters. pic.twitter.com/I9pUC0NdZC') + def test_entities_with_prefix(self): + """ + If there is a username mention at the start of a tweet it's in the + "prefix" and so isn't part of the main tweet display text. + But its length is still counted in the indices of any subsequent + mentions, urls, hashtags, etc. + """ + self.maxDiff = 2000 + tweet_object = self.load_tweet('entities_with_prefix') + tweet_text = self.api.html_for_tweet(tweet_object) + self.assertEqual(tweet_text, + '@philgyford This is a test for @visionphil that includes a link example.org and #hashtag and 😃 for good measure AND that is longer than 140 characters. example.com') + def test_media(self): tweet_object = self.load_tweet('media') tweet_text = self.api.html_for_tweet(tweet_object) diff --git a/tests/tweets/entities_with_prefix.json b/tests/tweets/entities_with_prefix.json new file mode 100644 index 0000000..72043f9 --- /dev/null +++ b/tests/tweets/entities_with_prefix.json @@ -0,0 +1,68 @@ +{ + "created_at":"Sat Jan 06 18:56:35 +0000 2018", + "id":949716340755091458, + "id_str":"949716340755091458", + "full_text":"@philgyford This is a test for @visionphil that includes a link https://t.co/sKw4J3A8SZ and #hashtag and \ud83d\ude03 for good measure AND that is longer than 140 characters. https://t.co/jnQdy7Zg7u", + "truncated":false, + "display_text_range":[ 12, 187 ], + "entities":{ + "hashtags":[ + { + "text":"hashtag", + "indices":[ 92, 100 ] + } + ], + "symbols":[ ], + "user_mentions":[ + { + "screen_name":"philgyford", + "name":"Phil Gyford", + "id":12552, + "id_str":"12552", + "indices":[ 0, 11 ] + }, + { + "screen_name":"visionphil", + "name":"Vision Phil", + "id":104456050, + "id_str":"104456050", + "indices":[ 31, 42 ] + } + ], + "urls":[ + { + "url":"https://t.co/sKw4J3A8SZ", + "expanded_url":"http://example.org", + "display_url":"example.org", + "indices":[ 64, 87 ] + }, + { + "url":"https://t.co/jnQdy7Zg7u", + "expanded_url":"http://example.com", + "display_url":"example.com", + "indices":[ 164, 187 ] + } + ] + }, + "source":"Tweetbot for Mac", + "in_reply_to_status_id":948561036889722880, + "in_reply_to_status_id_str":"948561036889722880", + "in_reply_to_user_id":12552, + "in_reply_to_user_id_str":"12552", + "in_reply_to_screen_name":"philgyford", + "user":{ + "id":2030131, + "id_str":"2030131" + }, + "geo":null, + "coordinates":null, + "place":null, + "contributors":null, + "is_quote_status":false, + "retweet_count":0, + "favorite_count":0, + "favorited":false, + "retweeted":false, + "possibly_sensitive":false, + "lang":"en" +} diff --git a/twython/api.py b/twython/api.py index f25cc97..57a10a6 100644 --- a/twython/api.py +++ b/twython/api.py @@ -581,6 +581,8 @@ class Twython(EndpointsMixin, object): if display_text_start <= temp['start'] <= display_text_end: temp['replacement'] = mention_html + temp['start'] -= display_text_start + temp['end'] -= display_text_start entities.append(temp) else: # Make the '@username' at the start, before @@ -592,8 +594,8 @@ class Twython(EndpointsMixin, object): if 'hashtags' in tweet['entities']: for entity in tweet['entities']['hashtags']: temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + temp['start'] = entity['indices'][0] - display_text_start + temp['end'] = entity['indices'][1] - display_text_start url_html = '#%(hashtag)s' % {'hashtag': entity['text']} @@ -604,8 +606,8 @@ class Twython(EndpointsMixin, object): if 'symbols' in tweet['entities']: for entity in tweet['entities']['symbols']: temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + temp['start'] = entity['indices'][0] - display_text_start + temp['end'] = entity['indices'][1] - display_text_start url_html = '$%(symbol)s' % {'symbol': entity['text']} @@ -616,8 +618,8 @@ class Twython(EndpointsMixin, object): if 'urls' in tweet['entities']: for entity in tweet['entities']['urls']: temp = {} - temp['start'] = entity['indices'][0] - temp['end'] = entity['indices'][1] + temp['start'] = entity['indices'][0] - display_text_start + temp['end'] = entity['indices'][1] - display_text_start if use_display_url and entity.get('display_url') and not use_expanded_url: shown_url = entity['display_url'] -- 2.39.5 From 2cfdaaf6e44ced237edc493147c16a38a60926eb Mon Sep 17 00:00:00 2001 From: Phil Gyford Date: Sat, 6 Jan 2018 19:53:00 +0000 Subject: [PATCH 176/204] Remove an emoji from a tweet to make test work Rendering emojis in tweets in python 2 may be an entirely different issue to tackle... --- tests/test_html_for_tweet.py | 4 ++-- tests/tweets/entities_with_prefix.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_html_for_tweet.py b/tests/test_html_for_tweet.py index 934c2dc..921dfee 100644 --- a/tests/test_html_for_tweet.py +++ b/tests/test_html_for_tweet.py @@ -96,11 +96,11 @@ class TestHtmlForTweetTestCase(unittest.TestCase): But its length is still counted in the indices of any subsequent mentions, urls, hashtags, etc. """ - self.maxDiff = 2000 + self.maxDiff = 2200 tweet_object = self.load_tweet('entities_with_prefix') tweet_text = self.api.html_for_tweet(tweet_object) self.assertEqual(tweet_text, - '@philgyford This is a test for @visionphil that includes a link example.org and #hashtag and 😃 for good measure AND that is longer than 140 characters. example.com') + u'@philgyford This is a test for @visionphil that includes a link example.org and #hashtag and X for good measure AND that is longer than 140 characters. example.com') def test_media(self): tweet_object = self.load_tweet('media') diff --git a/tests/tweets/entities_with_prefix.json b/tests/tweets/entities_with_prefix.json index 72043f9..8ba8975 100644 --- a/tests/tweets/entities_with_prefix.json +++ b/tests/tweets/entities_with_prefix.json @@ -2,7 +2,7 @@ "created_at":"Sat Jan 06 18:56:35 +0000 2018", "id":949716340755091458, "id_str":"949716340755091458", - "full_text":"@philgyford This is a test for @visionphil that includes a link https://t.co/sKw4J3A8SZ and #hashtag and \ud83d\ude03 for good measure AND that is longer than 140 characters. https://t.co/jnQdy7Zg7u", + "full_text":"@philgyford This is a test for @visionphil that includes a link https://t.co/sKw4J3A8SZ and #hashtag and X for good measure AND that is longer than 140 characters. https://t.co/jnQdy7Zg7u", "truncated":false, "display_text_range":[ 12, 187 ], "entities":{ -- 2.39.5 From c9e8a462000898dcd91b9babf130b907986591ea Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 7 May 2018 12:52:54 -0400 Subject: [PATCH 177/204] Version 3.7.0 --- HISTORY.rst | 9 +++++++++ docs/conf.py | 4 ++-- .../get_direct_messages.py | 0 setup.py | 2 +- twython/__init__.py | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) rename get_direct_message example => examples/get_direct_messages.py (100%) diff --git a/HISTORY.rst b/HISTORY.rst index 4087a68..75964ff 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,15 @@ History ------- +3.7.0 (2018-07-05) +++++++++++++++++++ +- Fixes for cursoring API endpoints +- Improve `html_for_tweet()` parsing +- Documentation cleanup +- Documentation for cursor's `return_pages` keyword argument +- Update links to Twitter API in documentation +- Added `create_metadata` endpoint +- Raise error for when cursor is not provided a callable 3.6.0 (2017-23-08) ++++++++++++++++++ diff --git a/docs/conf.py b/docs/conf.py index 601d066..8f3c3d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.6.0' +version = '3.7.0' # The full version, including alpha/beta/rc tags. -release = '3.6.0' +release = '3.7.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/get_direct_message example b/examples/get_direct_messages.py similarity index 100% rename from get_direct_message example rename to examples/get_direct_messages.py diff --git a/setup.py b/setup.py index 40b05b7..11cc2b4 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.6.0' +__version__ = '3.7.0' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index 84e9ee6..dc161d1 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.6.0' +__version__ = '3.7.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 36fda7ac02a8047d11ad4de12161a6ffd1e869ea Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 22 Sep 2018 12:01:57 -0500 Subject: [PATCH 178/204] Added new endpoints for direct messages --- twython/api.py | 22 +++++++++++++++------- twython/endpoints.py | 12 ++++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/twython/api.py b/twython/api.py index 4953129..10c4963 100644 --- a/twython/api.py +++ b/twython/api.py @@ -135,13 +135,13 @@ class Twython(EndpointsMixin, object): def __repr__(self): return '' % (self.app_key) - def _request(self, url, method='GET', params=None, api_call=None): + def _request(self, url, method='GET', params=None, api_call=None, json_encoded=False): """Internal request method""" method = method.lower() params = params or {} func = getattr(self.client, method) - if isinstance(params, dict): + if isinstance(params, dict) and json_encoded == False: params, files = _transparent_params(params) else: params = params @@ -156,8 +156,13 @@ class Twython(EndpointsMixin, object): if method == 'get': requests_args['params'] = params else: + # Check for json_encoded so we will sent params as "data" or "json" + if json_encoded: + data_key = "json" + else: + data_key = "data" requests_args.update({ - 'data': params, + data_key: params, 'files': files, }) try: @@ -230,7 +235,7 @@ class Twython(EndpointsMixin, object): return error_message - def request(self, endpoint, method='GET', params=None, version='1.1'): + def request(self, endpoint, method='GET', params=None, version='1.1', json_encoded=False): """Return dict of response received from Twitter's API :param endpoint: (required) Full url or Twitter API endpoint @@ -246,6 +251,9 @@ class Twython(EndpointsMixin, object): :param version: (optional) Twitter API version to access (default 1.1) :type version: string + :param json_encoded: (optional) Flag to indicate if this method should send data encoded as json + (default False) + :type json_encoded: bool :rtype: dict """ @@ -261,7 +269,7 @@ class Twython(EndpointsMixin, object): url = '%s/%s.json' % (self.api_url % version, endpoint) content = self._request(url, method=method, params=params, - api_call=url) + api_call=url, json_encoded=json_encoded) return content @@ -269,9 +277,9 @@ class Twython(EndpointsMixin, object): """Shortcut for GET requests via :class:`request`""" return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, version='1.1'): + def post(self, endpoint, params=None, version='1.1', json_encoded=False): """Shortcut for POST requests via :class:`request`""" - return self.request(endpoint, 'POST', params=params, version=version) + return self.request(endpoint, 'POST', params=params, version=version, json_encoded=json_encoded) def get_lastfunction_header(self, header, default_return_value=None): """Returns a specific header from the last API call diff --git a/twython/endpoints.py b/twython/endpoints.py index 00392b8..f7e0a5a 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -300,10 +300,10 @@ class EndpointsMixin(object): """Returns the 20 most recent direct messages sent to the authenticating user. Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-messages + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events """ - return self.get('direct_messages', params=params) + return self.get('direct_messages/events/list', params=params) get_direct_messages.iter_mode = 'id' def get_sent_messages(self, **params): @@ -320,10 +320,10 @@ class EndpointsMixin(object): """Returns a single direct message, specified by an ``id`` parameter. Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-message + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event """ - return self.get('direct_messages/show', params=params) + return self.get('direct_messages/events/show', params=params) def destroy_direct_message(self, **params): """Destroys the direct message specified in the required ``id`` parameter @@ -339,10 +339,10 @@ class EndpointsMixin(object): authenticating user. Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-message + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event """ - return self.post('direct_messages/new', params=params) + return self.post('direct_messages/events/new', params=params, json_encoded=True) # Friends & Followers def get_user_ids_of_blocked_retweets(self, **params): -- 2.39.5 From 340fb4ea162b3e02fb5e245ad8651ec62d34cea5 Mon Sep 17 00:00:00 2001 From: Vishvajit Pathak Date: Sat, 29 Sep 2018 15:18:31 +0530 Subject: [PATCH 179/204] ISSUE-497 : Fixed the api end point in get_oembed_tweet --- twython/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index 00392b8..9da30db 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -268,7 +268,7 @@ class EndpointsMixin(object): https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed """ - return self.get('statuses/oembed', params=params) + return self.get('oembed', params=params) def get_retweeters_ids(self, **params): """Returns a collection of up to 100 user IDs belonging to users who -- 2.39.5 From a8a0777f7266f87ca1f6af921c35f573a71f45e3 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 29 Sep 2018 11:09:00 -0500 Subject: [PATCH 180/204] Added delete methods to Twython's API --- twython/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/twython/api.py b/twython/api.py index 10c4963..96d1ae7 100644 --- a/twython/api.py +++ b/twython/api.py @@ -153,7 +153,7 @@ class Twython(EndpointsMixin, object): if k in ('timeout', 'allow_redirects', 'stream', 'verify'): requests_args[k] = v - if method == 'get': + if method == 'get' or method == 'delete': requests_args['params'] = params else: # Check for json_encoded so we will sent params as "data" or "json" @@ -242,7 +242,7 @@ class Twython(EndpointsMixin, object): (e.g. search/tweets) :type endpoint: string :param method: (optional) Method of accessing data, either - GET or POST. (default GET) + GET, POST or DELETE. (default GET) :type method: string :param params: (optional) Dict of parameters (if any) accepted the by Twitter API endpoint you are trying to @@ -281,6 +281,10 @@ class Twython(EndpointsMixin, object): """Shortcut for POST requests via :class:`request`""" return self.request(endpoint, 'POST', params=params, version=version, json_encoded=json_encoded) + def delete(self, endpoint, params=None, version='1.1', json_encoded=False): + """Shortcut for delete requests via :class:`request`""" + return self.request(endpoint, 'DELETE', params=params, version=version, json_encoded=json_encoded) + def get_lastfunction_header(self, header, default_return_value=None): """Returns a specific header from the last API call This will return None if the header is not present -- 2.39.5 From 96dd5b289792fff6b5f438471bc6b2ed21f62bcc Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sat, 29 Sep 2018 11:09:39 -0500 Subject: [PATCH 181/204] Added support for delete direct_messages/events/destroy endpoint --- twython/endpoints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/endpoints.py b/twython/endpoints.py index f7e0a5a..9adf800 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -329,10 +329,10 @@ class EndpointsMixin(object): """Destroys the direct message specified in the required ``id`` parameter Docs: - https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message + https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message-event """ - return self.post('direct_messages/destroy', params=params) + return self.delete('direct_messages/events/destroy', params=params) def send_direct_message(self, **params): """Sends a new direct message to the specified user from the -- 2.39.5 From 449807a75965a583f139df16d5e30c1e0e814613 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Sun, 30 Sep 2018 10:53:08 -0500 Subject: [PATCH 182/204] Fixed code styling --- twython/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index 96d1ae7..b0b7287 100644 --- a/twython/api.py +++ b/twython/api.py @@ -141,7 +141,7 @@ class Twython(EndpointsMixin, object): params = params or {} func = getattr(self.client, method) - if isinstance(params, dict) and json_encoded == False: + if isinstance(params, dict) and json_encoded is False: params, files = _transparent_params(params) else: params = params @@ -158,9 +158,9 @@ class Twython(EndpointsMixin, object): else: # Check for json_encoded so we will sent params as "data" or "json" if json_encoded: - data_key = "json" + data_key = 'json' else: - data_key = "data" + data_key = 'data' requests_args.update({ data_key: params, 'files': files, -- 2.39.5 From 4f29fd041b960569e46cdf17e08455d40cfc7f66 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Fri, 16 Nov 2018 05:34:52 -0600 Subject: [PATCH 183/204] Fixed indentation --- twython/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/api.py b/twython/api.py index b0b7287..bb10db0 100644 --- a/twython/api.py +++ b/twython/api.py @@ -160,7 +160,7 @@ class Twython(EndpointsMixin, object): if json_encoded: data_key = 'json' else: - data_key = 'data' + data_key = 'data' requests_args.update({ data_key: params, 'files': files, -- 2.39.5 From 02995d7e886fa1b01a1bb0ed0b759d75021835db Mon Sep 17 00:00:00 2001 From: Domenico Nappo Date: Mon, 19 Nov 2018 14:37:31 +0100 Subject: [PATCH 184/204] Adding response.headers to on_error method in twython.streaming.api #503 --- twython/streaming/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 0195f38..dd4ae89 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -125,7 +125,7 @@ class TwythonStreamer(object): self.on_timeout() else: if response.status_code != 200: - self.on_error(response.status_code, response.content) + self.on_error(response.status_code, response.content, response.headers) if self.retry_count and \ (self.retry_count - retry_counter) > 0: @@ -178,7 +178,7 @@ class TwythonStreamer(object): """ return True - def on_error(self, status_code, data): # pragma: no cover + def on_error(self, status_code, data, headers=None): # pragma: no cover """Called when stream returns non-200 status code Feel free to override this to handle your streaming data how you @@ -189,6 +189,9 @@ class TwythonStreamer(object): :param data: Error message sent from stream :type data: dict + + :param headers: Response headers sent from the stream (i.e. Retry-After) + :type headers: dict """ return -- 2.39.5 From 554fba43575e61c46e6e4b583484697fccf4a172 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 10 Mar 2019 20:01:14 +0700 Subject: [PATCH 185/204] MANIFEST.in: Add docs, examples and tests Closes https://github.com/ryanmcgrath/twython/issues/507 --- MANIFEST.in | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index eae175a..bcfd314 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,9 @@ include README.rst HISTORY.rst LICENSE requirements.txt + +recursive-include docs * +prune docs/_build + +recursive-include examples *.py + +recursive-include tests *.py +recursive-include tests/tweets *.json -- 2.39.5 From a029433247c4bdd15d56e27f4463a6a8a3ad503f Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 30 Mar 2019 11:03:51 +0700 Subject: [PATCH 186/204] Add Python 3.7 support Also remove branch restriction in .travis.yml which prevents contributors from seeing builds on their forks. --- .travis.yml | 9 ++++++--- setup.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8da6c43..6ec2958 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,12 @@ python: - 2.7 - 3.5 - 3.6 +# Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs +matrix: + include: + - python: 3.7 + dist: xenial + sudo: true env: global: - secure: USjLDneiXlVvEjkUVqTt+LBi0XJ4QhkRcJzqVXA9gEau1NTjAkNTPmHjUbOygp0dkfoV0uWrZKCw6fL1g+HJgWl0vHeHzcNl4mUkA+OwkGFHgaeIhvUfnyyJA8P3Zm21XHC+ehzMpEFN5fVNNhREjnRj+CXMc0FgA6knwBRobu4= @@ -24,9 +30,6 @@ install: script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing notifications: email: false -branches: - only: - - master after_success: - coveralls before_script: diff --git a/setup.py b/setup.py index 11cc2b4..3300b4d 100755 --- a/setup.py +++ b/setup.py @@ -47,5 +47,6 @@ setup( 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ] ) -- 2.39.5 From d05fe7516e5a3cd09656ccce6f0a7276dff9e0c6 Mon Sep 17 00:00:00 2001 From: AnnaYasenova Date: Tue, 20 Aug 2019 12:38:20 +0300 Subject: [PATCH 187/204] Update compat.py --- twython/compat.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/twython/compat.py b/twython/compat.py index 7c049b0..2ee7d37 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -9,6 +9,7 @@ Python 3 compatibility. """ import sys +import numpy as np _ver = sys.version_info @@ -29,7 +30,7 @@ if is_py2: str = unicode basestring = basestring - numeric_types = (int, long, float) + numeric_types = (int, long, float, np.int64, np.float64) elif is_py3: @@ -37,4 +38,4 @@ elif is_py3: str = str basestring = (str, bytes) - numeric_types = (int, float) + numeric_types = (int, float, np.int64, np.float64) -- 2.39.5 From 1b085180ff6d9cb4e395551682c5a628545cb70c Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Fri, 28 Feb 2020 05:37:05 +1100 Subject: [PATCH 188/204] Fix simple typo: specifcally -> specifically Closes #526 --- twython/advisory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/advisory.py b/twython/advisory.py index 31657ee..9a0863a 100644 --- a/twython/advisory.py +++ b/twython/advisory.py @@ -17,6 +17,6 @@ only TwythonDeprecationWarnings. class TwythonDeprecationWarning(DeprecationWarning): """Custom DeprecationWarning to be raised when methods/variables are being deprecated in Twython. Python 2.7 > ignores DeprecationWarning - so we want to specifcally bubble up ONLY Twython Deprecation Warnings + so we want to specifically bubble up ONLY Twython Deprecation Warnings """ pass -- 2.39.5 From 33fccac46b89262d5f41aee7d2e7a82c0891ae26 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Apr 2020 22:48:57 -0700 Subject: [PATCH 189/204] Fix this so Pypi publishing works again --- HISTORY.rst | 86 ++++--------- README.md | 163 +++++++++++++++++++++++ README.rst | 222 -------------------------------- examples/get_direct_messages.py | 14 +- examples/get_user_timeline.py | 2 +- setup.py | 5 +- 6 files changed, 193 insertions(+), 299 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/HISTORY.rst b/HISTORY.rst index 75964ff..fd2b8ee 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,10 +1,10 @@ -.. :changelog: +# History -History -------- +## 3.8.0 (2020-04-02) +- Bump release with latest patches from GitHub. +- Fix Direct Messages with patches from @manuelcortez. -3.7.0 (2018-07-05) -++++++++++++++++++ +## 3.7.0 (2018-07-05) - Fixes for cursoring API endpoints - Improve `html_for_tweet()` parsing - Documentation cleanup @@ -13,32 +13,27 @@ History - Added `create_metadata` endpoint - Raise error for when cursor is not provided a callable -3.6.0 (2017-23-08) -++++++++++++++++++ +## 3.6.0 (2017-23-08) - Improve replacing of entities with links in `html_for_tweet()` - Update classifiers for PyPI -3.5.0 (2017-06-06) -++++++++++++++++++ +## 3.5.0 (2017-06-06) - Added support for "symbols" in `Twython.html_for_tweet()` - Added support for extended tweets in `Twython.html_for_tweet()` - You can now check progress of video uploads to Twitter when using `Twython.upload_video()` -3.4.0 (2016-30-04) -++++++++++++++++++ +## 3.4.0 (2016-30-04) - Added `upload_video` endpoint - Fix quoted status checks in `html_for_tweet` - Fix `html_for_tweet` method response when hashtag/mention is a substring of another -3.3.0 (2015-18-07) -++++++++++++++++++ +## 3.3.0 (2015-18-07) - Added support for muting users - Fix typos in documentation - Updated documentation examples - Added dynamic filtering to streamer -3.2.0 (2014-10-30) -++++++++++++++++++ +## 3.2.0 (2014-10-30) - PEP8'd some code - Added `lookup_status` function to `endpoints.py` - Added keyword argument to `cursor` to return full pages rather than individual results @@ -51,23 +46,16 @@ History - Deprecating `update_with_media` per Twitter API 1.1 (https://dev.twitter.com/rest/reference/post/statuses/update_with_media) - Unpin `requests` and `requests-oauthlib` in `requirements.txt` - -3.1.2 (2013-12-05) -++++++++++++++++++ - +## 3.1.2 (2013-12-05) - Fixed Changelog (HISTORY.rst) -3.1.1 (2013-12-05) -++++++++++++++++++ - +## 3.1.1 (2013-12-05) - Update `requests` version to 2.1.0. - Fixed: Streaming issue where `Exceptions` in handlers or `on_success` which subclass `ValueError` would previously be caught and reported as a JSON decoding problem, and `on_error()` would be called (with status_code=200) - Fixed issue where XML was returned when bad tokens were passed to `get_authorized_tokens` - Fixed import for `setup` causing installation to fail on some devices (eg. Nokia N9/MeeGo) -3.1.0 (2013-09-25) -++++++++++++++++++ - +## 3.1.0 (2013-09-25) - Added ``html_for_tweet`` static method. This method accepts a tweet object returned from a Twitter API call and will return a string with urls, mentions and hashtags in the tweet replaced with HTML. - Pass ``client_args`` to the streaming ``__init__``, much like in core Twython (you can pass headers, timeout, hooks, proxies, etc.). - Streamer has new parameter ``handlers`` which accepts a list of strings related to functions that are apart of the Streaming class and start with "on\_". i.e. ['delete'] is passed, when 'delete' is received from a stream response; ``on_delete`` will be called. @@ -79,9 +67,7 @@ History - Fixed streaming issue where results wouldn't be returned for streams that weren't so active (See https://github.com/ryanmcgrath/twython/issues/202#issuecomment-19915708) - Streaming API now uses ``_transparent_params`` so when passed ``True`` or ``False`` or an array, etc. Twython formats it to meet Twitter parameter standards (i.e. ['ryanmcgrath', 'mikehelmick', 'twitterapi'] would convert to string 'ryanmcgrath,mikehelmick,twitterapi') -3.0.0 (2013-06-18) -++++++++++++++++++ - +## 3.0.0 (2013-06-18) - Changed ``twython/twython.py`` to ``twython/api.py`` in attempt to make structure look a little neater - Removed all camelCase function access (anything like ``getHomeTimeline`` is now ``get_home_timeline``) - Removed ``shorten_url``. With the ``requests`` library, shortening a URL on your own is simple enough @@ -100,9 +86,7 @@ History - Added ``invalidate_token`` API method which allows registed apps to revoke an access token presenting its client credentials - ``get_lastfunction_header`` now accepts a ``default_return_value`` parameter. This means that if you pass a second value (ex. ``Twython.get_lastfunction_header('x-rate-limit-remaining', 0)``) and the value is not found, it returns your default value -2.10.1 (2013-05-29) -++++++++++++++++++ - +## 2.10.1 (2013-05-29) - More test coverage! - Fix ``search_gen`` - Fixed ``get_lastfunction_header`` to actually do what its docstring says, returns ``None`` if header is not found @@ -112,9 +96,7 @@ History - No longer raise ``TwythonStreamError`` when stream line can't be decoded. Instead, sends signal to ``TwythonStreamer.on_error`` - Allow for (int, long, float) params to be passed to Twython Twitter API functions in Python 2, and (int, float) in Python 3 -2.10.0 (2013-05-21) -++++++++++++++++++ - +## 2.10.0 (2013-05-21) - Added ``get_retweeters_ids`` method - Fixed ``TwythonDeprecationWarning`` on camelCase functions if the camelCase was the same as the PEP8 function (i.e. ``Twython.retweet`` did not change) - Fixed error message bubbling when error message returned from Twitter was not an array (i.e. if you try to retweet something twice, the error is not found at index 0) @@ -125,20 +107,14 @@ History - Cleaned up ``Twython.construct_api_url``, uses "transparent" parameters (see 4th bullet in this version for explaination) - Update ``requests`` and ``requests-oauthlib`` requirements, fixing posting files AND post data together, making authenticated requests in general in Python 3.3 -2.9.1 (2013-05-04) -++++++++++++++++++ - +## 2.9.1 (2013-05-04) - "PEP8" all the functions. Switch functions from camelCase() to underscore_funcs(). (i.e. ``updateStatus()`` is now ``update_status()``) -2.9.0 (2013-05-04) -++++++++++++++++++ - +## 2.9.0 (2013-05-04) - Fixed streaming issue #144, added ``TwythonStreamer`` to aid users in a friendly streaming experience (streaming examples in ``examples`` and README's have been updated as well) - ``Twython`` now requires ``requests-oauthlib`` 0.3.1, fixes #154 (unable to upload media when sending POST data with the file) -2.8.0 (2013-04-29) -++++++++++++++++++ - +## 2.8.0 (2013-04-29) - Added a ``HISTORY.rst`` to start tracking history of changes - Updated ``twitter_endpoints.py`` to ``endpoints.py`` for cleanliness - Removed twython3k directory, no longer needed @@ -161,36 +137,24 @@ History - Twython now takes ``ssl_verify`` parameter, defaults True. Set False if you're having development server issues - Removed internal ``_media_update`` function, we could have always just used ``self.post`` -2.7.3 (2013-04-12) -++++++++++++++++++ - +## 2.7.3 (2013-04-12) - Fixed issue where Twython Exceptions were not being logged correctly -2.7.2 (2013-04-08) -++++++++++++++++++ - +## 2.7.2 (2013-04-08) - Fixed ``AttributeError`` when trying to decode the JSON response via ``Response.json()`` -2.7.1 (2013-04-08) -++++++++++++++++++ - +## 2.7.1 (2013-04-08) - Removed ``simplejson`` dependency - Fixed ``destroyDirectMessage``, ``createBlock``, ``destroyBlock`` endpoints in ``twitter_endpoints.py`` - Added ``getProfileBannerSizes`` method to ``twitter_endpoints.py`` - Made oauth_verifier argument required in ``get_authorized_tokens`` - Update ``updateProfileBannerImage`` to use v1.1 endpoint -2.7.0 (2013-04-04) -++++++++++++++++++ - +## 2.7.0 (2013-04-04) - New ``showOwnedLists`` method -2.7.0 (2013-03-31) -++++++++++++++++++ - +## 2.7.0 (2013-03-31) - Added missing slash to ``getMentionsTimeline`` in ``twitter_endpoints.py`` -2.6.0 (2013-03-29) -++++++++++++++++++ - +## 2.6.0 (2013-03-29) - Updated ``twitter_endpoints.py`` to better reflect order of API endpoints on the Twitter API v1.1 docs site diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe0aa1b --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# Twython + + + + + + +`Twython` is a Python library providing an easy way to access Twitter data. Supports Python 3. It's been battle tested by companies, educational institutions and individuals alike. Try it today! + +**Note**: As of Twython 3.7.0, there's a general call for maintainers put out. If you find the project useful and want to help out, reach out to Ryan with the info from the bottom of this README. Great open source project to get your feet wet with! + +## Features +- Query data for: + - User information + - Twitter lists + - Timelines + - Direct Messages + - and anything found in [the docs](https://developer.twitter.com/en/docs) +- Image Uploading: + - Update user status with an image + - Change user avatar + - Change user background image + - Change user banner image +- OAuth 2 Application Only (read-only) Support +- Support for Twitter's Streaming API +- Seamless Python 3 support! + +## Installation +Install Twython via pip: + +```bash +$ pip install twython +``` + +Or, if you want the code that is currently on GitHub + +```bash +git clone git://github.com/ryanmcgrath/twython.git +cd twython +python setup.py install +``` + +## Documentation +Documentation is available at https://twython.readthedocs.io/en/latest/ + +## Starting Out +First, you'll want to head over to https://apps.twitter.com and register an application! + +After you register, grab your applications `Consumer Key` and `Consumer Secret` from the application details tab. + +The most common type of authentication is Twitter user authentication using OAuth 1. If you're a web app planning to have users sign up with their Twitter account and interact with their timelines, updating their status, and stuff like that this **is** the authentication for you! + +First, you'll want to import Twython + +```python +from twython import Twython +``` + +## Obtain Authorization URL +Now, you'll want to create a Twython instance with your `Consumer Key` and `Consumer Secret`: + +- Only pass *callback_url* to *get_authentication_tokens* if your application is a Web Application +- Desktop and Mobile Applications **do not** require a callback_url + +```python +APP_KEY = 'YOUR_APP_KEY' +APP_SECRET = 'YOUR_APP_SECRET' + +twitter = Twython(APP_KEY, APP_SECRET) + +auth = twitter.get_authentication_tokens(callback_url='http://mysite.com/callback') +``` + +From the `auth` variable, save the `oauth_token` and `oauth_token_secret` for later use (these are not the final auth tokens). In Django or other web frameworks, you might want to store it to a session variable + +```python +OAUTH_TOKEN = auth['oauth_token'] +OAUTH_TOKEN_SECRET = auth['oauth_token_secret'] +``` + +Send the user to the authentication url, you can obtain it by accessing + +```python +auth['auth_url'] +``` + +## Handling the Callback +If your application is a Desktop or Mobile Application *oauth_verifier* will be the PIN code + +After they authorize your application to access some of their account details, they'll be redirected to the callback url you specified in `get_authentication_tokens`. + +You'll want to extract the `oauth_verifier` from the url. + +Django example: + +```python +oauth_verifier = request.GET['oauth_verifier'] +``` + +Now that you have the `oauth_verifier` stored to a variable, you'll want to create a new instance of Twython and grab the final user tokens + +```python +twitter = Twython( + APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET +) + +final_step = twitter.get_authorized_tokens(oauth_verifier) +``` + +Once you have the final user tokens, store them in a database for later use:: + +```python + OAUTH_TOKEN = final_step['oauth_token'] + OAUTH_TOKEN_SECRET = final_step['oauth_token_secret'] +``` + +For OAuth 2 (Application Only, read-only) authentication, see [our documentation](https://twython.readthedocs.io/en/latest/usage/starting_out.html#oauth-2-application-authentication). + +## Dynamic Function Arguments +Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. + +Basic Usage +----------- + +**Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py** + +Create a Twython instance with your application keys and the users OAuth tokens + +```python +from twython import Twython +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) +``` + +## Authenticated Users Home Timeline +```python +twitter.get_home_timeline() +``` + +## Updating Status +This method makes use of dynamic arguments, [read more about them](https://twython.readthedocs.io/en/latest/usage/starting_out.html#dynamic-function-arguments). + +```python +twitter.update_status(status='See how easy using Twython is!') +``` + +## Advanced Usage +- [Advanced Twython Usage](https://twython.readthedocs.io/en/latest/usage/advanced_usage.html) +- [Streaming with Twython](https://twython.readthedocs.io/en/latest/usage/streaming_api.html) + + +## Questions, Comments, etc? +My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. + +Or if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well. + +Follow us on Twitter: + +- [@ryanmcgrath](https://twitter.com/ryanmcgrath) +- [@mikehelmick](https://twitter.com/mikehelmick) + +## Want to help? +Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! diff --git a/README.rst b/README.rst deleted file mode 100644 index edddacc..0000000 --- a/README.rst +++ /dev/null @@ -1,222 +0,0 @@ -Twython -======= - - -.. image:: https://img.shields.io/pypi/v/twython.svg?style=flat-square - :target: https://pypi.python.org/pypi/twython - -.. image:: https://img.shields.io/pypi/dw/twython.svg?style=flat-square - :target: https://pypi.python.org/pypi/twython - -.. image:: https://img.shields.io/travis/ryanmcgrath/twython.svg?style=flat-square - :target: https://travis-ci.org/ryanmcgrath/twython - -.. image:: https://img.shields.io/coveralls/ryanmcgrath/twython/master.svg?style=flat-square - :target: https://coveralls.io/r/ryanmcgrath/twython?branch=master - -``Twython`` is the premier Python library providing an easy (and up-to-date) way to access Twitter data. Actively maintained and featuring support for Python 2.6+ and Python 3. It's been battle tested by companies, educational institutions and individuals alike. Try it today! - -Features --------- - -- Query data for: - - User information - - Twitter lists - - Timelines - - Direct Messages - - and anything found in `the docs `_ -- Image Uploading: - - Update user status with an image - - Change user avatar - - Change user background image - - Change user banner image -- OAuth 2 Application Only (read-only) Support -- Support for Twitter's Streaming API -- Seamless Python 3 support! - -Installation ------------- - -Install Twython via `pip `_ - -.. code-block:: bash - - $ pip install twython - -or, with `easy_install `_ - -.. code-block:: bash - - $ easy_install twython - -But, hey... `that's up to you `_. - -Or, if you want the code that is currently on GitHub - -.. code-block:: bash - - git clone git://github.com/ryanmcgrath/twython.git - cd twython - python setup.py install - -Documentation -------------- - -Documentation is available at https://twython.readthedocs.io/en/latest/ - -Starting Out ------------- - -First, you'll want to head over to https://apps.twitter.com and register an application! - -After you register, grab your applications ``Consumer Key`` and ``Consumer Secret`` from the application details tab. - -The most common type of authentication is Twitter user authentication using OAuth 1. If you're a web app planning to have users sign up with their Twitter account and interact with their timelines, updating their status, and stuff like that this **is** the authentication for you! - -First, you'll want to import Twython - -.. code-block:: python - - from twython import Twython - -Authentication -~~~~~~~~~~~~~~ - -Obtain Authorization URL -^^^^^^^^^^^^^^^^^^^^^^^^ - -Now, you'll want to create a Twython instance with your ``Consumer Key`` and ``Consumer Secret`` - - Only pass *callback_url* to *get_authentication_tokens* if your application is a Web Application - - Desktop and Mobile Applications **do not** require a callback_url - -.. code-block:: python - - APP_KEY = 'YOUR_APP_KEY' - APP_SECRET = 'YOUR_APP_SECRET' - - twitter = Twython(APP_KEY, APP_SECRET) - - auth = twitter.get_authentication_tokens(callback_url='http://mysite.com/callback') - -From the ``auth`` variable, save the ``oauth_token`` and ``oauth_token_secret`` for later use (these are not the final auth tokens). In Django or other web frameworks, you might want to store it to a session variable - -.. code-block:: python - - OAUTH_TOKEN = auth['oauth_token'] - OAUTH_TOKEN_SECRET = auth['oauth_token_secret'] - -Send the user to the authentication url, you can obtain it by accessing - -.. code-block:: python - - auth['auth_url'] - -Handling the Callback -^^^^^^^^^^^^^^^^^^^^^ - - If your application is a Desktop or Mobile Application *oauth_verifier* will be the PIN code - -After they authorize your application to access some of their account details, they'll be redirected to the callback url you specified in ``get_authentication_tokens`` - -You'll want to extract the ``oauth_verifier`` from the url. - -Django example: - -.. code-block:: python - - oauth_verifier = request.GET['oauth_verifier'] - -Now that you have the ``oauth_verifier`` stored to a variable, you'll want to create a new instance of Twython and grab the final user tokens - -.. code-block:: python - - twitter = Twython(APP_KEY, APP_SECRET, - OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - - final_step = twitter.get_authorized_tokens(oauth_verifier) - -Once you have the final user tokens, store them in a database for later use:: - - OAUTH_TOKEN = final_step['oauth_token'] - OAUTH_TOKEN_SECRET = final_step['oauth_token_secret'] - -For OAuth 2 (Application Only, read-only) authentication, see `our documentation `_ - -Dynamic Function Arguments -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. - -Basic Usage ------------ - -**Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py** - -Create a Twython instance with your application keys and the users OAuth tokens - -.. code-block:: python - - from twython import Twython - twitter = Twython(APP_KEY, APP_SECRET, - OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - -Authenticated Users Home Timeline -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Documentation: https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-home_timeline - -.. code-block:: python - - twitter.get_home_timeline() - -Updating Status -~~~~~~~~~~~~~~~ - -This method makes use of dynamic arguments, `read more about them `_ - -Documentation: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update - -.. code-block:: python - - twitter.update_status(status='See how easy using Twython is!') - -Searching -~~~~~~~~~ - - https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets says it takes "q" and "result_type" amongst other arguments - -.. code-block:: python - - twitter.search(q='twitter') - twitter.search(q='twitter', result_type='popular') - -Advanced Usage --------------- - -- `Advanced Twython Usage `_ -- `Streaming with Twython `_ - - -Notes ------ - -- Twython 3.0.0 has been injected with 1000mgs of pure awesomeness! OAuth 2 application authentication is now supported. And a *whole lot* more! See the `CHANGELOG `_ for more details! - -Questions, Comments, etc? -------------------------- - -My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. - -Or if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well. - -Follow us on Twitter: - -- `@ryanmcgrath `_ -- `@mikehelmick `_ - -Want to help? -------------- - -Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! diff --git a/examples/get_direct_messages.py b/examples/get_direct_messages.py index 5a27a06..fa80865 100644 --- a/examples/get_direct_messages.py +++ b/examples/get_direct_messages.py @@ -1,17 +1,5 @@ from twython import Twython, TwythonError twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) - get_list = twitter.get_direct_messages() -#Returns All Twitter DM information which is a lot in a list format - -dm_dict = get_list[0] -#Sets get_list to a dictionary, the number in the list is the direct message retrieved -#That means that 0 is the most recent and n-1 is the last DM revieved. -#You can cycle through all the numbers and it will return the text and the sender id of each - -print dm_dict['text'] -#Gets the text from the dictionary - -print dm_dict['sender']['id'] -#Gets the ID of the sender +print(get_list) diff --git a/examples/get_user_timeline.py b/examples/get_user_timeline.py index 0a1f4df..55ab427 100644 --- a/examples/get_user_timeline.py +++ b/examples/get_user_timeline.py @@ -7,4 +7,4 @@ try: except TwythonError as e: print e -print user_timeline +print(user_timeline) diff --git a/setup.py b/setup.py index 11cc2b4..0531221 100755 --- a/setup.py +++ b/setup.py @@ -31,8 +31,9 @@ setup( keywords='twitter search api tweet twython stream', description='Actively maintained, pure Python wrapper for the \ Twitter API. Supports both normal and streaming Twitter APIs', - long_description=open('README.rst').read() + '\n\n' + - open('HISTORY.rst').read(), + long_description=open('README.md').read() + '\n\n' + + open('HISTORY.md').read(), + long_description_content_type='text/markdown', include_package_data=True, packages=packages, classifiers=[ -- 2.39.5 From 7ce058e6fd3a7307e4ac88e855257a9e99cc7163 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Apr 2020 22:58:15 -0700 Subject: [PATCH 190/204] Resolve issues with Pypi publishing (RST -> MD) and bump to 3.8.0 --- HISTORY.rst => HISTORY.md | 0 setup.py | 12 +++++------- 2 files changed, 5 insertions(+), 7 deletions(-) rename HISTORY.rst => HISTORY.md (100%) diff --git a/HISTORY.rst b/HISTORY.md similarity index 100% rename from HISTORY.rst rename to HISTORY.md diff --git a/setup.py b/setup.py index 0531221..0c0e758 100755 --- a/setup.py +++ b/setup.py @@ -8,8 +8,8 @@ try: except ImportError: from distutils.core import setup -__author__ = 'Ryan McGrath ' -__version__ = '3.7.0' +__author__ = 'Ryan McGrath ' +__version__ = '3.8.0' packages = [ 'twython', @@ -26,13 +26,11 @@ setup( install_requires=['requests>=2.1.0', 'requests_oauthlib>=0.4.0'], author='Ryan McGrath', author_email='ryan@venodesigns.net', - license=open('LICENSE').read(), + license='MIT', url='https://github.com/ryanmcgrath/twython/tree/master', keywords='twitter search api tweet twython stream', - description='Actively maintained, pure Python wrapper for the \ - Twitter API. Supports both normal and streaming Twitter APIs', - long_description=open('README.md').read() + '\n\n' + - open('HISTORY.md').read(), + description='Actively maintained, pure Python wrapper for the Twitter API. Supports both normal and streaming Twitter APIs', + long_description=open('README.md', encoding='utf-8').read() + '\n\n' +open('HISTORY.md', encoding='utf-8').read(), long_description_content_type='text/markdown', include_package_data=True, packages=packages, -- 2.39.5 From ea2979c75f8f771a70617e607b8398809dba8dac Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Apr 2020 23:02:01 -0700 Subject: [PATCH 191/204] Remove this merge as numpy shouldn't be a dependency --- twython/compat.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/twython/compat.py b/twython/compat.py index 2ee7d37..7c049b0 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -9,7 +9,6 @@ Python 3 compatibility. """ import sys -import numpy as np _ver = sys.version_info @@ -30,7 +29,7 @@ if is_py2: str = unicode basestring = basestring - numeric_types = (int, long, float, np.int64, np.float64) + numeric_types = (int, long, float) elif is_py3: @@ -38,4 +37,4 @@ elif is_py3: str = str basestring = (str, bytes) - numeric_types = (int, float, np.int64, np.float64) + numeric_types = (int, float) -- 2.39.5 From b8d927df8eeff88717f3b7547461b637de87caf7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Apr 2020 23:08:52 -0700 Subject: [PATCH 192/204] Kill 2.6/2.7 --- .travis.yml | 3 --- tests/config.py | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6ec2958..231b15c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: python python: - - 2.6 - - 2.7 - 3.5 - 3.6 # Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs @@ -26,7 +24,6 @@ env: - ACCESS_TOKEN_B64=U2FsdGVkX18QdBhvMNshM4PGy04tU3HLwKP+nNSoNZHKsvGLjELcWEXN2LIu/T+yngX1vGONf9lo14ElnfS4k7sfhiru8phR4+rZuBVP3bDvC2A6fXJuhuLqNhBrWqg32WQewvxLWDWBoKmnvRHg5b74GHh+IN/12tU0cBF2HK8= install: - pip install -r requirements.txt - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi script: nosetests -v -w tests/ --logging-filter="twython" --with-cov --cov twython --cov-config .coveragerc --cov-report term-missing notifications: email: false diff --git a/tests/config.py b/tests/config.py index 8812b81..10ab3ea 100644 --- a/tests/config.py +++ b/tests/config.py @@ -2,10 +2,7 @@ import os import sys -if sys.version_info[0] == 2 and sys.version_info[1] == 6: - import unittest2 as unittest -else: - import unittest +import unittest app_key = os.environ.get('APP_KEY') app_secret = os.environ.get('APP_SECRET') -- 2.39.5 From cbfec150dfffb63abae8c0e6edd137d02a0fe417 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Apr 2020 23:28:35 -0700 Subject: [PATCH 193/204] Close #486 - given how long ago this was deprecated I'll assume it's not even used --- twython/streaming/types.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/twython/streaming/types.py b/twython/streaming/types.py index 81c5c07..27c9ea8 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -20,26 +20,6 @@ class TwythonStreamerTypes(object): self.streamer = streamer self.statuses = TwythonStreamerTypesStatuses(streamer) - def user(self, **params): - """Stream user - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/user - """ - url = 'https://userstream.twitter.com/%s/user.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - def site(self, **params): - """Stream site - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/site - """ - url = 'https://sitestream.twitter.com/%s/site.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - class TwythonStreamerTypesStatuses(object): """Class for different statuses endpoints -- 2.39.5 From 74c72f88fddf2623e23601785d8a80c1152989eb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Apr 2020 23:39:07 -0700 Subject: [PATCH 194/204] Remove old links in docs --- docs/_themes/basicstrap/customsidebar.html | 15 +-------------- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/docs/_themes/basicstrap/customsidebar.html b/docs/_themes/basicstrap/customsidebar.html index ac8c583..dbeab99 100644 --- a/docs/_themes/basicstrap/customsidebar.html +++ b/docs/_themes/basicstrap/customsidebar.html @@ -1,18 +1,5 @@ -

{{ _('Donate') }}

- -

-Find Twython useful? Consider supporting the author on Gittip: -

- -

- -

-

{{ _('Links') }}

\ No newline at end of file + diff --git a/docs/conf.py b/docs/conf.py index 8f3c3d2..3ffa5b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ copyright = u'2013, Ryan McGrath' # built documents. # # The short X.Y version. -version = '3.7.0' +version = '3.8.0' # The full version, including alpha/beta/rc tags. -release = '3.7.0' +release = '3.8.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -- 2.39.5 From ba1110d4b8c6c385bf59fe5318b4aa4726ca00f7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 3 Apr 2020 02:22:17 -0700 Subject: [PATCH 195/204] Update Manifest to fix installation --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index bcfd314..79f570a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README.rst HISTORY.rst LICENSE requirements.txt +include README.md HISTORY.md LICENSE requirements.txt recursive-include docs * prune docs/_build -- 2.39.5 From e6b5364d28ba3e7f3688bcdbdb8441e2d33d3ecf Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 3 Apr 2020 02:22:33 -0700 Subject: [PATCH 196/204] Bump version for patching manifest --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bff13d7..ff6dee8 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.8.0' +__version__ = '3.8.1' packages = [ 'twython', -- 2.39.5 From 02fb35651dd253254ff22c51c591d1bd0c9cf807 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 Apr 2020 16:21:59 -0700 Subject: [PATCH 197/204] Fixes #530, bump version to 3.8.2 --- setup.py | 2 +- twython/streaming/api.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ff6dee8..3742f24 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.8.1' +__version__ = '3.8.2' packages = [ 'twython', diff --git a/twython/streaming/api.py b/twython/streaming/api.py index dd4ae89..6073abb 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -86,8 +86,6 @@ class TwythonStreamer(object): # Set up type methods StreamTypes = TwythonStreamerTypes(self) self.statuses = StreamTypes.statuses - self.user = StreamTypes.user - self.site = StreamTypes.site self.connected = False -- 2.39.5 From 233b20a71057c98cba6f745ecfeada47c76bb67d Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sun, 12 Apr 2020 05:25:01 +0000 Subject: [PATCH 198/204] Fix deprecation warning regarding invalid escape sequences. --- twython/api.py | 4 ++-- twython/streaming/types.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/twython/api.py b/twython/api.py index bb10db0..76b6f28 100644 --- a/twython/api.py +++ b/twython/api.py @@ -430,7 +430,7 @@ class Twython(EndpointsMixin, object): @staticmethod def construct_api_url(api_url, **params): - """Construct a Twitter API url, encoded, with parameters + r"""Construct a Twitter API url, encoded, with parameters :param api_url: URL of the Twitter API endpoint you are attempting to construct @@ -469,7 +469,7 @@ class Twython(EndpointsMixin, object): return self.cursor(self.search, q=search_query, **params) def cursor(self, function, return_pages=False, **params): - """Returns a generator for results that match a specified query. + r"""Returns a generator for results that match a specified query. :param function: Instance of a Twython function (Twython.get_home_timeline, Twython.search) diff --git a/twython/streaming/types.py b/twython/streaming/types.py index 27c9ea8..5042d29 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -35,7 +35,7 @@ class TwythonStreamerTypesStatuses(object): self.params = None def filter(self, **params): - """Stream statuses/filter + r"""Stream statuses/filter :param \*\*params: Parameters to send with your stream request @@ -47,7 +47,7 @@ class TwythonStreamerTypesStatuses(object): self.streamer._request(url, 'POST', params=params) def sample(self, **params): - """Stream statuses/sample + r"""Stream statuses/sample :param \*\*params: Parameters to send with your stream request @@ -59,7 +59,7 @@ class TwythonStreamerTypesStatuses(object): self.streamer._request(url, params=params) def firehose(self, **params): - """Stream statuses/firehose + r"""Stream statuses/firehose :param \*\*params: Parameters to send with your stream request @@ -71,7 +71,7 @@ class TwythonStreamerTypesStatuses(object): self.streamer._request(url, params=params) def set_dynamic_filter(self, **params): - """Set/update statuses/filter + r"""Set/update statuses/filter :param \*\*params: Parameters to send with your stream request -- 2.39.5 From 33f46c087ec6c92dd325101169f3c5c0894d3b7c Mon Sep 17 00:00:00 2001 From: Hannes Date: Fri, 17 Jul 2020 11:50:06 +0200 Subject: [PATCH 199/204] update example for a post involving image editing Python 2 support was dropped from Twython, thanks! In Python3 we actually have to use BytesIO, see https://github.com/python-pillow/Pillow/issues/2205 --- docs/usage/advanced_usage.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/usage/advanced_usage.rst b/docs/usage/advanced_usage.rst index bdb0b23..df3bc48 100644 --- a/docs/usage/advanced_usage.rst +++ b/docs/usage/advanced_usage.rst @@ -59,15 +59,10 @@ with a status update. .. code-block:: python - # Assume you are working with a JPEG + # Assuming that you are working with a JPEG from PIL import Image - try: - # Python 3 - from io import StringIO - except ImportError: - # Python 2 - from StringIO import StringIO + from io import BytesIO photo = Image.open('/path/to/file/image.jpg') @@ -76,14 +71,13 @@ with a status update. height = int((float(photo.size[1]) * float(wpercent))) photo = photo.resize((basewidth, height), Image.ANTIALIAS) - image_io = StringIO.StringIO() + image_io = BytesIO() photo.save(image_io, format='JPEG') # If you do not seek(0), the image will be at the end of the file and # unable to be read image_io.seek(0) - response = twitter.upload_media(media=image_io) twitter.update_status(status='Checkout this cool image!', media_ids=[response['media_id']]) -- 2.39.5 From 1a54c15a71d054c6421e29cbcefcb9f2186cad49 Mon Sep 17 00:00:00 2001 From: Erik Zscheile Date: Tue, 26 Jan 2021 22:19:13 +0100 Subject: [PATCH 200/204] PEP 479: Change StopIteration handling inside generators --- twython/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/api.py b/twython/api.py index bb10db0..bd49c49 100644 --- a/twython/api.py +++ b/twython/api.py @@ -501,7 +501,7 @@ class Twython(EndpointsMixin, object): content = function(**params) if not content: - raise StopIteration + return if hasattr(function, 'iter_key'): results = content.get(function.iter_key) @@ -516,7 +516,7 @@ class Twython(EndpointsMixin, object): if function.iter_mode == 'cursor' and \ content['next_cursor_str'] == '0': - raise StopIteration + return try: if function.iter_mode == 'id': @@ -529,7 +529,7 @@ class Twython(EndpointsMixin, object): params = dict(parse_qsl(next_results.query)) else: # No more results - raise StopIteration + return else: # Twitter gives tweets in reverse chronological order: params['max_id'] = str(int(content[-1]['id_str']) - 1) -- 2.39.5 From 61c1ba9600986d8263af81b8cee954828bb7ce7c Mon Sep 17 00:00:00 2001 From: Erik Zscheile Date: Tue, 26 Jan 2021 22:22:56 +0100 Subject: [PATCH 201/204] PEP 479: add appropriate __future__ tag --- twython/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/twython/api.py b/twython/api.py index bd49c49..5de6c67 100644 --- a/twython/api.py +++ b/twython/api.py @@ -9,6 +9,7 @@ Twitter Authentication, and miscellaneous methods that are useful when dealing with the Twitter API """ +from __future__ import generator_stop import warnings import re -- 2.39.5 From 0b6f372620e864055f9c141c5d72b85a93230d50 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 6 Feb 2021 17:47:42 -0500 Subject: [PATCH 202/204] Update metadata to describe present support and CI testing --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3742f24..39a8e5f 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ setup( name='twython', version=__version__, install_requires=['requests>=2.1.0', 'requests_oauthlib>=0.4.0'], + python_requires='>=3.5', author='Ryan McGrath', author_email='ryan@venodesigns.net', license='MIT', @@ -41,9 +42,8 @@ setup( 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Communications :: Chat', 'Topic :: Internet', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', -- 2.39.5 From 4be4a504a30ad5e1b2ade398582e6a09e7d97759 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 16 Jul 2021 14:32:48 -0700 Subject: [PATCH 203/204] Push 3.9.0 --- setup.py | 4 ++-- twython/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 3742f24..307ad8b 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.8.2' +__version__ = '3.9.0' packages = [ 'twython', @@ -25,7 +25,7 @@ setup( version=__version__, install_requires=['requests>=2.1.0', 'requests_oauthlib>=0.4.0'], author='Ryan McGrath', - author_email='ryan@venodesigns.net', + author_email='ryan@rymc.io', license='MIT', url='https://github.com/ryanmcgrath/twython/tree/master', keywords='twitter search api tweet twython stream', diff --git a/twython/__init__.py b/twython/__init__.py index dc161d1..e01d85d 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.7.0' +__version__ = '3.9.0' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5 From 0c405604285364457f3c309969f11ba68163bd05 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 16 Jul 2021 15:33:17 -0700 Subject: [PATCH 204/204] 3.9.1 --- README.md | 8 +++++++- setup.py | 2 +- twython/__init__.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe0aa1b..853136e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,13 @@ Install Twython via pip: $ pip install twython ``` -Or, if you want the code that is currently on GitHub +If you're on a legacy project that needs Python 2.7 support, you can install the last version of Twython that supported 2.7: + +``` +pip install twython==3.7.0` +``` + +Or, if you want the code that is currently on GitHub: ```bash git clone git://github.com/ryanmcgrath/twython.git diff --git a/setup.py b/setup.py index 4c0cc12..a3600de 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ except ImportError: from distutils.core import setup __author__ = 'Ryan McGrath ' -__version__ = '3.9.0' +__version__ = '3.9.1' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index e01d85d..7d25ef3 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -19,7 +19,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '3.9.0' +__version__ = '3.9.1' from .api import Twython from .streaming import TwythonStreamer -- 2.39.5