diff --git a/Makefile b/Makefile index e8b8a9f1..010053ed 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ docs: $(MAKE) -C doc html lint: - flake8 twitter > violations.flake8.txt + pycodestyle --config={toxinidir}/setup.cfg twitter tests test: lint python setup.py test diff --git a/README.rst b/README.rst index 6bf1c1a3..a735cfab 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,7 @@ You can install python-twitter using:: $ pip install python-twitter - + If you are using python-twitter on Google App Engine, see `more information `_ about including 3rd party vendor library dependencies in your App Engine project. diff --git a/doc/changelog.rst b/doc/changelog.rst index cb3464f0..8256048b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -36,6 +36,8 @@ What's New * Google App Engine support has been reintegrated into the library. Check out `PR #383 `_. +* `video_info` is now available on a `twitter.models.Media` object, which allows access to video urls/bitrates/etc. in the `extended_entities` node of a tweet. + What's Changed -------------- @@ -99,3 +101,5 @@ ______________ * Updated examples, specifically ``examples/twitter-to-xhtml.py``, ``examples/view_friends.py``, ``examples/shorten_url.py`` * Updated ``get_access_token.py`` script to be python3 compatible. + +* :py:func:`twitter.api.Api.GetStreamFilter()` now accepts an optional languages parameter as a list. diff --git a/doc/changes_to_tweet_counting.rst b/doc/changes_to_tweet_counting.rst new file mode 100644 index 00000000..d1c9b249 --- /dev/null +++ b/doc/changes_to_tweet_counting.rst @@ -0,0 +1,79 @@ +REST API Changes +================= + +Information compiled on Sept 14, 2016. + +``statuses/update`` Endpoint +---------------------------- + +``auto_populate_reply_metadata`` ++++++++++++++++++++++++++++++++ + +* Default is ``false`` + +* Must have ``in_reply_to_status_id`` set. + +* Unknown what happens if not set. Probably error (does it get posted?) + +* If the status to which you're replying is deleted, tweet will fail to post. + +``exclude_reply_user_ids`` +++++++++++++++++++++++++++ + +* List of ``user_ids`` to remove from result of ``auto_populate_reply_metadata``. + +* Doesn't apply to the first ``user_id``. + +* If you try to remove it, this will be silently ignored by Twitter. + +``attachment_url`` +++++++++++++++++++ + +* Must be a status permalnk or a DM deep link. + +* If it's anything else and included in this parameter, Twitter will return an error. + + +Most Other Endpoints +-------------------- + +``tweet_mode`` +++++++++++++++ + +* Any endpoint that returns a tweet will accept this param. + +* Must be in ``['compat', 'extended']`` + +* If ``tweet_mode == 'compat'``, then no ``extended_tweet`` node in the json returned. + +* If ``tweet_mode == 'extended'``, then you'll get the ``extended_tweet`` node. + + +Errors +------ +* 44 -> URL passed to attachment_url is invalid + +* 385 -> Replied to deleted tweet or tweet not visible to you + +* 386 -> Too many attachments types (ie a GIF + quote tweet) + + +Streaming API +============= + +Everything is going to be compatibility mode for now; however **all** tweets with have an ``extended_tweet`` node, which will contain the new information. According to Twitter's documentation though, there's the possibility that this node may not exist. We should be careful about making assumptions here. + + +Changes to Models +================= + +Classic tweet: tweet with length < 140 char. +Extended tweet: tweet with extended entities and text > 140 chars. + +Twitter doesn't say if extended tweet with a total length of < 140 characters will be considered a "Classic tweet". They also state that an extended tweet shall have "text content [that] exceeds 140 characters in length", however this is contradictory to earlier statements about total text length retaining a hard max at 140 characters. + +There will be two rendering modes: Compatibility and Extended. If in compatibility mode and tweet is "classic", no changes to tweet JSON. If in Extended mode, the following will change: + +* ``text`` -> truncated version of the extended tweet's text + "..." + permalink to tweet. (Twitter is mute on whether an extended tweet's with (text + @mentions + urls) < 140 characters will have the @mentions + urls put back in ``text`` field.) + +* ``truncated`` -> gets set to ``True`` if extended tweet is rendered in compat mode. diff --git a/doc/conf.py b/doc/conf.py index b46d6802..4deeaac5 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -57,9 +57,9 @@ # built documents. # # The short X.Y version. -version = '3.1' +version = '3.2' # The full version, including alpha/beta/rc tags. -release = '3.1' +release = '3.2dev0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/examples/streaming/track_users.py b/examples/streaming/track_users.py index fab36d55..faa37565 100644 --- a/examples/streaming/track_users.py +++ b/examples/streaming/track_users.py @@ -16,9 +16,9 @@ # ---------------------------------------------------------------------- -# This file demonstrates how to track mentions of a specific set of users and -# archive those mentions to a local file. The output file will contain one -# JSON string per line per Tweet. +# This file demonstrates how to track mentions of a specific set of users in +# english language and archive those mentions to a local file. The output +# file will contain one JSON string per line per Tweet. # To use this example, replace the W/X/Y/Zs with your keys obtained from # Twitter, or uncomment the lines for getting an environment variable. If you @@ -52,6 +52,10 @@ '@twitterapi', '@support'] +# Languages to filter tweets by is a list. This will be joined by Twitter +# to return data mentioning tweets only in the english language. +LANGUAGES = ['en'] + # Since we're going to be using a streaming endpoint, there is no need to worry # about rate limits. api = Api(CONSUMER_KEY, @@ -64,7 +68,7 @@ def main(): with open('output.txt', 'a') as f: # api.GetStreamFilter will return a generator that yields one status # message (i.e., Tweet) at a time as a JSON dictionary. - for line in api.GetStreamFilter(track=USERS): + for line in api.GetStreamFilter(track=USERS, languages=LANGUAGES): f.write(json.dumps(line)) f.write('\n') diff --git a/examples/view_friends.py b/examples/view_friends.py index 4e7740bd..499849fe 100644 --- a/examples/view_friends.py +++ b/examples/view_friends.py @@ -36,10 +36,10 @@ # Create an Api instance. -api = twitter.Api(consumer_key='consumer_key', - consumer_secret='consumer_secret', - access_token_key='access_token', - access_token_secret='access_token_secret') +api = twitter.Api(consumer_key=CONSUMER_KEY, + consumer_secret=CONSUMER_SECRET, + access_token_key=ACCESS_TOKEN, + access_token_secret=ACCESS_TOKEN_SECRET) users = api.GetFriends() diff --git a/requirements.testing.txt b/requirements.testing.txt index d492598b..595062af 100644 --- a/requirements.testing.txt +++ b/requirements.testing.txt @@ -7,7 +7,6 @@ pytest pytest-cov pytest-runner mccabe -flake8 mock six coverage diff --git a/setup.cfg b/setup.cfg index be369735..6dd25c8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,8 +8,8 @@ ignore = violations.flake8.txt [flake8] -ignore = E111,E124,E126,E201,E202,E221,E241,E302,E501 +ignore = E111,E124,E126,E221,E501 [pep8] -ignore = E111,E124,E126,E128,E201,E202,E221,E226,E241,E301,E302,E303,E402,E501,W291 -max-line-length = 160 +ignore = E111,E124,E126,E221,E501 +max-line-length = 100 diff --git a/testdata/3.2/extended_tweet_in_compat_mode.json b/testdata/3.2/extended_tweet_in_compat_mode.json new file mode 100644 index 00000000..9069add3 --- /dev/null +++ b/testdata/3.2/extended_tweet_in_compat_mode.json @@ -0,0 +1 @@ +{"contributors": null, "in_reply_to_status_id_str": null, "lang": "en", "source": "Twitter Web Client", "in_reply_to_user_id": null, "possibly_sensitive_appealable": false, "coordinates": null, "truncated": true, "retweet_count": 0, "retweeted": false, "possibly_sensitive": false, "in_reply_to_user_id_str": null, "entities": {"symbols": [], "user_mentions": [], "urls": [{"expanded_url": "https://twitter.com/i/web/status/782737772490600448", "indices": [117, 140], "url": "https://t.co/et3OTOxWSa", "display_url": "twitter.com/i/web/status/7\u2026"}], "hashtags": []}, "geo": null, "is_quote_status": false, "favorite_count": 0, "id_str": "782737772490600448", "id": 782737772490600448, "created_at": "Mon Oct 03 00:23:22 +0000 2016", "text": "has more details about these changes. Thanks for making more expressive!writing requirements to python_twitt pytho\u2026 https://t.co/et3OTOxWSa", "place": null, "in_reply_to_screen_name": null, "in_reply_to_status_id": null, "favorited": false, "user": {"follow_request_sent": false, "protected": true, "default_profile_image": true, "profile_sidebar_fill_color": "000000", "favourites_count": 1, "utc_offset": null, "has_extended_profile": false, "lang": "en", "profile_image_url": "http://abs.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", "friends_count": 2, "profile_text_color": "000000", "geo_enabled": true, "profile_banner_url": "https://pbs.twimg.com/profile_banners/4012966701/1453123196", "verified": false, "listed_count": 1, "is_translator": false, "location": "", "entities": {"description": {"urls": []}}, "name": "notinourselves", "is_translation_enabled": false, "time_zone": null, "id_str": "4012966701", "profile_background_tile": false, "followers_count": 1, "profile_sidebar_border_color": "000000", "contributors_enabled": false, "following": false, "description": "", "url": null, "statuses_count": 84, "default_profile": false, "profile_link_color": "000000", "profile_image_url_https": "https://abs.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", "notifications": false, "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/736320724164448256/LgaAQoav.jpg", "screen_name": "notinourselves", "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/736320724164448256/LgaAQoav.jpg", "id": 4012966701, "profile_use_background_image": true, "created_at": "Wed Oct 21 23:53:04 +0000 2015", "profile_background_color": "000000"}} \ No newline at end of file diff --git a/testdata/3.2/extended_tweet_in_extended_mode.json b/testdata/3.2/extended_tweet_in_extended_mode.json new file mode 100644 index 00000000..c420ac2d --- /dev/null +++ b/testdata/3.2/extended_tweet_in_extended_mode.json @@ -0,0 +1 @@ +{"contributors": null, "in_reply_to_status_id_str": null, "lang": "en", "in_reply_to_user_id_str": null, "in_reply_to_user_id": null, "possibly_sensitive_appealable": false, "coordinates": null, "full_text": "has more details about these changes. Thanks for making more expressive!writing requirements to python_twitt python_twitter.egg-info/SOURCE https://t.co/JWSPztfoyt", "truncated": false, "retweet_count": 0, "retweeted": false, "possibly_sensitive": false, "entities": {"symbols": [], "media": [{"id_str": "782737766455119872", "type": "photo", "media_url_https": "https://pbs.twimg.com/media/CtzX2fnXEAAMAjK.jpg", "display_url": "pic.twitter.com/JWSPztfoyt", "sizes": {"large": {"w": 1024, "resize": "fit", "h": 1024}, "thumb": {"w": 150, "resize": "crop", "h": 150}, "small": {"w": 680, "resize": "fit", "h": 680}, "medium": {"w": 1024, "resize": "fit", "h": 1024}}, "expanded_url": "https://twitter.com/notinourselves/status/782737772490600448/photo/1", "indices": [141, 164], "id": 782737766455119872, "url": "https://t.co/JWSPztfoyt", "media_url": "http://pbs.twimg.com/media/CtzX2fnXEAAMAjK.jpg"}], "user_mentions": [], "urls": [], "hashtags": []}, "geo": null, "is_quote_status": false, "favorite_count": 0, "id_str": "782737772490600448", "extended_entities": {"media": [{"id_str": "782737766455119872", "type": "photo", "media_url_https": "https://pbs.twimg.com/media/CtzX2fnXEAAMAjK.jpg", "display_url": "pic.twitter.com/JWSPztfoyt", "sizes": {"large": {"w": 1024, "resize": "fit", "h": 1024}, "thumb": {"w": 150, "resize": "crop", "h": 150}, "small": {"w": 680, "resize": "fit", "h": 680}, "medium": {"w": 1024, "resize": "fit", "h": 1024}}, "expanded_url": "https://twitter.com/notinourselves/status/782737772490600448/photo/1", "indices": [141, 164], "id": 782737766455119872, "url": "https://t.co/JWSPztfoyt", "media_url": "http://pbs.twimg.com/media/CtzX2fnXEAAMAjK.jpg", "ext_alt_text": null}]}, "created_at": "Mon Oct 03 00:23:22 +0000 2016", "source": "Twitter Web Client", "place": null, "favorited": false, "in_reply_to_screen_name": null, "in_reply_to_status_id": null, "id": 782737772490600448, "display_text_range": [0, 140], "user": {"follow_request_sent": false, "protected": true, "default_profile_image": true, "profile_sidebar_fill_color": "000000", "favourites_count": 1, "utc_offset": null, "has_extended_profile": false, "lang": "en", "profile_image_url": "http://abs.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", "friends_count": 2, "profile_text_color": "000000", "geo_enabled": true, "profile_banner_url": "https://pbs.twimg.com/profile_banners/4012966701/1453123196", "verified": false, "listed_count": 1, "is_translator": false, "location": "", "entities": {"description": {"urls": []}}, "name": "notinourselves", "is_translation_enabled": false, "time_zone": null, "id_str": "4012966701", "profile_background_tile": false, "followers_count": 1, "profile_sidebar_border_color": "000000", "contributors_enabled": false, "following": false, "description": "", "url": null, "statuses_count": 84, "default_profile": false, "profile_link_color": "000000", "profile_image_url_https": "https://abs.twimg.com/sticky/default_profile_images/default_profile_2_normal.png", "notifications": false, "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/736320724164448256/LgaAQoav.jpg", "screen_name": "notinourselves", "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/736320724164448256/LgaAQoav.jpg", "id": 4012966701, "profile_use_background_image": true, "created_at": "Wed Oct 21 23:53:04 +0000 2015", "profile_background_color": "000000"}} \ No newline at end of file diff --git a/testdata/get_status_promoted_video_tweet.json b/testdata/get_status_promoted_video_tweet.json new file mode 100644 index 00000000..0643aa78 --- /dev/null +++ b/testdata/get_status_promoted_video_tweet.json @@ -0,0 +1 @@ +{"possibly_sensitive_appealable": false, "entities": {"user_mentions": [], "hashtags": [{"indices": [22, 29], "text": "amiibo"}, {"indices": [33, 41], "text": "Picross"}], "symbols": [], "urls": [{"display_url": "nintendo.com/games/detail/p\u2026", "url": "https://t.co/MjciohRcuW", "expanded_url": "http://www.nintendo.com/games/detail/picross-3d-round-2-3ds", "indices": [90, 113]}], "media": [{"type": "photo", "id": 778025997606105089, "url": "https://t.co/ibou4buFxe", "media_url": "http://pbs.twimg.com/media/CswaoY4UAAA8-Zj.jpg", "indices": [114, 137], "id_str": "778025997606105089", "display_url": "pic.twitter.com/ibou4buFxe", "expanded_url": "https://twitter.com/NintendoAmerica/status/778307811012780032/video/1", "sizes": {"large": {"resize": "fit", "w": 1280, "h": 720}, "small": {"resize": "fit", "w": 680, "h": 383}, "thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 1200, "h": 675}}, "media_url_https": "https://pbs.twimg.com/media/CswaoY4UAAA8-Zj.jpg"}]}, "extended_entities": {"media": [{"id": 778025997606105089, "url": "https://t.co/ibou4buFxe", "media_url": "http://pbs.twimg.com/media/CswaoY4UAAA8-Zj.jpg", "video_info": {"duration_millis": 62996, "aspect_ratio": [16, 9], "variants": [{"bitrate": 320000, "url": "https://video.twimg.com/amplify_video/778025997606105089/vid/320x180/5Qr0z_HeycC2DvRj.mp4", "content_type": "video/mp4"}, {"bitrate": 2176000, "url": "https://video.twimg.com/amplify_video/778025997606105089/vid/1280x720/mUiy98wFwECTRNxT.mp4", "content_type": "video/mp4"}, {"bitrate": 832000, "url": "https://video.twimg.com/amplify_video/778025997606105089/vid/640x360/SX_HepRw0MeH796L.mp4", "content_type": "video/mp4"}, {"url": "https://video.twimg.com/amplify_video/778025997606105089/pl/PX7Gx8TRhJyUZ2-L.m3u8", "content_type": "application/x-mpegURL"}, {"url": "https://video.twimg.com/amplify_video/778025997606105089/pl/PX7Gx8TRhJyUZ2-L.mpd", "content_type": "application/dash+xml"}]}, "ext_alt_text": null, "sizes": {"large": {"resize": "fit", "w": 1280, "h": 720}, "small": {"resize": "fit", "w": 680, "h": 383}, "thumb": {"resize": "crop", "w": 150, "h": 150}, "medium": {"resize": "fit", "w": 1200, "h": 675}}, "indices": [114, 137], "type": "video", "additional_media_info": {"title": "Picross 3D Round 2 - amiibo \"Hands-On\u201d Gameplay ", "description": "Unlock more puzzles in Picross 3D Round 2 with amiibo!", "call_to_actions": {"visit_site": {"url": "http://www.nintendo.com/games/detail/picross-3d-round-2-3ds"}}, "monetizable": false, "embeddable": true}, "id_str": "778025997606105089", "display_url": "pic.twitter.com/ibou4buFxe", "expanded_url": "https://twitter.com/NintendoAmerica/status/778307811012780032/video/1", "media_url_https": "https://pbs.twimg.com/media/CswaoY4UAAA8-Zj.jpg"}]}, "favorited": false, "text": "Puzzled on how to use #amiibo in #Picross 3D Round 2? Just follow these six simple steps!\nhttps://t.co/MjciohRcuW https://t.co/ibou4buFxe", "retweeted": false, "retweet_count": 119, "user": {"is_translator": false, "profile_image_url_https": "https://pbs.twimg.com/profile_images/745752686780387333/wsjpSx2K_normal.jpg", "url": "https://t.co/cMLmFbyXaL", "entities": {"description": {"urls": [{"display_url": "esrb.org", "url": "https://t.co/OgSR65P8OY", "expanded_url": "http://esrb.org", "indices": [103, 126]}]}, "url": {"urls": [{"display_url": "nintendo.com", "url": "https://t.co/cMLmFbyXaL", "expanded_url": "http://www.nintendo.com/", "indices": [0, 23]}]}}, "listed_count": 10347, "friends_count": 1350, "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/623621309210083328/e9ZICp8d.jpg", "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/623621309210083328/e9ZICp8d.jpg", "profile_use_background_image": true, "profile_link_color": "038543", "description": "Welcome to the official Nintendo profile for gaming news! We\u2019re listening, too. For ESRB ratings go to https://t.co/OgSR65P8OY", "favourites_count": 260, "protected": false, "profile_background_tile": false, "id_str": "5162861", "has_extended_profile": false, "profile_text_color": "333333", "verified": true, "follow_request_sent": false, "contributors_enabled": false, "lang": "en", "id": 5162861, "statuses_count": 11909, "notifications": false, "location": "", "created_at": "Wed Apr 18 22:43:15 +0000 2007", "name": "Nintendo of America", "is_translation_enabled": false, "default_profile_image": false, "profile_background_color": "ACDED6", "utc_offset": -25200, "geo_enabled": false, "profile_banner_url": "https://pbs.twimg.com/profile_banners/5162861/1476972565", "profile_sidebar_border_color": "FFFFFF", "screen_name": "NintendoAmerica", "profile_sidebar_fill_color": "F6F6F6", "profile_image_url": "http://pbs.twimg.com/profile_images/745752686780387333/wsjpSx2K_normal.jpg", "default_profile": false, "time_zone": "Pacific Time (US & Canada)", "followers_count": 5246308, "translator_type": "none", "following": false}, "id_str": "778307811012780032", "is_quote_status": false, "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "contributors": null, "id": 778307811012780032, "favorite_count": 609, "in_reply_to_screen_name": null, "geo": null, "created_at": "Tue Sep 20 19:00:17 +0000 2016", "source": "Twitter Web Client", "truncated": false, "lang": "en", "in_reply_to_user_id_str": null, "place": null, "coordinates": null, "in_reply_to_user_id": null, "possibly_sensitive": false} \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py index 396dcadb..86ab2268 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,6 +12,7 @@ ACCESS_TOKEN_KEY = os.getenv('ACCESS_TOKEN_KEY', None) ACCESS_TOKEN_SECRET = os.getenv('ACCESS_TOKEN_SECRET', None) + @unittest.skipIf(not CONSUMER_KEY and not CONSUMER_SECRET, "No tokens provided") class ApiTest(unittest.TestCase): def setUp(self): diff --git a/tests/test_api_30.py b/tests/test_api_30.py index 37446941..b966c70d 100644 --- a/tests/test_api_30.py +++ b/tests/test_api_30.py @@ -9,9 +9,11 @@ import twitter +import responses +from responses import GET, POST + warnings.filterwarnings('ignore', category=DeprecationWarning) -import responses DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') @@ -34,7 +36,7 @@ def setUp(self): access_token_key='test', access_token_secret='test', sleep_on_rate_limit=False, - chunk_size=500*1024) + chunk_size=500 * 1024) self.base_url = 'https://api.twitter.com/1.1' self._stderr = sys.stderr sys.stderr = ErrNull() @@ -70,12 +72,8 @@ def testSetAndClearCredentials(self): @responses.activate def testApiRaisesAuthErrors(self): - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/search/tweets.json?count=15&result_type=mixed&q=python', - body='', - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body='') + api = twitter.Api() api.SetCredentials(consumer_key='test', consumer_secret='test', @@ -88,11 +86,8 @@ def testApiRaisesAuthErrors(self): def testGetHelpConfiguration(self): with open('testdata/get_help_configuration.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/help/configuration.json', - body=resp_data, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetHelpConfiguration() self.assertEqual(resp.get('short_url_length_https'), 23) @@ -100,11 +95,8 @@ def testGetHelpConfiguration(self): def testGetShortUrlLength(self): with open('testdata/get_help_configuration.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/help/configuration.json', - body=resp_data, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetShortUrlLength() self.assertEqual(resp, 23) resp = self.api.GetShortUrlLength(https=True) @@ -114,12 +106,8 @@ def testGetShortUrlLength(self): def testGetSearch(self): with open('testdata/get_search.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/search/tweets.json?count=15&result_type=mixed&q=python', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetSearch(term='python') self.assertEqual(len(resp), 1) self.assertTrue(type(resp[0]), twitter.Status) @@ -140,12 +128,8 @@ def testGetSearch(self): def testGetSeachRawQuery(self): with open('testdata/get_search_raw.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/search/tweets.json?q=twitter%20&result_type=recent&since=2014-07-19&count=100', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetSearch(raw_query="q=twitter%20&result_type=recent&since=2014-07-19&count=100") self.assertTrue([type(status) is twitter.Status for status in resp]) self.assertTrue(['twitter' in status.text for status in resp]) @@ -154,12 +138,8 @@ def testGetSeachRawQuery(self): def testGetSearchGeocode(self): with open('testdata/get_search_geocode.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/search/tweets.json?result_type=mixed&count=15&geocode=37.781157%2C-122.398720%2C100mi&q=python', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetSearch( term="python", geocode=('37.781157', '-122.398720', '100mi')) @@ -178,12 +158,8 @@ def testGetSearchGeocode(self): def testGetUsersSearch(self): with open('testdata/get_users_search.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/users/search.json?count=20&q=python', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetUsersSearch(term='python') self.assertEqual(type(resp[0]), twitter.User) self.assertEqual(len(resp), 20) @@ -196,12 +172,8 @@ def testGetUsersSearch(self): def testGetTrendsCurrent(self): with open('testdata/get_trends_current.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/trends/place.json?id=1', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetTrendsCurrent() self.assertTrue(type(resp[0]) is twitter.Trend) @@ -210,8 +182,7 @@ def testGetHomeTimeline(self): with open('testdata/get_home_timeline.json') as f: resp_data = f.read() responses.add( - responses.GET, - 'https://api.twitter.com/1.1/statuses/home_timeline.json', + GET, 'https://api.twitter.com/1.1/statuses/home_timeline.json?tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -233,20 +204,17 @@ def testGetHomeTimeline(self): lambda: self.api.GetHomeTimeline( since_id='still infinity')) - - # TODO: Get data for this call against which we can test exclusions. - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/statuses/home_timeline.json?count=100&max_id=674674925823787008&trim_user=1', - body=resp_data, - match_querystring=True, - status=200) + @responses.activate + def testGetHomeTimelineWithExclusions(self): + with open('testdata/get_home_timeline.json') as f: + resp_data = f.read() + responses.add(GET, DEFAULT_URL, body=resp_data) self.assertTrue(self.api.GetHomeTimeline(count=100, trim_user=True, max_id=674674925823787008)) @responses.activate - def testGetUserTimeline(self): + def testGetUserTimelineByUserID(self): with open('testdata/get_user_timeline.json') as f: resp_data = f.read() responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) @@ -255,12 +223,12 @@ def testGetUserTimeline(self): self.assertTrue(type(resp[0].user) is twitter.User) self.assertEqual(resp[0].user.id, 673483) + @responses.activate + def testGetUserTimelineByScreenName(self): + with open('testdata/get_user_timeline.json') as f: + resp_data = f.read() responses.add( - responses.GET, - 'https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=dewitt', - body=resp_data, - match_querystring=True, - status=200) + GET, DEFAULT_URL, body=resp_data) resp = self.api.GetUserTimeline(screen_name='dewitt') self.assertEqual(resp[0].id, 675055636267298821) self.assertTrue(resp) @@ -269,8 +237,8 @@ def testGetUserTimeline(self): def testGetRetweets(self): with open('testdata/get_retweets.json') as f: resp_data = f.read() - responses.add( - responses.GET, DEFAULT_URL, body=resp_data, status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetRetweets(statusid=397) self.assertTrue(type(resp[0]) is twitter.Status) self.assertTrue(type(resp[0].user) is twitter.User) @@ -279,12 +247,8 @@ def testGetRetweets(self): def testGetRetweetsCount(self): with open('testdata/get_retweets_count.json') as f: resp_data = f.read() - responses.add( - responses.GET, - DEFAULT_URL, - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetRetweets(statusid=312, count=63) self.assertTrue(len(resp), 63) @@ -292,7 +256,8 @@ def testGetRetweetsCount(self): def testGetRetweeters(self): with open('testdata/get_retweeters.json') as f: resp_data = f.read() - responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetRetweeters(status_id=397) self.assertTrue(type(resp) is list) self.assertTrue(type(resp[0]) is int) @@ -303,7 +268,7 @@ def testGetBlocks(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/list.json?cursor=-1', + 'https://api.twitter.com/1.1/blocks/list.json?cursor=-1&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -311,7 +276,7 @@ def testGetBlocks(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/list.json?cursor=1524574483549312671', + 'https://api.twitter.com/1.1/blocks/list.json?cursor=1524574483549312671&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -339,12 +304,8 @@ def testGetBlocks(self): def testGetBlocksPaged(self): with open('testdata/get_blocks_1.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/blocks/list.json?cursor=1524574483549312671', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + ncur, pcur, resp = self.api.GetBlocksPaged(cursor=1524574483549312671) self.assertTrue( isinstance(resp, list), @@ -366,7 +327,7 @@ def testGetBlocksIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/ids.json?cursor=-1', + 'https://api.twitter.com/1.1/blocks/ids.json?cursor=-1&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -374,7 +335,7 @@ def testGetBlocksIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/blocks/ids.json?cursor=1524566179872860311', + 'https://api.twitter.com/1.1/blocks/ids.json?cursor=1524566179872860311&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -393,12 +354,8 @@ def testGetBlocksIDs(self): def testGetBlocksIDsPaged(self): with open('testdata/get_blocks_ids_1.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/blocks/ids.json?cursor=1524566179872860311', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + _, _, resp = self.api.GetBlocksIDsPaged(cursor=1524566179872860311) self.assertTrue( isinstance(resp, list), @@ -416,9 +373,8 @@ def testGetFriendIDs(self): with open('testdata/get_friend_ids_0.json') as f: resp_data = f.read() responses.add( - responses.GET, - '{base_url}/friends/ids.json?screen_name=EricHolthaus&count=5000&stringify_ids=False&cursor=-1'.format( - base_url=self.api.base_url), + GET, + 'https://api.twitter.com/1.1/friends/ids.json?count=5000&cursor=-1&stringify_ids=False&screen_name=EricHolthaus&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -428,8 +384,7 @@ def testGetFriendIDs(self): resp_data = f.read() responses.add( responses.GET, - '{base_url}/friends/ids.json?count=5000&screen_name=EricHolthaus&stringify_ids=False&cursor=1417903878302254556'.format( - base_url=self.api.base_url), + 'https://api.twitter.com/1.1/friends/ids.json?stringify_ids=False&count=5000&cursor=1417903878302254556&screen_name=EricHolthaus&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -448,13 +403,7 @@ def testGetFriendIDs(self): def testGetFriendIDsPaged(self): with open('testdata/get_friend_ids_0.json') as f: resp_data = f.read() - responses.add( - responses.GET, - '{base_url}/friends/ids.json?count=5000&cursor=-1&screen_name=EricHolthaus&stringify_ids=False'.format( - base_url=self.api.base_url), - body=resp_data, - match_querystring=True, - status=200) + responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) ncursor, pcursor, resp = self.api.GetFriendIDsPaged(screen_name='EricHolthaus') self.assertLessEqual(len(resp), 5000) @@ -465,13 +414,7 @@ def testGetFriendIDsPaged(self): def testGetFriendsPaged(self): with open('testdata/get_friends_paged.json') as f: resp_data = f.read() - responses.add( - responses.GET, - '{base_url}/friends/list.json?screen_name=codebear&count=200&cursor=-1&skip_status=False&include_user_entities=True'.format( - base_url=self.api.base_url), - body=resp_data, - match_querystring=True, - status=200) + responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) ncursor, pcursor, resp = self.api.GetFriendsPaged(screen_name='codebear', count=200) self.assertEqual(ncursor, 1494734862149901956) @@ -479,15 +422,11 @@ def testGetFriendsPaged(self): self.assertEqual(len(resp), 200) self.assertTrue(type(resp[0]) is twitter.User) + @responses.activate + def testGetFriendsPagedUID(self): with open('testdata/get_friends_paged_uid.json') as f: resp_data = f.read() - responses.add( - responses.GET, - '{base_url}/friends/list.json?user_id=12&skip_status=False&cursor=-1&include_user_entities=True&count=200'.format( - base_url=self.api.base_url), - body=resp_data, - match_querystring=True, - status=200) + responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) ncursor, pcursor, resp = self.api.GetFriendsPaged(user_id=12, count=200) self.assertEqual(ncursor, 1510410423140902959) @@ -495,15 +434,11 @@ def testGetFriendsPaged(self): self.assertEqual(len(resp), 200) self.assertTrue(type(resp[0]) is twitter.User) + @responses.activate + def testGetFriendsAdditionalParams(self): with open('testdata/get_friends_paged_additional_params.json') as f: resp_data = f.read() - responses.add( - responses.GET, - '{base_url}/friends/list.json?include_user_entities=True&user_id=12&count=200&cursor=-1&skip_status=True'.format( - base_url=self.api.base_url), - body=resp_data, - match_querystring=True, - status=200) + responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) ncursor, pcursor, resp = self.api.GetFriendsPaged(user_id=12, count=200, @@ -527,15 +462,8 @@ def testGetFriends(self): for i in range(0, 5): with open('testdata/get_friends_{0}.json'.format(i)) as f: resp_data = f.read() - endpoint = '/friends/list.json?screen_name=codebear&count=200&skip_status=False&include_user_entities=True&cursor={0}'.format(cursor) - - responses.add( - responses.GET, - '{base_url}{endpoint}'.format( - base_url=self.api.base_url, - endpoint=endpoint), - body=resp_data, match_querystring=True, status=200) - + endpoint = 'https://api.twitter.com/1.1/friends/list.json?count=200&tweet_mode=compat&include_user_entities=True&screen_name=codebear&skip_status=False&cursor={0}'.format(cursor) + responses.add(GET, endpoint, body=resp_data, match_querystring=True) cursor = json.loads(resp_data)['next_cursor'] resp = self.api.GetFriends(screen_name='codebear') @@ -545,14 +473,7 @@ def testGetFriends(self): def testGetFriendsWithLimit(self): with open('testdata/get_friends_0.json') as f: resp_data = f.read() - - responses.add( - responses.GET, - '{base_url}/friends/list.json?include_user_entities=True&skip_status=False&screen_name=codebear&count=200&cursor=-1'.format( - base_url=self.api.base_url), - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) resp = self.api.GetFriends(screen_name='codebear', total_count=200) self.assertEqual(len(resp), 200) @@ -574,7 +495,7 @@ def testGetFollowersIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?cursor=-1&stringify_ids=False&count=5000&screen_name=GirlsMakeGames', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&cursor=-1&stringify_ids=False&count=5000&screen_name=GirlsMakeGames', body=resp_data, match_querystring=True, status=200) @@ -584,7 +505,7 @@ def testGetFollowersIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?count=5000&screen_name=GirlsMakeGames&cursor=1482201362283529597&stringify_ids=False', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&count=5000&screen_name=GirlsMakeGames&cursor=1482201362283529597&stringify_ids=False', body=resp_data, match_querystring=True, status=200) @@ -606,7 +527,7 @@ def testGetFollowers(self): resp_data = f.read() responses.add( responses.GET, - '{base_url}/followers/list.json?include_user_entities=True&count=200&screen_name=himawari8bot&skip_status=False&cursor=-1'.format( + '{base_url}/followers/list.json?tweet_mode=compat&include_user_entities=True&count=200&screen_name=himawari8bot&skip_status=False&cursor=-1'.format( base_url=self.api.base_url), body=resp_data, match_querystring=True, @@ -617,7 +538,7 @@ def testGetFollowers(self): resp_data = f.read() responses.add( responses.GET, - '{base_url}/followers/list.json?include_user_entities=True&skip_status=False&count=200&screen_name=himawari8bot&cursor=1516850034842747602'.format( + '{base_url}/followers/list.json?tweet_mode=compat&include_user_entities=True&skip_status=False&count=200&screen_name=himawari8bot&cursor=1516850034842747602'.format( base_url=self.api.base_url), body=resp_data, match_querystring=True, @@ -631,13 +552,7 @@ def testGetFollowers(self): def testGetFollowersPaged(self): with open('testdata/get_followers_0.json') as f: resp_data = f.read() - responses.add( - responses.GET, - '{base_url}/followers/list.json?include_user_entities=True&count=200&screen_name=himawari8bot&skip_status=False&cursor=-1'.format( - base_url=self.api.base_url), - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) ncursor, pcursor, resp = self.api.GetFollowersPaged(screen_name='himawari8bot') @@ -651,7 +566,7 @@ def testGetFollowerIDsPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/followers/ids.json?count=5000&stringify_ids=False&cursor=-1&screen_name=himawari8bot', + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&count=5000&stringify_ids=False&cursor=-1&screen_name=himawari8bot', body=resp_data, match_querystring=True, status=200) @@ -667,8 +582,7 @@ def testGetFollowerIDsPaged(self): resp_data = f.read() responses.add( responses.GET, - '{base_url}/followers/ids.json?count=5000&stringify_ids=True&user_id=12&cursor=-1'.format( - base_url=self.api.base_url), + 'https://api.twitter.com/1.1/followers/ids.json?tweet_mode=compat&count=5000&stringify_ids=True&user_id=12&cursor=-1', body=resp_data, match_querystring=True, status=200) @@ -688,12 +602,8 @@ def testGetFollowerIDsPaged(self): def testUsersLookup(self): with open('testdata/users_lookup.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/users/lookup.json?user_id=718443', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.UsersLookup(user_id=[718443]) self.assertTrue(type(resp) is list) self.assertEqual(len(resp), 1) @@ -706,12 +616,8 @@ def testUsersLookup(self): def testGetUser(self): with open('testdata/get_user.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/users/show.json?user_id=718443', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetUser(user_id=718443) self.assertTrue(type(resp) is twitter.User) self.assertEqual(resp.screen_name, 'kesuke') @@ -721,12 +627,8 @@ def testGetUser(self): def testGetDirectMessages(self): with open('testdata/get_direct_messages.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/direct_messages.json', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetDirectMessages() self.assertTrue(type(resp) is list) direct_message = resp[0] @@ -737,12 +639,8 @@ def testGetDirectMessages(self): def testGetSentDirectMessages(self): with open('testdata/get_sent_direct_messages.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/direct_messages/sent.json', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetSentDirectMessages() self.assertTrue(type(resp) is list) direct_message = resp[0] @@ -754,12 +652,8 @@ def testGetSentDirectMessages(self): def testGetFavorites(self): with open('testdata/get_favorites.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/favorites/list.json?include_entities=True', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetFavorites() self.assertTrue(type(resp) is list) fav = resp[0] @@ -770,12 +664,8 @@ def testGetFavorites(self): def testGetMentions(self): with open('testdata/get_mentions.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/statuses/mentions_timeline.json', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetMentions() self.assertTrue(type(resp) is list) self.assertTrue([type(mention) is twitter.Status for mention in resp]) @@ -785,18 +675,14 @@ def testGetMentions(self): def testGetListTimeline(self): with open('testdata/get_list_timeline.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?slug=space-bots&owner_screen_name=inky', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetListTimeline(list_id=None, slug='space-bots', owner_screen_name='inky') self.assertTrue(type(resp) is list) self.assertTrue([type(status) is twitter.Status for status in resp]) - self.assertEqual(resp[0].id, 677891843946766336) + self.assertEqual(resp[0].id, 693191602957852676) self.assertRaises( twitter.TwitterError, @@ -816,7 +702,7 @@ def testPostUpdate(self): with open('testdata/post_update.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/statuses/update.json', body=resp_data, status=200) @@ -833,7 +719,7 @@ def testPostUpdateExtraParams(self): with open('testdata/post_update_extra_params.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/statuses/update.json', body=resp_data, status=200) @@ -852,11 +738,7 @@ def testPostUpdateExtraParams(self): def testVerifyCredentials(self): with open('testdata/verify_credentials.json') as f: resp_data = f.read() - responses.add( - responses.GET, - '{0}/account/verify_credentials.json'.format(self.api.base_url), - body=resp_data, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) resp = self.api.VerifyCredentials() self.assertEqual(type(resp), twitter.User) @@ -866,12 +748,8 @@ def testVerifyCredentials(self): def testVerifyCredentialsIncludeEmail(self): with open('testdata/get_verify_credentials_include_email.json') as f: resp_data = f.read() - responses.add( - responses.GET, - DEFAULT_URL, - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.VerifyCredentials(skip_status=True, include_email=True) self.assertTrue(isinstance(resp, twitter.User)) self.assertEqual(resp.email, 'test@example.com') @@ -879,7 +757,7 @@ def testVerifyCredentialsIncludeEmail(self): @responses.activate def testUpdateBanner(self): responses.add( - responses.POST, + POST, '{0}/account/update_profile_banner.json'.format(self.api.base_url), body=b'', status=201 @@ -890,7 +768,7 @@ def testUpdateBanner(self): @responses.activate def testUpdateBanner422Error(self): responses.add( - responses.POST, + POST, '{0}/account/update_profile_banner.json'.format(self.api.base_url), body=b'', status=422 @@ -907,7 +785,7 @@ def testUpdateBanner422Error(self): @responses.activate def testUpdateBanner400Error(self): responses.add( - responses.POST, + POST, '{0}/account/update_profile_banner.json'.format(self.api.base_url), body=b'', status=400 @@ -921,12 +799,8 @@ def testUpdateBanner400Error(self): def testGetMemberships(self): with open('testdata/get_memberships.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/lists/memberships.json?cursor=-1&count=20', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetMemberships() self.assertTrue(type(resp) is list) self.assertTrue([type(lst) is twitter.List for lst in resp]) @@ -938,7 +812,7 @@ def testGetListsList(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/list.json', + 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -951,7 +825,7 @@ def testGetListsList(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/list.json?screen_name=inky', + 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=compat&screen_name=inky', body=resp_data, match_querystring=True, status=200) @@ -964,7 +838,7 @@ def testGetListsList(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/list.json?user_id=13148', + 'https://api.twitter.com/1.1/lists/list.json?tweet_mode=compat&user_id=13148', body=resp_data, match_querystring=True, status=200) @@ -977,12 +851,8 @@ def testGetListsList(self): def testGetLists(self): with open('testdata/get_lists.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/lists/ownerships.json?cursor=-1&count=20', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetLists() self.assertTrue(resp) lst = resp[0] @@ -997,7 +867,7 @@ def testGetListMembers(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=False&skip_status=False&list_id=93527328&cursor=-1', + 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=False&skip_status=False&list_id=93527328&cursor=-1&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1006,7 +876,7 @@ def testGetListMembers(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=False&skip_status=False&cursor=4611686020936348428&list_id=93527328', + 'https://api.twitter.com/1.1/lists/members.json?list_id=93527328&skip_status=False&include_entities=False&count=100&tweet_mode=compat&cursor=4611686020936348428', body=resp_data, match_querystring=True, status=200) @@ -1020,7 +890,7 @@ def testGetListMembersPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=True&skip_status=False&cursor=4611686020936348428&list_id=93527328', + 'https://api.twitter.com/1.1/lists/members.json?count=100&include_entities=True&cursor=4611686020936348428&list_id=93527328&skip_status=False&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1031,15 +901,15 @@ def testGetListMembersPaged(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/members.json?count=100&skip_status=True&include_entities=False&cursor=4611686020936348428&list_id=93527328', + 'https://api.twitter.com/1.1/lists/members.json?count=100&tweet_mode=compat&cursor=4611686020936348428&list_id=93527328&skip_status=True&include_entities=False', body=resp_data, match_querystring=True, status=200) _, _, resp = self.api.GetListMembersPaged(list_id=93527328, - cursor=4611686020936348428, - skip_status=True, - include_entities=False, - count=100) + cursor=4611686020936348428, + skip_status=True, + include_entities=False, + count=100) self.assertFalse(resp[0].status) @responses.activate @@ -1048,7 +918,7 @@ def testGetListTimeline(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?&list_id=229581524', + 'https://api.twitter.com/1.1/lists/statuses.json?&list_id=229581524&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1059,7 +929,7 @@ def testGetListTimeline(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?since_id=692829211019575296&owner_screen_name=notinourselves&slug=test&max_id=692980243339071488', + 'https://api.twitter.com/1.1/lists/statuses.json?owner_screen_name=notinourselves&slug=test&max_id=692980243339071488&tweet_mode=compat&since_id=692829211019575296', body=resp_data, match_querystring=True, status=200) @@ -1084,7 +954,7 @@ def testGetListTimeline(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/statuses.json?count=13&slug=test&owner_id=4012966701&include_rts=False&include_entities=False', + 'https://api.twitter.com/1.1/lists/statuses.json?include_rts=False&count=13&tweet_mode=compat&include_entities=False&slug=test&owner_id=4012966701', body=resp_data, match_querystring=True, status=200) @@ -1094,15 +964,13 @@ def testGetListTimeline(self): include_entities=False, include_rts=False) self.assertEqual(len(resp), 13) - # TODO: test the other exclusions, but my bots don't retweet and - # twitter.status.Status doesn't include entities node? @responses.activate def testCreateList(self): with open('testdata/post_create_list.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/create.json', body=resp_data, match_querystring=True, @@ -1120,7 +988,7 @@ def testDestroyList(self): with open('testdata/post_destroy_list.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/destroy.json', body=resp_data, match_querystring=True, @@ -1134,7 +1002,7 @@ def testCreateSubscription(self): with open('testdata/post_create_subscription.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/subscribers/create.json', body=resp_data, match_querystring=True, @@ -1148,7 +1016,7 @@ def testDestroySubscription(self): with open('testdata/post_destroy_subscription.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/subscribers/destroy.json', body=resp_data, match_querystring=True, @@ -1164,7 +1032,7 @@ def testShowSubscription(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/subscribers/show.json?user_id=4040207472&list_id=189643778', + 'https://api.twitter.com/1.1/lists/subscribers/show.json?tweet_mode=compat&user_id=4040207472&list_id=189643778', body=resp_data, match_querystring=True, status=200) @@ -1180,7 +1048,7 @@ def testShowSubscription(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/subscribers/show.json?list_id=189643778&screen_name=__jcbl__', + 'https://api.twitter.com/1.1/lists/subscribers/show.json?list_id=189643778&tweet_mode=compat&screen_name=__jcbl__', body=resp_data, match_querystring=True, status=200) @@ -1195,7 +1063,7 @@ def testShowSubscription(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/subscribers/show.json?include_entities=True&list_id=18964377&skip_status=True&screen_name=__jcbl__', + 'https://api.twitter.com/1.1/lists/subscribers/show.json?include_entities=True&tweet_mode=compat&list_id=18964377&skip_status=True&screen_name=__jcbl__', body=resp_data, match_querystring=True, status=200) @@ -1209,12 +1077,8 @@ def testShowSubscription(self): def testGetSubscriptions(self): with open('testdata/get_get_subscriptions.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/lists/subscriptions.json?count=20&cursor=-1', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetSubscriptions() self.assertEqual(len(resp), 1) self.assertEqual(resp[0].name, 'space bots') @@ -1223,12 +1087,8 @@ def testGetSubscriptions(self): def testGetSubscriptionsSN(self): with open('testdata/get_get_subscriptions_uid.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/lists/subscriptions.json?count=20&cursor=-1&screen_name=inky', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetSubscriptions(screen_name='inky') self.assertEqual(len(resp), 20) self.assertTrue([isinstance(l, twitter.List) for l in resp]) @@ -1239,7 +1099,7 @@ def testGetMemberships(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1', + 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1251,7 +1111,7 @@ def testGetMemberships(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&screen_name=himawari8bot', + 'https://api.twitter.com/1.1/lists/memberships.json?count=20&cursor=-1&screen_name=himawari8bot&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1264,7 +1124,7 @@ def testCreateListsMember(self): with open('testdata/post_create_lists_member.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/members/create.json', body=resp_data, match_querystring=True, @@ -1279,7 +1139,7 @@ def testCreateListsMemberMultiple(self): with open('testdata/post_create_lists_member_multiple.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/members/create_all.json', body=resp_data, match_querystring=True, @@ -1295,7 +1155,7 @@ def testDestroyListsMember(self): with open('testdata/post_destroy_lists_member.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/members/destroy.json', body=resp_data, match_querystring=True, @@ -1310,7 +1170,7 @@ def testDestroyListsMemberMultiple(self): with open('testdata/post_destroy_lists_member_multiple.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/lists/members/destroy_all.json', body=resp_data, match_querystring=True, @@ -1327,7 +1187,7 @@ def testPostUpdateWithMedia(self): with open('testdata/post_upload_media_simple.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://upload.twitter.com/1.1/media/upload.json', body=resp_data, match_querystring=True, @@ -1337,7 +1197,7 @@ def testPostUpdateWithMedia(self): with open('testdata/post_update_media_id.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/statuses/update.json?media_ids=697007311538229248', body=resp_data, match_querystring=True, @@ -1360,7 +1220,7 @@ def testPostUpdateWithMedia(self): # Media ID as list of ints resp = self.api.PostUpdate(media=[697007311538229248], status='test') responses.add( - responses.POST, + POST, "https://api.twitter.com/1.1/statuses/update.json?media_ids=697007311538229248,697007311538229249", body=resp_data, match_querystring=True, @@ -1375,26 +1235,26 @@ def testLookupFriendship(self): responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12', + 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12,6385432', + 'https://api.twitter.com/1.1/friendships/lookup.json?user_id=12,6385432&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack', + 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack,dickc', + 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=jack,dickc&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1405,7 +1265,7 @@ def testLookupFriendship(self): self.assertEqual(resp[0].following, False) self.assertEqual(resp[0].followed_by, False) - # If any of the following produce an unexpect result, the test will + # If any of the following produce an unexpected result, the test will # fail on a request to a URL that hasn't been set by responses: test_user = twitter.User(id=12, screen_name='jack') test_user2 = twitter.User(id=6385432, screen_name='dickc') @@ -1428,12 +1288,8 @@ def testLookupFriendship(self): def testLookupFriendshipMute(self): with open('testdata/get_friendships_lookup_muting.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=dickc', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.LookupFriendship(screen_name='dickc') self.assertEqual(resp[0].blocking, False) self.assertEqual(resp[0].muting, True) @@ -1442,12 +1298,8 @@ def testLookupFriendshipMute(self): def testLookupFriendshipBlockMute(self): with open('testdata/get_friendships_lookup_muting_blocking.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/friendships/lookup.json?screen_name=dickc', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.LookupFriendship(screen_name='dickc') self.assertEqual(resp[0].muting, True) self.assertEqual(resp[0].blocking, True) @@ -1455,7 +1307,7 @@ def testLookupFriendshipBlockMute(self): @responses.activate def testPostMediaMetadata(self): responses.add( - responses.POST, + POST, 'https://upload.twitter.com/1.1/media/metadata/create.json', body=b'', status=200) @@ -1469,16 +1321,17 @@ def testPostMediaMetadata(self): def testGetStatusWithExtAltText(self): with open('testdata/get_status_ext_alt.json') as f: resp_data = f.read() - responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetStatus(status_id=724441953534877696) self.assertEqual(resp.media[0].ext_alt_text, "\u201cJon Snow is dead.\u2026\u201d from \u201cGAME OF THRONES SEASON 6 EPISODES\u201d by HBO PR.") - @responses.activate def testGetStatus(self): with open('testdata/get_status.json') as f: resp_data = f.read() - responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetStatus(status_id=397) self.assertTrue(type(resp) is twitter.Status) @@ -1494,24 +1347,24 @@ def testGetStatus(self): def testGetStatusExtraParams(self): with open('testdata/get_status_extra_params.json') as f: resp_data = f.read() - responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetStatus(status_id=397, trim_user=True, include_entities=False) self.assertFalse(resp.user.screen_name) - @responses.activate def testGetStatusOembed(self): with open('testdata/get_status_oembed.json') as f: resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/statuses/oembed.json?id=397', + 'https://api.twitter.com/1.1/statuses/oembed.json?tweet_mode=compat&id=397', body=resp_data, match_querystring=True, status=200) responses.add( responses.GET, - 'https://api.twitter.com/1.1/statuses/oembed.json?url=https://twitter.com/jack/statuses/397', + 'https://api.twitter.com/1.1/statuses/oembed.json?tweet_mode=compat&url=https://twitter.com/jack/statuses/397', body=resp_data, match_querystring=True, status=200) @@ -1541,7 +1394,7 @@ def testGetMutes(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=-1&include_entities=True', + 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=-1&tweet_mode=compat&include_entities=True', body=resp_data, match_querystring=True, status=200) @@ -1551,7 +1404,7 @@ def testGetMutes(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=1535206520056388207&include_entities=True', + 'https://api.twitter.com/1.1/mutes/users/list.json?cursor=1535206520056388207&include_entities=True&tweet_mode=compat', body=resp_data, match_querystring=True, status=200) @@ -1559,7 +1412,6 @@ def testGetMutes(self): self.assertEqual(len(resp), 82) self.assertTrue(isinstance(resp[0], twitter.User)) - @responses.activate def testGetMutesIDs(self): # First iteration of the loop to get all the user's mutes @@ -1567,7 +1419,7 @@ def testGetMutesIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/ids.json?cursor=-1', + 'https://api.twitter.com/1.1/mutes/users/ids.json?tweet_mode=compat&cursor=-1', body=resp_data, match_querystring=True, status=200) @@ -1577,7 +1429,7 @@ def testGetMutesIDs(self): resp_data = f.read() responses.add( responses.GET, - 'https://api.twitter.com/1.1/mutes/users/ids.json?cursor=1535206520056565155', + 'https://api.twitter.com/1.1/mutes/users/ids.json?tweet_mode=compat&cursor=1535206520056565155', body=resp_data, match_querystring=True, status=200) @@ -1590,7 +1442,7 @@ def testCreateBlock(self): with open('testdata/post_blocks_create.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/blocks/create.json', body=resp_data, match_querystring=True, @@ -1608,7 +1460,7 @@ def testDestroyBlock(self): with open('testdata/post_blocks_destroy.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/blocks/destroy.json', body=resp_data, match_querystring=True, @@ -1626,7 +1478,7 @@ def testCreateMute(self): with open('testdata/post_mutes_users_create.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/mutes/users/create.json', body=resp_data, match_querystring=True, @@ -1644,7 +1496,7 @@ def testDestroyMute(self): with open('testdata/post_mutes_users_destroy.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/mutes/users/destroy.json', body=resp_data, match_querystring=True, @@ -1670,7 +1522,7 @@ def testMuteBlockParamsAndErrors(self): with open('testdata/post_mutes_users_create_skip_status.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, 'https://api.twitter.com/1.1/mutes/users/create.json', body=resp_data, match_querystring=True, @@ -1683,7 +1535,7 @@ def testMuteBlockParamsAndErrors(self): def testPostUploadMediaChunkedInit(self): with open('testdata/post_upload_chunked_INIT.json') as f: resp_data = f.read() - responses.add(responses.POST, DEFAULT_URL, body=resp_data, status=200) + responses.add(POST, DEFAULT_URL, body=resp_data, status=200) with open('testdata/corgi.gif', 'rb') as fp: resp = self.api._UploadMediaChunkedInit(fp) @@ -1694,7 +1546,7 @@ def testPostUploadMediaChunkedInit(self): def testPostUploadMediaChunkedAppend(self): media_fp, filename, _, _ = twitter.twitter_utils.parse_media_file( 'testdata/corgi.gif') - responses.add(responses.POST, DEFAULT_URL, body='', status=200) + responses.add(POST, DEFAULT_URL, body='', status=200) resp = self.api._UploadMediaChunkedAppend(media_id=737956420046356480, media_fp=media_fp, @@ -1718,8 +1570,7 @@ def testPostUploadMediaChunkedAppendNonASCIIFilename(self): def testPostUploadMediaChunkedFinalize(self): with open('testdata/post_upload_chunked_FINAL.json') as f: resp_data = f.read() - - responses.add(responses.POST, DEFAULT_URL, body=resp_data, status=200) + responses.add(POST, DEFAULT_URL, body=resp_data, status=200) resp = self.api._UploadMediaChunkedFinalize(media_id=737956420046356480) self.assertEqual(len(responses.calls), 1) @@ -1729,12 +1580,8 @@ def testPostUploadMediaChunkedFinalize(self): def testGetUserSuggestionCategories(self): with open('testdata/get_user_suggestion_categories.json') as f: resp_data = f.read() - responses.add( - responses.GET, - 'https://api.twitter.com/1.1/users/suggestions.json', - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetUserSuggestionCategories() self.assertTrue(type(resp[0]) is twitter.Category) @@ -1743,6 +1590,7 @@ def testGetUserSuggestion(self): with open('testdata/get_user_suggestion.json') as f: resp_data = f.read() responses.add(responses.GET, DEFAULT_URL, body=resp_data, status=200) + category = twitter.Category(name='Funny', slug='funny', size=20) resp = self.api.GetUserSuggestion(category=category) self.assertTrue(type(resp[0]) is twitter.User) @@ -1751,12 +1599,8 @@ def testGetUserSuggestion(self): def testGetUserTimeSinceMax(self): with open('testdata/get_user_timeline_sincemax.json') as f: resp_data = f.read() - responses.add( - responses.GET, - DEFAULT_URL, - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetUserTimeline(user_id=12, since_id=757782013914951680, max_id=758097930670645248) self.assertEqual(len(resp), 6) @@ -1764,12 +1608,8 @@ def testGetUserTimeSinceMax(self): def testGetUserTimelineCount(self): with open('testdata/get_user_timeline_count.json') as f: resp_data = f.read() - responses.add( - responses.GET, - DEFAULT_URL, - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.GetUserTimeline(user_id=12, count=63) self.assertEqual(len(resp), 63) @@ -1778,7 +1618,7 @@ def testDestroyStatus(self): with open('testdata/post_destroy_status.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, DEFAULT_URL, body=resp_data, match_querystring=True, @@ -1791,7 +1631,8 @@ def testDestroyStatus(self): def testCreateFavorite(self): with open('testdata/post_create_favorite.json') as f: resp_data = f.read() - responses.add(responses.POST, DEFAULT_URL, body=resp_data, status=200) + responses.add(POST, DEFAULT_URL, body=resp_data, status=200) + resp = self.api.CreateFavorite(status_id=757283981683412992) self.assertEqual(resp.id, 757283981683412992) status = twitter.models.Status(id=757283981683412992) @@ -1802,7 +1643,8 @@ def testCreateFavorite(self): def testDestroyFavorite(self): with open('testdata/post_destroy_favorite.json') as f: resp_data = f.read() - responses.add(responses.POST, DEFAULT_URL, body=resp_data, status=200) + responses.add(POST, DEFAULT_URL, body=resp_data, status=200) + resp = self.api.DestroyFavorite(status_id=757283981683412992) self.assertEqual(resp.id, 757283981683412992) status = twitter.models.Status(id=757283981683412992) @@ -1814,7 +1656,7 @@ def testPostDirectMessage(self): with open('testdata/post_post_direct_message.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, DEFAULT_URL, body=resp_data, match_querystring=True, @@ -1825,6 +1667,7 @@ def testPostDirectMessage(self): resp = self.api.PostDirectMessage(text="test message", screen_name="__jcbl__") self.assertEqual(resp.sender_id, 4012966701) self.assertEqual(resp.recipient_id, 372018022) + self.assertTrue(resp._json) self.assertRaises( twitter.TwitterError, @@ -1835,7 +1678,7 @@ def testDestroyDirectMessage(self): with open('testdata/post_destroy_direct_message.json') as f: resp_data = f.read() responses.add( - responses.POST, + POST, DEFAULT_URL, body=resp_data, match_querystring=True, @@ -1846,12 +1689,8 @@ def testDestroyDirectMessage(self): def testShowFriendship(self): with open('testdata/get_show_friendship.json') as f: resp_data = f.read() - responses.add( - responses.GET, - DEFAULT_URL, - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + resp = self.api.ShowFriendship(source_user_id=4012966701, target_user_id=372018022) self.assertTrue(resp['relationship']['target'].get('following', None)) @@ -1866,9 +1705,10 @@ def testShowFriendship(self): twitter.TwitterError, lambda: self.api.ShowFriendship(target_screen_name='__jcbl__') ) + @responses.activate def test_UpdateBackgroundImage_deprecation(self): - responses.add(responses.POST, DEFAULT_URL, body='{}', status=200) + responses.add(POST, DEFAULT_URL, body='{}', status=200) warnings.simplefilter("always") with warnings.catch_warnings(record=True) as w: resp = self.api.UpdateBackgroundImage(image='testdata/168NQ.jpg') diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py new file mode 100644 index 00000000..22c6a47b --- /dev/null +++ b/tests/test_error_handling.py @@ -0,0 +1,38 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import re +import sys +import unittest +import warnings + +import twitter +import responses +from responses import GET, POST + +warnings.filterwarnings('ignore', category=DeprecationWarning) + +DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') +BODY = b'{"request":"\\/1.1\\/statuses\\/user_timeline.json","error":"Not authorized."}' + + +class ApiTest(unittest.TestCase): + + def setUp(self): + self.api = twitter.Api( + consumer_key='test', + consumer_secret='test', + access_token_key='test', + access_token_secret='test', + sleep_on_rate_limit=False, + chunk_size=500 * 1024) + + @responses.activate + def testGetShortUrlLength(self): + responses.add(GET, DEFAULT_URL, body=BODY, status=401) + + try: + resp = self.api.GetUserTimeline(screen_name="twitter") + except twitter.TwitterError as e: + self.assertEqual(e.message, "Not authorized.") diff --git a/tests/test_media.py b/tests/test_media.py index 6b9be3c7..e158bce4 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -1,7 +1,10 @@ -import twitter +# -*- coding: utf-8 -*- + import json import unittest +import twitter + class MediaTest(unittest.TestCase): SIZES = {'large': {'h': 175, 'resize': 'fit', 'w': 333}, @@ -10,7 +13,7 @@ class MediaTest(unittest.TestCase): 'thumb': {'h': 150, 'resize': 'crop', 'w': 150}} RAW_JSON = '''{"display_url": "pic.twitter.com/lX5LVZO", "expanded_url": "http://twitter.com/fakekurrik/status/244204973972410368/photo/1", "id": 244204973989187584, "id_str": "244204973989187584", "indices": [44,63], "media_url": "http://pbs.twimg.com/media/A2OXIUcCUAAXj9k.png", "media_url_https": "https://pbs.twimg.com/media/A2OXIUcCUAAXj9k.png", "sizes": {"large": {"h": 175, "resize": "fit", "w": 333}, "medium": {"h": 175, "resize": "fit", "w": 333}, "small": {"h": 175, "resize": "fit", "w": 333}, "thumb": {"h": 150, "resize": "crop", "w": 150}}, "type": "photo", "url": "http://t.co/lX5LVZO"}''' SAMPLE_JSON = '''{"display_url": "pic.twitter.com/lX5LVZO", "expanded_url": "http://twitter.com/fakekurrik/status/244204973972410368/photo/1", "id": 244204973989187584, "media_url": "http://pbs.twimg.com/media/A2OXIUcCUAAXj9k.png", "media_url_https": "https://pbs.twimg.com/media/A2OXIUcCUAAXj9k.png", "sizes": {"large": {"h": 175, "resize": "fit", "w": 333}, "medium": {"h": 175, "resize": "fit", "w": 333}, "small": {"h": 175, "resize": "fit", "w": 333}, "thumb": {"h": 150, "resize": "crop", "w": 150}}, "type": "photo", "url": "http://t.co/lX5LVZO"}''' -# '''{"display_url": "pic.twitter.com/lX5LVZO", "expanded_url": "http://twitter.com/fakekurrik/status/244204973972410368/photo/1", "id": 244204973989187584, "media_url": "http://pbs.twimg.com/media/A2OXIUcCUAAXj9k.png", "media_url_https": "https://pbs.twimg.com/media/A2OXIUcCUAAXj9k.png", "type": "photo", "url": "http://t.co/lX5LVZO"}''' + def _GetSampleMedia(self): return twitter.Media( id=244204973989187584, @@ -103,3 +106,14 @@ def testNewFromJsonDict(self): data = json.loads(MediaTest.RAW_JSON) media = twitter.Media.NewFromJsonDict(data) self.assertEqual(self._GetSampleMedia(), media) + + def test_media_info(self): + with open('testdata/get_status_promoted_video_tweet.json', 'r') as f: + tweet = twitter.Status.NewFromJsonDict(json.loads(f.read())) + media = tweet.media[0] + self.assertTrue(isinstance(tweet.media, list)) + self.assertTrue(media.video_info) + self.assertTrue(media.video_info.get('variants', None)) + self.assertTrue( + media.video_info.get('variants', None)[0]['url'], + 'https://video.twimg.com/amplify_video/778025997606105089/vid/320x180/5Qr0z_HeycC2DvRj.mp4') diff --git a/tests/test_parse_tweet.py b/tests/test_parse_tweet.py index 6548a436..66cdb4d5 100644 --- a/tests/test_parse_tweet.py +++ b/tests/test_parse_tweet.py @@ -3,6 +3,7 @@ import unittest import twitter + class ParseTest(unittest.TestCase): """ Test the ParseTweet class """ diff --git a/tests/test_rate_limit.py b/tests/test_rate_limit.py index d93ebe70..9774f1c2 100644 --- a/tests/test_rate_limit.py +++ b/tests/test_rate_limit.py @@ -8,6 +8,7 @@ import twitter import responses +from responses import GET, POST warnings.filterwarnings('ignore', category=DeprecationWarning) DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') @@ -48,14 +49,8 @@ def tearDown(self): def testInitializeRateLimit(self): with open('testdata/ratelimit.json') as f: resp_data = f.read() + responses.add(GET, DEFAULT_URL, body=resp_data) - url = '%s/application/rate_limit_status.json' % self.api.base_url - responses.add( - responses.GET, - url, - body=resp_data, - match_querystring=True, - status=200) self.api.InitializeRateLimit() self.assertTrue(self.api.rate_limit) @@ -76,13 +71,8 @@ def testInitializeRateLimit(self): def testCheckRateLimit(self): with open('testdata/ratelimit.json') as f: resp_data = f.read() - url = '%s/application/rate_limit_status.json' % self.api.base_url - responses.add( - responses.GET, - url, - body=resp_data, - match_querystring=True, - status=200) + responses.add(GET, DEFAULT_URL, body=resp_data) + rt = self.api.CheckRateLimit('https://api.twitter.com/1.1/help/privacy.json') self.assertEqual(rt.limit, 15) self.assertEqual(rt.remaining, 15) @@ -107,7 +97,7 @@ def setUp(self): with open('testdata/ratelimit.json') as f: resp_data = f.read() - url = '%s/application/rate_limit_status.json' % self.api.base_url + url = '%s/application/rate_limit_status.json?tweet_mode=compat' % self.api.base_url responses.add( responses.GET, url, @@ -223,13 +213,12 @@ def testLimitsViaHeadersWithSleep(self): sleep_on_rate_limit=True) # Add handler for ratelimit check - url = '%s/application/rate_limit_status.json' % api.base_url + url = '%s/application/rate_limit_status.json?tweet_mode=compat' % api.base_url responses.add( method=responses.GET, url=url, body='{}', match_querystring=True) # Get initial rate limit data to populate api.rate_limit object - url = "{0}/search/tweets.json?result_type=mixed&q=test&count=15".format( - api.base_url) + url = "https://api.twitter.com/1.1/search/tweets.json?tweet_mode=compat&q=test&count=15&result_type=mixed" responses.add( method=responses.GET, url=url, diff --git a/tests/test_tweet_changes.py b/tests/test_tweet_changes.py new file mode 100644 index 00000000..bec70f6f --- /dev/null +++ b/tests/test_tweet_changes.py @@ -0,0 +1,62 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import re +import sys +import unittest +import warnings + +import twitter +import responses +from responses import GET + +warnings.filterwarnings('ignore', category=DeprecationWarning) + +DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') + + +class ModelsChangesTest(unittest.TestCase): + """Test how changes to tweets affect model creation""" + + def setUp(self): + self.api = twitter.Api( + consumer_key='test', + consumer_secret='test', + access_token_key='test', + access_token_secret='test', + sleep_on_rate_limit=False) + + @responses.activate + def test_extended_in_compat_mode(self): + """API is in compatibility mode, but we call GetStatus on a tweet that + was written in extended mode. + + The tweet in question is exactly 140 characters and attaches a photo. + + """ + with open('testdata/3.2/extended_tweet_in_compat_mode.json') as f: + resp_data = f.read() + status = twitter.Status.NewFromJsonDict(json.loads(resp_data)) + self.assertTrue(status) + self.assertEqual(status.id, 782737772490600448) + self.assertEqual(status.text, "has more details about these changes. Thanks for making more expressive!writing requirements to python_twitt pytho… https://t.co/et3OTOxWSa") + self.assertEqual(status.tweet_mode, 'compatibility') + self.assertTrue(status.truncated) + + @responses.activate + def test_extended_in_extended_mode(self): + """API is in extended mode, and we call GetStatus on a tweet that + was written in extended mode. + + The tweet in question is exactly 140 characters and attaches a photo. + + """ + with open('testdata/3.2/extended_tweet_in_extended_mode.json') as f: + resp_data = f.read() + status = twitter.Status.NewFromJsonDict(json.loads(resp_data)) + self.assertTrue(status) + self.assertEqual(status.id, 782737772490600448) + self.assertEqual(status.full_text, "has more details about these changes. Thanks for making more expressive!writing requirements to python_twitt python_twitter.egg-info/SOURCE https://t.co/JWSPztfoyt") + self.assertEqual(status.tweet_mode, 'extended') + self.assertFalse(status.truncated) diff --git a/tests/test_tweet_length.py b/tests/test_tweet_length.py index 39c75c77..2853244d 100644 --- a/tests/test_tweet_length.py +++ b/tests/test_tweet_length.py @@ -22,12 +22,12 @@ def test_find_urls(self): self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) url = "HTTPS://www.ExaMPLE.COM/index.html" self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) - url = "http://user:PASSW0RD@example.com:8080/login.php" - self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) + # url = "http://user:PASSW0RD@example.com:8080/login.php" + # self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) url = "http://sports.yahoo.com/nfl/news;_ylt=Aom0;ylu=XyZ?slug=ap-superbowlnotebook" self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) - url = "http://192.168.0.1/index.html?src=asdf" - self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) + # url = "http://192.168.0.1/index.html?src=asdf" + # self.assertTrue(twitter.twitter_utils.is_url(url), "'{0}'".format(url)) # Have to figure out what a valid IPv6 range looks like, then # uncomment this. diff --git a/tests/test_unicode.py b/tests/test_unicode.py index e7edbc9d..0be70761 100644 --- a/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -14,6 +14,7 @@ DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') + class ErrNull(object): """ Suppress output of tests while writing to stdout or stderr. This just takes in data and does nothing with it. @@ -67,11 +68,7 @@ def test_trend_repr3(self): resp_data = f.read() responses.add( - responses.GET, - 'https://api.twitter.com/1.1/trends/place.json?id=1', - body=resp_data, - status=200, - match_querystring=True) + responses.GET, DEFAULT_URL, body=resp_data, match_querystring=True) resp = self.api.GetTrendsCurrent() for r in resp: diff --git a/tests/test_url_regex.py b/tests/test_url_regex.py new file mode 100644 index 00000000..4bf8e02b --- /dev/null +++ b/tests/test_url_regex.py @@ -0,0 +1,118 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import re +import sys +import unittest +import warnings + +import twitter +from twitter import twitter_utils + +import responses +from responses import GET, POST + +warnings.filterwarnings('ignore', category=DeprecationWarning) + + +DEFAULT_URL = re.compile(r'https?://.*\.twitter.com/1\.1/.*') +URLS = { + "is_url": [ + "t.co/test" + "http://foo.com/blah_blah", + "http://foo.com/blah_blah/", + "http://foo.com/blah_blah_(wikipedia)", + "http://foo.com/blah_blah_(wikipedia)_(again)", + "http://www.example.com/wpstyle/?p=364", + "https://www.example.com/foo/?bar=baz&inga=42&quux", + # "http://✪df.ws/123", + # "https://➡.ws/", + # "http://➡.ws/䨹", + # "http://⌘.ws", + # "http://⌘.ws/", + "http://foo.com/blah_(wikipedia)#cite-1", + "http://foo.com/blah_(wikipedia)_blah#cite-1", + "http://foo.com/(something)?after=parens", + # "http://☺.damowmow.com/", + "http://code.google.com/events/#&product=browser", + "http://j.mp", + "http://foo.bar/?q=Test%20URL-encoded%20stuff", + "http://1337.net", + "http://example.com/2.3.1.3/" + "http://a.b-c.de", + "foo.com" + ], + "is_not_url": [ + "http://userid:password@example.com:8080", + "http://userid:password@example.com:8080/", + "http://userid@example.com", + "http://userid@example.com/", + "http://userid@example.com:8080", + "http://userid@example.com:8080/", + "http://userid:password@example.com", + "http://userid:password@example.com/", + # "http://142.42.1.1/", + "2.3", + ".hello.com", + # "http://142.42.1.1:8080/", + "ftp://foo.bar/baz", + "http://مثال.إختبار", + "http://例子.测试", + "http://उदाहरण.परीक्षा", + "http://", + "http://.", + "http://..", + "http://../", + "http://?", + "http://??", + "http://??/", + "http://#", + "http://##", + "http://##/", + "//", + "//a", + "///a", + "///", + "http:///a", + "rdar://1234", + "h://test", + ":// should fail", + "ftps://foo.bar/", + "http://-error-.invalid/", + # "http://a.b--c.de/", + # "http://-a.b.co", + # "http://a.b-.co", + # "http://223.255.255.254", + # "http://0.0.0.0", + # "http://10.1.1.0", + # "http://10.1.1.255", + # "http://224.1.1.1", + # "http://1.1.1.1.1", + # "http://123.123.123", + "http://3628126748", + "http://.www.foo.bar/", + "http://.www.foo.bar./", + # "http://10.1.1.1" + ] +} + + +class TestUrlRegex(unittest.TestCase): + + def test_yes_urls(self): + for yes_url in URLS['is_url']: + self.assertTrue(twitter_utils.is_url(yes_url), yes_url) + + def test_no_urls(self): + for no_url in URLS['is_not_url']: + self.assertFalse(twitter_utils.is_url(no_url), no_url) + + def test_regex_finds_unicode(self): + string = "http://www.➡.ws" + string2 = "http://www.example.com" + pattern = re.compile(r'➡', re.U | re.I) + pattern2 = re.compile(r'(?:http?://|www\\.)*(?:[\w+-_][.])', re.I | re.U) + self.assertTrue(re.findall(pattern, string)) + self.assertTrue(re.findall(pattern2, string2)) + self.assertTrue(re.findall(pattern2, string)) diff --git a/twitter/__init__.py b/twitter/__init__.py index 75d57bb4..523a2b79 100644 --- a/twitter/__init__.py +++ b/twitter/__init__.py @@ -23,7 +23,7 @@ __email__ = 'python-twitter@googlegroups.com' __copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers' __license__ = 'Apache License 2.0' -__version__ = '3.1' +__version__ = '3.2dev0' __url__ = 'https://github.com/bear/python-twitter' __download_url__ = 'https://pypi.python.org/pypi/python-twitter' __description__ = 'A Python wrapper around the Twitter API' diff --git a/twitter/_file_cache.py b/twitter/_file_cache.py index 197b1909..39962457 100644 --- a/twitter/_file_cache.py +++ b/twitter/_file_cache.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import errno import os -import re import tempfile from hashlib import md5 @@ -47,7 +46,7 @@ def Remove(self, key): path = self._GetPath(key) if not path.startswith(self._root_directory): raise _FileCacheError('%s does not appear to live under %s' % - (path, self._root_directory )) + (path, self._root_directory)) if os.path.exists(path): os.remove(path) @@ -101,61 +100,3 @@ def _GetPath(self, key): def _GetPrefix(self, hashed_key): return os.path.sep.join(hashed_key[0:_FileCache.DEPTH]) - - -class ParseTweet(object): - # compile once on import - regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)", - "HASHTAG": r"(#[\w\d]+)", "URL": r"([http://]?[a-zA-Z\d\/]+[\.]+[a-zA-Z\d\/\.]+)"} - regexp = dict((key, re.compile(value)) for key, value in list(regexp.items())) - - def __init__(self, timeline_owner, tweet): - """ timeline_owner : twitter handle of user account. tweet - 140 chars from feed; object does all computation on construction - properties: - RT, MT - boolean - URLs - list of URL - Hashtags - list of tags - """ - self.Owner = timeline_owner - self.tweet = tweet - self.UserHandles = ParseTweet.getUserHandles(tweet) - self.Hashtags = ParseTweet.getHashtags(tweet) - self.URLs = ParseTweet.getURLs(tweet) - self.RT = ParseTweet.getAttributeRT(tweet) - self.MT = ParseTweet.getAttributeMT(tweet) - - # additional intelligence - if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet? - self.Owner = self.UserHandles[0] - return - - def __str__(self): - """ for display method """ - return "owner %s, urls: %d, hashtags %d, user_handles %d, len_tweet %d, RT = %s, MT = %s" % ( - self.Owner, len(self.URLs), len(self.Hashtags), len(self.UserHandles), - len(self.tweet), self.RT, self.MT) - - @staticmethod - def getAttributeRT(tweet): - """ see if tweet is a RT """ - return re.search(ParseTweet.regexp["RT"], tweet.strip()) is not None - - @staticmethod - def getAttributeMT(tweet): - """ see if tweet is a MT """ - return re.search(ParseTweet.regexp["MT"], tweet.strip()) is not None - - @staticmethod - def getUserHandles(tweet): - """ given a tweet we try and extract all user handles in order of occurrence""" - return re.findall(ParseTweet.regexp["ALNUM"], tweet) - - @staticmethod - def getHashtags(tweet): - """ return all hashtags""" - return re.findall(ParseTweet.regexp["HASHTAG"], tweet) - - @staticmethod - def getURLs(tweet): - """ URL : [http://]?[\w\.?/]+""" - return re.findall(ParseTweet.regexp["URL"], tweet) diff --git a/twitter/api.py b/twitter/api.py index 2ca730f4..e09edf1c 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -2,7 +2,7 @@ # # -# Copyright 2007 The Python-Twitter Developers +# Copyright 2007-2016 The Python-Twitter Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -156,39 +156,55 @@ def __init__(self, use_gzip_compression=False, debugHTTP=False, timeout=None, - sleep_on_rate_limit=False): + sleep_on_rate_limit=False, + tweet_mode='compat'): """Instantiate a new twitter.Api object. Args: - consumer_key: + consumer_key (str): Your Twitter user's consumer_key. - consumer_secret: + consumer_secret (str): Your Twitter user's consumer_secret. - access_token_key: + access_token_key (str): The oAuth access token key value you retrieved from running get_access_token.py. - access_token_secret: + access_token_secret (str): The oAuth access token's secret, also retrieved from the get_access_token.py run. - input_encoding: - The encoding used to encode input strings. [Optional] - request_header: - A dictionary of additional HTTP request headers. [Optional] - cache: + input_encoding (str, optional): + The encoding used to encode input strings. + request_header (dict, optional): + A dictionary of additional HTTP request headers. + cache (object, optional): The cache instance to use. Defaults to DEFAULT_CACHE. - Use None to disable caching. [Optional] - base_url: + Use None to disable caching. + base_url (str, optional): The base URL to use to contact the Twitter API. - Defaults to https://api.twitter.com. [Optional] - use_gzip_compression: + Defaults to https://api.twitter.com. + stream_url (str, optional): + The base URL to use for streaming endpoints. + Defaults to 'https://stream.twitter.com/1.1'. + upload_url (str, optional): + The base URL to use for uploads. Defaults to 'https://upload.twitter.com/1.1'. + chunk_size (int, optional): + Chunk size to use for chunked (multi-part) uploads of images/videos/gifs. + Defaults to 1MB. Anything under 16KB and you run the risk of erroring out + on 15MB files. + use_gzip_compression (bool, optional): Set to True to tell enable gzip compression for any call - made to Twitter. Defaults to False. [Optional] - debugHTTP: + made to Twitter. Defaults to False. + debugHTTP (bool, optional): Set to True to enable debug output from urllib2 when performing - any HTTP requests. Defaults to False. [Optional] - timeout: + any HTTP requests. Defaults to False. + timeout (int, optional): Set timeout (in seconds) of the http/https requests. If None the - requests lib default will be used. Defaults to None. [Optional] + requests lib default will be used. Defaults to None. + sleep_on_rate_limit (bool, optional): + Whether to sleep an appropriate amount of time if a rate limit is hit for + an endpoint. + tweet_mode (str, optional): + Whether to use the new (as of Sept. 2016) extended tweet mode. See docs for + details. Choices are ['compatibility', 'extended']. """ # check to see if the library is running on a Google App Engine instance @@ -214,6 +230,7 @@ def __init__(self, self.rate_limit = RateLimit() self.sleep_on_rate_limit = sleep_on_rate_limit + self.tweet_mode = tweet_mode if base_url is None: self.base_url = 'https://api.twitter.com/1.1' @@ -295,6 +312,14 @@ def SetCredentials(self, self._config = None def GetHelpConfiguration(self): + """Get basic help configuration details from Twitter. + + Args: + None + + Returns: + dict: Sets self._config and returns dict of help config values. + """ if self._config is None: url = '%s/help/configuration.json' % self.base_url resp = self._RequestUrl(url, 'GET') @@ -303,6 +328,15 @@ def GetHelpConfiguration(self): return self._config def GetShortUrlLength(self, https=False): + """Returns number of characters reserved per URL included in a tweet. + + Args: + https (bool, optional): + If True, return number of characters reserved for https urls + or, if False, return number of character reserved for http urls. + Returns: + (int): Number of characters reserved per URL. + """ config = self.GetHelpConfiguration() if https: return config['short_url_length_https'] @@ -343,6 +377,7 @@ def GetSearch(self, type checking and ensuring that the query string is properly formatted, as it will only be URL-encoded before be passed directly to Twitter with no other checks performed. For advanced usage only. + *This will override any other parameters passed* since_id (int, optional): Returns results with an ID greater than (that is, more recent than) the specified ID. There are limits to the number of @@ -890,64 +925,79 @@ def PostUpdate(self, media_additional_owners=None, media_category=None, in_reply_to_status_id=None, + auto_populate_reply_metadata=False, + exclude_reply_user_ids=None, latitude=None, longitude=None, place_id=None, display_coordinates=False, trim_user=False, - verify_status_length=True): + verify_status_length=True, + attachment_url=None): """Post a twitter status message from the authenticated user. https://dev.twitter.com/docs/api/1.1/post/statuses/update Args: - status: - The message text to be posted. Must be less than or equal to 140 - characters. - media: - A URL, a local file, or a file-like object (something with a read() - method), or a list of any combination of the above. - media_additional_owners: - A list of user ids representing Twitter users that should be able - to use the uploaded media in their tweets. If you pass a list of - media, then additional_owners will apply to each object. If you - need more granular control, please use the UploadMedia* methods. - media_category: - Only for use with the AdsAPI. See - https://dev.twitter.com/ads/creative/promoted-video-overview if - this applies to your application. - in_reply_to_status_id: - The ID of an existing status that the status to be posted is - in reply to. This implicitly sets the in_reply_to_user_id - attribute of the resulting status to the user ID of the - message being replied to. Invalid/missing status IDs will be - ignored. [Optional] - latitude: - Latitude coordinate of the tweet in degrees. Will only work - in conjunction with longitude argument. Both longitude and - latitude will be ignored by twitter if the user has a false - geo_enabled setting. [Optional] - longitude: - Longitude coordinate of the tweet in degrees. Will only work - in conjunction with latitude argument. Both longitude and - latitude will be ignored by twitter if the user has a false - geo_enabled setting. [Optional] - place_id: - A place in the world. These IDs can be retrieved from - GET geo/reverse_geocode. [Optional] - display_coordinates: - Whether or not to put a pin on the exact coordinates a tweet - has been sent from. [Optional] - trim_user: - If True the returned payload will only contain the user IDs, - otherwise the payload will contain the full user data item. - [Optional] - verify_status_length: - If True, api throws a hard error that the status is over - 140 characters. If False, Api will attempt to post the - status. [Optional] + status (str): + The message text to be posted. Must be less than or equal to 140 + characters. + media (int, str, fp, optional): + A URL, a local file, or a file-like object (something with a + read() method), or a list of any combination of the above. + media_additional_owners (list, optional): + A list of user ids representing Twitter users that should be able + to use the uploaded media in their tweets. If you pass a list of + media, then additional_owners will apply to each object. If you + need more granular control, please use the UploadMedia* methods. + media_category (str, optional): + Only for use with the AdsAPI. See + https://dev.twitter.com/ads/creative/promoted-video-overview if + this applies to your application. + in_reply_to_status_id (int, optional): + The ID of an existing status that the status to be posted is + in reply to. This implicitly sets the in_reply_to_user_id + attribute of the resulting status to the user ID of the + message being replied to. Invalid/missing status IDs will be + ignored. + auto_populate_reply_metadata (bool, optional): + Automatically include the @usernames of the users mentioned or + participating in the tweet to which this tweet is in reply. + exclude_reply_user_ids (list, optional): + Remove given user_ids (*not* @usernames) from the tweet's + automatically generated reply metadata. + attachment_url (str, optional): + URL to an attachment resource: one to four photos, a GIF, + video, Quote Tweet, or DM deep link. If not specified and + media parameter is not None, we will attach the first media + object as the attachment URL. If a bad URL is passed, Twitter + will raise an error. + latitude (float, optional): + Latitude coordinate of the tweet in degrees. Will only work + in conjunction with longitude argument. Both longitude and + latitude will be ignored by twitter if the user has a false + geo_enabled setting. + longitude (float, optional): + Longitude coordinate of the tweet in degrees. Will only work + in conjunction with latitude argument. Both longitude and + latitude will be ignored by twitter if the user has a false + geo_enabled setting. + place_id (int, optional): + A place in the world. These IDs can be retrieved from + GET geo/reverse_geocode. + display_coordinates (bool, optional): + Whether or not to put a pin on the exact coordinates a tweet + has been sent from. + trim_user (bool, optional): + If True the returned payload will only contain the user IDs, + otherwise the payload will contain the full user data item. + verify_status_length (bool, optional): + If True, api throws a hard error that the status is over + 140 characters. If False, Api will attempt to post the + status. Returns: - A twitter.Status instance representing the message posted. + (twitter.Status) A twitter.Status instance representing the + message posted. """ url = '%s/statuses/update.json' % self.base_url @@ -959,7 +1009,21 @@ def PostUpdate(self, if verify_status_length and calc_expected_status_length(u_status) > 140: raise TwitterError("Text must be less than or equal to 140 characters.") - parameters = {'status': u_status} + if auto_populate_reply_metadata and not in_reply_to_status_id: + raise TwitterError("If auto_populate_reply_metadata is True, you must set in_reply_to_status_id") + + parameters = { + 'status': u_status, + 'in_reply_to_status_id': in_reply_to_status_id, + 'auto_populate_reply_metadata': auto_populate_reply_metadata, + 'place_id': place_id, + 'display_coordinates': display_coordinates, + 'trim_user': trim_user, + 'exclude_reply_user_ids': ','.join([str(u) for u in exclude_reply_user_ids or []]), + } + + if attachment_url: + parameters['attachment_url'] = attachment_url if media: media_ids = [] @@ -993,25 +1057,16 @@ def PostUpdate(self, else: _, _, file_size, _ = parse_media_file(media) if file_size > self.chunk_size: - media_ids.append(self.UploadMediaChunked(media, - media_additional_owners)) + media_ids.append( + self.UploadMediaChunked(media, media_additional_owners)) else: media_ids.append( - self.UploadMediaSimple(media, - media_additional_owners)) + self.UploadMediaSimple(media, media_additional_owners)) parameters['media_ids'] = ','.join([str(mid) for mid in media_ids]) - if in_reply_to_status_id: - parameters['in_reply_to_status_id'] = in_reply_to_status_id if latitude is not None and longitude is not None: parameters['lat'] = str(latitude) parameters['long'] = str(longitude) - if place_id is not None: - parameters['place_id'] = str(place_id) - if display_coordinates: - parameters['display_coordinates'] = 'true' - if trim_user: - parameters['trim_user'] = 'true' resp = self._RequestUrl(url, 'POST', data=parameters) data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) @@ -1044,7 +1099,7 @@ def UploadMediaSimple(self, url = '%s/media/upload.json' % self.upload_url parameters = {} - media_fp, filename, file_size, media_type = parse_media_file(media) + media_fp, _, _, _ = parse_media_file(media) parameters['media'] = media_fp.read() @@ -3243,7 +3298,6 @@ def IncomingFriendship(self, parameters['count'] = int(cursor) except ValueError: raise TwitterError({'message': "cursor must be an integer"}) - break resp = self._RequestUrl(url, 'GET', data=parameters) data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) result += [x for x in data['ids']] @@ -3288,7 +3342,6 @@ def OutgoingFriendship(self, parameters['count'] = int(cursor) except ValueError: raise TwitterError({'message': "cursor must be an integer"}) - break resp = self._RequestUrl(url, 'GET', data=parameters) data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) result += [x for x in data['ids']] @@ -4335,7 +4388,8 @@ def UpdateBackgroundImage(self, tile=False, include_entities=False, skip_status=False): - + """Deprecated function. Used to update the background of a User's + Twitter profile. Removed in approx. July, 2015""" warnings.warn(( "This method has been deprecated by Twitter as of July 2015 and " "will be removed in future versions of python-twitter."), @@ -4365,6 +4419,20 @@ def UpdateImage(self, image, include_entities=False, skip_status=False): + """Update a User's profile image. Change may not be immediately + reflected due to image processing on Twitter's side. + + Args: + image (str): + Location of local image file to use. + include_entities (bool, optional): + Include the entities node in the return data. + skip_status (bool, optional): + Include the User's last Status in the User entity returned. + + Returns: + (twitter.models.User): Updated User object. + """ url = '%s/account/update_profile_image.json' % (self.base_url) with open(image, 'rb') as image_file: @@ -4429,7 +4497,7 @@ def UpdateBanner(self, raise TwitterError({'message': "Unkown banner image upload issue"}) - def GetStreamSample(self, delimited=None, stall_warnings=None): + def GetStreamSample(self, delimited=False, stall_warnings=True): """Returns a small sample of public statuses. Args: @@ -4442,7 +4510,11 @@ def GetStreamSample(self, delimited=None, stall_warnings=None): A Twitter stream """ url = '%s/statuses/sample.json' % self.stream_url - resp = self._RequestStream(url, 'GET') + parameters = { + 'delimited': bool(delimited), + 'stall_warnings': bool(stall_warnings) + } + resp = self._RequestStream(url, 'GET', data=parameters) for line in resp.iter_lines(): if line: data = self._ParseAndCheckTwitter(line.decode('utf-8')) @@ -4452,6 +4524,7 @@ def GetStreamFilter(self, follow=None, track=None, locations=None, + languages=None, delimited=None, stall_warnings=None): """Returns a filtered view of public statuses. @@ -4468,6 +4541,10 @@ def GetStreamFilter(self, Specifies a message length. [Optional] stall_warnings: Set to True to have Twitter deliver stall warnings. [Optional] + languages: + A list of Languages. + Will only return Tweets that have been detected as being + written in the specified languages. [Optional] Returns: A twitter stream @@ -4486,6 +4563,8 @@ def GetStreamFilter(self, data['delimited'] = str(delimited) if stall_warnings is not None: data['stall_warnings'] = str(stall_warnings) + if languages is not None: + data['language'] = ','.join(languages) resp = self._RequestStream(url, 'POST', data=data) for line in resp.iter_lines(): @@ -4732,7 +4811,8 @@ def _InitializeUserAgent(self): def _InitializeDefaultParameters(self): self._default_params = {} - def _DecompressGzippedResponse(self, response): + @staticmethod + def _DecompressGzippedResponse(response): raw_data = response.read() if response.headers.get('content-encoding', None) == 'gzip': url_data = gzip.GzipFile(fileobj=io.StringIO(raw_data)).read() @@ -4740,7 +4820,8 @@ def _DecompressGzippedResponse(self, response): url_data = raw_data return url_data - def _EncodeParameters(self, parameters): + @staticmethod + def _EncodeParameters(parameters): """Return a string in key=value&key=value form. Values of None are not included in the output string. @@ -4766,38 +4847,32 @@ def _ParseAndCheckTwitter(self, json_data): This is a purely defensive check because during some Twitter network outages it will return an HTML failwhale page. """ - data = None try: data = json.loads(json_data) - try: - self._CheckForTwitterError(data) - - except ValueError: - if "Twitter / Over capacity" in json_data: - raise TwitterError({'message': "Capacity Error"}) - if "Twitter / Error" in json_data: - raise TwitterError({'message': "Technical Error"}) - if "Exceeded connection limit for user" in json_data: - raise TwitterError({'message': "Exceeded connection limit for user"}) - if "Error 401 Unauthorized" in json_data: - raise TwitterError({'message': "Unauthorized"}) - raise TwitterError({'message': "Unknown error, try addeding "}) - - except: + except ValueError: + if "Twitter / Over capacity" in json_data: + raise TwitterError({'message': "Capacity Error"}) + if "Twitter / Error" in json_data: + raise TwitterError({'message': "Technical Error"}) + if "Exceeded connection limit for user" in json_data: + raise TwitterError({'message': "Exceeded connection limit for user"}) if "Error 401 Unauthorized" in json_data: raise TwitterError({'message': "Unauthorized"}) - + raise TwitterError({'Unknown error: {0}'.format(json_data)}) + self._CheckForTwitterError(data) return data - def _CheckForTwitterError(self, data): + @staticmethod + def _CheckForTwitterError(data): """Raises a TwitterError if twitter returns an error message. Args: - data: - A python dict created from the Twitter json response + data (dict): + A python dict created from the Twitter json response Raises: - TwitterError wrapping the twitter error message if one exists. + (twitter.TwitterError): TwitterError wrapping the twitter error + message if one exists. """ # Twitter errors are relatively unlikely, so it is faster # to check first, rather than try and catch the exception @@ -4833,8 +4908,7 @@ def _RequestUrl(self, url, verb, data=None, json=None): A JSON object. """ if not self.__auth: - raise TwitterError( - "The twitter.Api instance must be authenticated.") + raise TwitterError("The twitter.Api instance must be authenticated.") if url and self.sleep_on_rate_limit: limit = self.CheckRateLimit(url) @@ -4844,6 +4918,8 @@ def _RequestUrl(self, url, verb, data=None, json=None): time.sleep(max(int(limit.reset - time.time()) + 2, 0)) except ValueError: pass + if not data: + data = {} if verb == 'POST': if data: @@ -4860,6 +4936,7 @@ def _RequestUrl(self, url, verb, data=None, json=None): resp = 0 # POST request, but without data or json elif verb == 'GET': + data['tweet_mode'] = self.tweet_mode url = self._BuildUrl(url, extra_params=data) resp = requests.get(url, auth=self.__auth, timeout=self._timeout) diff --git a/twitter/models.py b/twitter/models.py index 5c92b030..61fb1935 100644 --- a/twitter/models.py +++ b/twitter/models.py @@ -78,11 +78,14 @@ def NewFromJsonDict(cls, data, **kwargs): """ + json_data = data.copy() if kwargs: for key, val in kwargs.items(): - data[key] = val + json_data[key] = val - return cls(**data) + c = cls(**json_data) + c._json = data + return c class Media(TwitterModel): @@ -100,6 +103,7 @@ def __init__(self, **kwargs): 'sizes': None, 'type': None, 'url': None, + 'video_info': None, } for (param, default) in self.param_defaults.items(): @@ -379,6 +383,7 @@ def __init__(self, **kwargs): 'current_user_retweet': None, 'favorite_count': None, 'favorited': None, + 'full_text': None, 'geo': None, 'hashtags': None, 'id': None, @@ -409,6 +414,11 @@ def __init__(self, **kwargs): for (param, default) in self.param_defaults.items(): setattr(self, param, kwargs.get(param, default)) + if kwargs.get('full_text', None): + self.tweet_mode = 'extended' + else: + self.tweet_mode = 'compatibility' + @property def created_at_in_seconds(self): """ Get the time this status message was posted, in seconds since diff --git a/twitter/parse_tweet.py b/twitter/parse_tweet.py index b1fe6064..c662016e 100644 --- a/twitter/parse_tweet.py +++ b/twitter/parse_tweet.py @@ -2,6 +2,7 @@ import re + class Emoticons: POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *", ":P", ":D", ":d", ":p", @@ -27,6 +28,7 @@ class Emoticons: "[:", ";]" ] + class ParseTweet(object): # compile once on import regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)", @@ -51,7 +53,7 @@ def __init__(self, timeline_owner, tweet): self.Emoticon = ParseTweet.getAttributeEmoticon(tweet) # additional intelligence - if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet? + if (self.RT and len(self.UserHandles) > 0): # change the owner of tweet? self.Owner = self.UserHandles[0] return @@ -66,10 +68,10 @@ def getAttributeEmoticon(tweet): emoji = list() for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()): if tok in Emoticons.POSITIVE: - emoji.append( tok ) + emoji.append(tok) continue if tok in Emoticons.NEGATIVE: - emoji.append( tok ) + emoji.append(tok) return emoji @staticmethod diff --git a/twitter/ratelimit.py b/twitter/ratelimit.py index 552373a3..3a717b38 100644 --- a/twitter/ratelimit.py +++ b/twitter/ratelimit.py @@ -118,8 +118,7 @@ def url_to_resource(url): for non_std_endpoint in NON_STANDARD_ENDPOINTS: if re.match(non_std_endpoint.regex, resource): return non_std_endpoint.resource - else: - return resource + return resource def set_unknown_limit(self, url, limit, remaining, reset): return self.set_limit(url, limit, remaining, reset) diff --git a/twitter/twitter_utils.py b/twitter/twitter_utils.py index 883d62b2..081d1ed9 100644 --- a/twitter/twitter_utils.py +++ b/twitter/twitter_utils.py @@ -1,11 +1,12 @@ # encoding: utf-8 +from __future__ import unicode_literals + import mimetypes import os import re - -import requests from tempfile import NamedTemporaryFile +import requests from twitter import TwitterError @@ -138,7 +139,14 @@ "淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团", "飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"] -URL_REGEXP = re.compile(r'(?i)((?:https?://|www\\.)*(?:[\w+-_]+[.])(?:' + r'\b|'.join(TLDS) + r'\b|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))+(?:[:\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*)', re.UNICODE) +URL_REGEXP = re.compile(( + r'(' + r'^(?!(https?://|www\.)?\.|ftps?://|([0-9]+\.){{1,3}}\d+)' # exclude urls that start with "." + r'(?:https?://|www\.)*^(?!.*@)(?:[\w+-_]+[.])' # beginning of url + r'(?:{0}\b|' # all tlds + r'(?:[:0-9]))' # port numbers & close off TLDs + r'(?:[\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*' # path/query params + r')').format(r'\b|'.join(TLDS)), re.U | re.I | re.X) def calc_expected_status_length(status, short_url_length=23): @@ -171,10 +179,7 @@ def is_url(text): Returns: Boolean of whether the text should be treated as a URL or not. """ - if re.findall(URL_REGEXP, text): - return True - else: - return False + return bool(re.findall(URL_REGEXP, text)) def http_to_file(http):